Form
File Upload
Usage#
import { WdsFileUploadComponent } from '@wds/angular/file-upload';files (or the form data) and upload them yourself, showing the progress with setFileStatus(). Name the field with a wds-field-label in a wds-field (or label).
Examples#
Default#
multiple a new file replaces the previous one. The hint comes from accept and max-size (in bytes); images get a preview.
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsFileUploadComponent } from '@wds/angular/file-upload';
@Component({
selector: 'app-file-upload-default-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFileUploadComponent],
template: `
<wds-field class="w-full max-w-md">
<wds-field-label>Cover image</wds-field-label>
<wds-file-upload accept="image/*" max-size="5242880"></wds-file-upload>
</wds-field>
`,
})
export class FileUploadDefaultDemo {}Multiple files#
multiple keeps adding files to the list, up to max-files. The same file added twice is listed once.
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldDescriptionComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsFileUploadComponent } from '@wds/angular/file-upload';
@Component({
selector: 'app-file-upload-multiple-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFieldDescriptionComponent, WdsFileUploadComponent],
template: `
<wds-field class="w-full max-w-md">
<wds-field-label>Attachments</wds-field-label>
<wds-file-upload multiple max-files="5" max-size="10485760" accept="image/*,.pdf"></wds-file-upload>
<wds-field-description>Invoices or photos of receipts.</wds-field-description>
</wds-field>
`,
})
export class FileUploadMultipleDemo {}Upload progress and rejected files#
wds-change gives the files; setFileStatus(file, { progress }) shows a progress bar under a file, { error } an error, and null clears it. Files that do not pass accept, max-size or max-files are not added and fire wds-reject with the reason.
import { Component, signal, viewChild } from '@angular/core';
import type { WdsFileUploadRejectDetail } from '@wds/core/file-upload';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsFileUploadComponent } from '@wds/angular/file-upload';
@Component({
selector: 'app-file-upload-progress-demo',
imports: [...WDS_FIELD, WdsFileUploadComponent],
template: `
<div class="flex w-full max-w-md flex-col gap-3">
<wds-field>
<wds-field-label>Photos</wds-field-label>
<wds-file-upload
multiple
accept="image/*"
max-size="2097152"
(wdsChange)="startUploads($event.files)"
(wdsReject)="showRejected($event)"
/>
</wds-field>
@if (rejected()) {
<p class="m-0 text-sm text-destructive">{{ rejected() }}</p>
}
</div>
`,
})
export class FileUploadProgressDemo {
private readonly upload = viewChild.required(WdsFileUploadComponent);
private readonly started = new WeakSet<File>();
protected readonly rejected = signal('');
/** The element only collects files; this fake upload shows the progress with setFileStatus(). */
protected startUploads(files: File[]): void {
const el = this.upload();
for (const file of files) {
if (this.started.has(file)) continue;
this.started.add(file);
let progress = 0;
const timer = setInterval(() => {
progress = Math.min(100, progress + 10 + Math.random() * 20);
el.setFileStatus(file, { progress: Math.round(progress) });
if (progress >= 100) {
clearInterval(timer);
setTimeout(() => el.setFileStatus(file, null), 400);
}
}, 300);
}
}
protected showRejected(detail: WdsFileUploadRejectDetail): void {
const why = { size: 'larger than 2 MB', type: 'not an image', count: 'too many files' };
this.rejected.set(detail.rejected.map(({ file, reason }) => `${file.name}: ${why[reason]}`).join('. '));
}
}Custom drop zone#
--wds-file-upload-min-height: 0 makes the zone as low as its content. Use text and icons only: the drop zone is a button.
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsFileUploadComponent } from '@wds/angular/file-upload';
@Component({
selector: 'app-file-upload-compact-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFileUploadComponent],
template: `
<wds-field class="w-full max-w-md">
<wds-field-label>Resume</wds-field-label>
<wds-file-upload accept=".pdf,.docx" class="[--wds-file-upload-min-height:0]">
<span class="flex items-center gap-2 text-sm">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" /></svg>
<span><span class="font-medium underline underline-offset-3">Attach a file</span> <span class="text-muted-foreground">PDF or DOCX</span></span>
</span>
</wds-file-upload>
</wds-field>
`,
})
export class FileUploadCompactDemo {}Disabled#
disabled blocks the picker and dropping, and excludes the files from the form.
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldDescriptionComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsFileUploadComponent } from '@wds/angular/file-upload';
@Component({
selector: 'app-file-upload-disabled-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFieldDescriptionComponent, WdsFileUploadComponent],
template: `
<wds-field class="w-full max-w-md" disabled>
<wds-field-label>Documents</wds-field-label>
<wds-file-upload multiple disabled></wds-file-upload>
<wds-field-description>Uploads are paused during maintenance.</wds-field-description>
</wds-field>
`,
})
export class FileUploadDisabledDemo {}Forms#
Integration with Angular forms. Pick the API your app uses.
Signal forms#
WdsFileUploadField connects the field to [formField]; the model is File[]. In signal forms required() does not treat an empty array as empty, so check the number of files with validate() (here with requiredError); required() still marks the field as required.
import { Component, computed, signal } from '@angular/core';
import { FormField, FormRoot, form, required, requiredError, validate } from '@angular/forms/signals';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsFileUploadComponent } from '@wds/angular/file-upload';
import { WdsFileUploadField } from '@wds/angular/forms';
interface Claim {
receipts: File[];
}
@Component({
selector: 'app-file-upload-signal-forms-demo',
imports: [FormField, FormRoot, WdsButtonComponent, ...WDS_FIELD, WdsFileUploadComponent, WdsFileUploadField],
template: `
<form class="flex w-md max-w-full flex-col gap-6" [formRoot]="claimForm">
<wds-field [invalid]="showError()">
<wds-field-label>Receipts</wds-field-label>
<wds-file-upload [formField]="claimForm.receipts" multiple accept="image/*,.pdf" />
<wds-field-error [errors]="showError() ? claimForm.receipts().errors() : []" />
</wds-field>
<wds-button type="submit" class="self-start">Submit claim</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class FileUploadSignalFormsDemo {
protected readonly claim = signal<Claim>({ receipts: [] });
protected readonly submitted = signal('—');
protected readonly claimForm = form(
this.claim,
(path) => {
// required() marks the field as required, but in signal forms an empty array is not "empty": check the length too.
required(path.receipts);
validate(path.receipts, ({ value }) => (value().length ? undefined : requiredError({ message: 'Add at least one receipt.' })));
},
{
submission: {
action: async (form) => {
this.submitted.set(form().value().receipts.map((f) => f.name).join(', '));
},
},
}
);
/** The error appears after the user leaves the field or submits the form. */
protected readonly showError = computed(() => this.claimForm.receipts().touched() && this.claimForm.receipts().invalid());
}Template-driven#
WdsFileUploadValueAccessor makes ngModel work on wds-file-upload. Inside a <form> the field needs a name.
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsFileUploadComponent } from '@wds/angular/file-upload';
import { WdsFileUploadValueAccessor } from '@wds/angular/forms';
@Component({
selector: 'app-file-upload-template-driven-demo',
imports: [FormsModule, WdsButtonComponent, ...WDS_FIELD, WdsFileUploadComponent, WdsFileUploadValueAccessor],
template: `
<form class="flex w-md max-w-full flex-col gap-6" (ngSubmit)="submitted.set(names())">
<wds-field>
<wds-field-label>Receipts</wds-field-label>
<wds-file-upload name="receipts" [(ngModel)]="receipts" multiple accept="image/*,.pdf" />
</wds-field>
<wds-button type="submit" class="self-start">Submit claim</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class FileUploadTemplateDrivenDemo {
protected receipts: File[] = [];
protected readonly submitted = signal('—');
protected names(): string {
return this.receipts.map((f) => f.name).join(', ') || '—';
}
}Reactive forms#
WdsFileUploadValueAccessor works with formControl and formControlName; an empty list fails Validators.required.
import { Component, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsFileUploadComponent } from '@wds/angular/file-upload';
import { WdsFileUploadValueAccessor } from '@wds/angular/forms';
@Component({
selector: 'app-file-upload-reactive-forms-demo',
imports: [ReactiveFormsModule, WdsButtonComponent, ...WDS_FIELD, WdsFileUploadComponent, WdsFileUploadValueAccessor],
template: `
<form class="flex w-md max-w-full flex-col gap-6" [formGroup]="claimForm" (ngSubmit)="save()">
<wds-field [invalid]="receipts.touched && receipts.invalid">
<wds-field-label>Receipts</wds-field-label>
<wds-file-upload formControlName="receipts" multiple accept="image/*,.pdf" />
@if (receipts.touched && receipts.invalid) {
<wds-field-error>Add at least one receipt.</wds-field-error>
}
</wds-field>
<wds-button type="submit" class="self-start">Submit claim</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class FileUploadReactiveFormsDemo {
protected readonly claimForm = new FormGroup({
// The value is a File[]; an empty list fails Validators.required.
receipts: new FormControl<File[]>([], { nonNullable: true, validators: Validators.required }),
});
protected readonly receipts = this.claimForm.controls.receipts;
protected readonly submitted = signal('—');
protected save(): void {
this.claimForm.markAllAsTouched();
if (this.claimForm.valid) this.submitted.set(this.receipts.value.map((f) => f.name).join(', '));
}
}API References#
wds-file-upload#
accept, max-size and
max-files filter the files as they are added; the rejected ones fire wds-reject.
The element does not upload anything: read files (or the form data) and send them yourself. During an upload
setFileStatus() shows a progress bar or an error under a file. In a <form> the files are submitted under
name, and required validates as in a native file field.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| accept | string | '' | <input type="file">: extensions and MIME types, e.g. image/*,.pdf. |
| multiple | boolean | false | |
| max-files.maxFiles | number | undefined | multiple). | |
| max-size.maxSize | number | undefined | 5242880 for 5 MB). | |
| name | string | '' | |
| disabled | boolean | false | |
| required | boolean | false | |
| invalid | boolean | false | aria-invalid. |
| hide-list.hideList | boolean | false | |
| label | string | '' | wds-field with a label. |
Properties
| Property | Description |
|---|---|
| filesFile[] | [] to clear it). |
Events
| Event | Description |
|---|---|
| wds-rejectCustomEvent<WdsFileUploadRejectDetail> | accept, max-size or max-files. |
| wds-changeCustomEvent<WdsFileUploadChangeDetail> | |
| wds-removeCustomEvent<WdsFileUploadRemoveDetail> |
Methods
| Method | Description |
|---|---|
| browse() | |
| addFiles(files: Iterable<File>) | |
| removeFile(file: File) | |
| clear() | |
| setFileStatus(file: File, status: WdsFileUploadStatus | null) | null removes the status. |
| formResetCallback() | |
| formDisabledCallback(disabled: boolean) | |
| checkValidity() | true when the value is valid; otherwise fires invalid on the element. |
| reportValidity() | checkValidity(), and shows the browser validation bubble when the value is invalid. |
| setCustomValidity(message: string) | input.setCustomValidity(). |
Slots
| Slot | Description |
|---|---|
| (default) |
CSS parts (::part)
| Part | Description |
|---|---|
| dropzone | <button>). |
| hint | |
| list | |
| file | |
| remove |
CSS custom properties
| Property | Description |
|---|---|
| --wds-file-upload-min-height |
Accessibility#
Screen reader#
wds-field-label (or label), with the hint (types, size, number of files) as its description. Dragging and dropping is optional: every file can be added with the keyboard.
status region, e.g. "photo.jpg added." or "2 files not added." Show the reasons for rejected files on the page as well (see the example).
progressbar named "Uploading" with the file name. invalid and required set aria-invalid and the validation state, as in a native file field.
Keyboard support#
| Key | Function |
|---|---|