forty-cdk
llms.txt

Primitives

File Upload

A headless drag-and-drop / dialog file-selection zone: a visually-hidden native <input type="file"> stays the accessible control while a trigger button opens the picker, and dropping files emits the same change. Supports multiple, accept filters and whole-folder (directory) selection.

forty-cdk/file-upload

Click the zone or drop a file on it. The zone sets data-dragging while a file hovers it, and the chosen files arrive as a FileList you render yourself.

or drag and drop

Any file type

No ARIA role is imposed on the drop zone, which is a plain container. The <input type="file"> remains the accessible form control; the trigger is a native <button>.

Anatomy

<div forFileUpload accept="image/*,.pdf" (filesChange)="onFiles($event)">
  <input forFileUploadInput aria-label="Upload files" class="sr-only" />
  <button forFileUploadTrigger>Choose files</button>
  <p>or drag and drop</p>
</div>

Examples

Multiple files

multiple lets the picker (and a drop) accept more than one file at once, and accept narrows the chooser to the MIME types you list.

or drag and drop

Images only — select as many as you like

States

One class and one directive, two states. disabled blocks the dialog and drops alike. It also reflects data-disabled on the zone, so the zone dims and ignores pointer events from the same stylesheet that styles data-dragging, without the input leaving the DOM.

Default

or drag and drop

Any file type

Disabled

or drag and drop

Uploads are paused

Folder selection

Set directory to switch the native picker into folder mode. The input mirrors it as webkitdirectory, which modern Chromium, Firefox and WebKit all support despite the prefix. Choose a folder and the emitted FileList holds every file inside it, each carrying a webkitRelativePath to rebuild the tree from. Dropping a folder is out of scope: a drop surfaces DataTransfer.files only, with no webkitGetAsEntry traversal.

The whole folder’s contents are read, recursively.

Handling rejections

Every file the zone refuses arrives on filesRejected with the reason it was refused, and never reaches filesChange or the native input's files. A file outside accept is rejected with 'accept', and a file larger than maxSize bytes with 'size'; a file of exactly maxSize bytes is accepted. With multiple off, the zone keeps the first accepted file and surfaces the extras with the reason 'multiple'. That cap is applied last, so a refused file never takes the one slot from a valid file behind it. Nothing is discarded silently, so you can tell the user why a file did not go through. Combining directory with multiple off is noisy by design: every file in the chosen folder past the first is reported as a 'multiple' rejection.

<div forFileUpload accept="image/*" [maxSize]="5000000" (filesRejected)="onRejected($event)">
  <input forFileUploadInput aria-label="Upload an image" class="sr-only" />
  <button forFileUploadTrigger>Choose an image</button>
</div>
onRejected(rejections: ForFileUploadRejection[]): void {
  for (const { file, reason } of rejections) {
    console.warn(`${file.name} rejected: ${reason}`);
  }
}

Disabled

<div forFileUpload [disabled]="isDisabled()">
  <input forFileUploadInput aria-label="Upload files" class="sr-only" />
  <button forFileUploadTrigger>Choose files</button>
</div>

API

ForFileUpload

PropertyTypeDescription
accept
string | null
MIME types or file extensions accepted by the chooser (e.g. "image/*,.pdf").
Default: null
multiple
boolean
Whether multiple files can be selected at once.
Default: false
directory
boolean
Whether the picker selects a whole folder (mirrored as webkitdirectory).
Default: false
maxSize
number | null
Largest accepted file size, in bytes; a file of exactly this size is accepted. null sets no limit.
Default: null
disabled
boolean
Whether the zone and all its pieces are disabled.
Default: false
OutputTypeDescription
filesChange
FileList
Files chosen via the dialog or dropped onto the zone, filtered against accept and maxSize before emission through either path.
filesRejected
ForFileUploadRejection[]
Files refused by accept, by maxSize or by the single-file cap of multiple="false", each paired with the reason ('accept' / 'size' / 'multiple'). Fires only when at least one file was refused; every selected file lands in exactly one output.
Data attributeValues
data-draggingpresent while files are dragged over the drop zone, else absent
data-disabledpresent when the zone (and all its pieces) is disabled, else absent

Accessibility

  • The <input type="file"> is the accessible control. Keep it reachable with a visually-hidden utility class (sr-only / visually-hidden) rather than display: none or visibility: hidden, which would remove it from the tab order and from assistive technology.
  • Label the input. Supply aria-label directly on [forFileUploadInput] (as in the examples above), or wrap it in a <label>.
  • The trigger is a native <button>. It receives focus, is announced as a button, and activates the file dialog via click / Enter / Space. No ARIA role augmentation is needed.

Styling

forty-cdk ships no styles: put your own class on each piece and key your CSS off the data-* attributes listed under API, not off the for* selectors (Styling forty-cdk explains why).

Wrapping in a design system

Subclass the root and re-provide FOR_FILE_UPLOAD_CONTEXT with useExisting pointing at the subclass, since Angular does not inherit a directive's providers; Wrapping non-form roots walks the pattern.