# File upload

- Entry point: `@koobiq/components/file-upload`
- NgModule: `KbqFileUploadModule`
- Page: https://koobiq.io/en/components/file-upload/overview

## Overview

Allows the user to upload files to the product.

**Example: File-upload overview** (`file-upload-overview`)

```ts file-upload-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqSingleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-overview-example',
    imports: [
        KbqIconModule,
        KbqSingleFileUploadComponent
    ],
    template: `
        <kbq-single-file-upload [showFileSize]="false">
            <i kbq-icon="kbq-file-o_16"></i>
        </kbq-single-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadOverviewExample {}
```

### Multiple upload

Multiple files can be dragged into the upload area or selected via the system dialog.

**Example: File-upload multiple default** (`file-upload-multiple-default-overview`)

```ts file-upload-multiple-default-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-multiple-default-overview-example',
    imports: [
        KbqIconModule,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <kbq-multiple-file-upload>
            <ng-template #kbqFileIcon>
                <i kbq-icon="kbq-file-text-o_16"></i>
            </ng-template>
        </kbq-multiple-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleDefaultOverviewExample {}
```

#### Compact variant

For the multiple uploader, a compact view is available when it is empty.

**Example: File-upload multiple compact** (`file-upload-multiple-compact-overview`)

```ts file-upload-multiple-compact-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-multiple-compact-overview-example',
    imports: [
        KbqIconModule,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <kbq-multiple-file-upload size="compact">
            <ng-template #kbqFileIcon>
                <i kbq-icon="kbq-file-o_16"></i>
            </ng-template>
        </kbq-multiple-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleCompactOverviewExample {}
```

### File size

When uploading multiple files, their size is always shown in the list. For the single uploader, displaying the file size is configured separately.

**Example: File-upload single with size** (`file-upload-single-with-size`)

```ts file-upload-single-with-size-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { KbqSingleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';

const createBlobPart = (sizeInMB: number): BlobPart => {
    const chunk = 'a'.repeat(1024 * 1024);

    return Array(sizeInMB).fill(chunk).join('');
};

const createMockFile = (fileName: string = 'Filename.txt', options?: FilePropertyBag) =>
    new File([createBlobPart(2)] satisfies BlobPart[], fileName, options);

@Component({
    selector: 'file-upload-single-with-size-example',
    imports: [
        KbqIconModule,
        KbqSingleFileUploadComponent,
        ReactiveFormsModule
    ],
    template: `
        <kbq-single-file-upload [showFileSize]="true" [formControl]="control">
            <i kbq-icon="kbq-file-text-o_16"></i>
        </kbq-single-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadSingleWithSizeExample {
    control = new FormControl(createMockFile('Filename.txt', { type: 'text/plain' }));
}
```

### Icon for selected file

Selected files can be configured with a custom icon, for example, for images.

**Example: File-upload multiple with custom icon** (`file-upload-multiple-with-custom-icon`)

```ts file-upload-multiple-with-custom-icon-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqFileItem, KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';

const createBlobPart = (sizeInMB: number): BlobPart => {
    const chunk = 'a'.repeat(1024 * 1024);

    return Array(sizeInMB).fill(chunk).join('');
};

const createMockFile = (fileName: string = 'Filename.txt', options?: FilePropertyBag) =>
    new File([createBlobPart(2)] satisfies BlobPart[], fileName, options);

@Component({
    selector: 'file-upload-multiple-with-custom-icon-example',
    imports: [
        KbqIconModule,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <kbq-multiple-file-upload [files]="files">
            <ng-template #kbqFileIcon let-file>
                @if (file.file.name.includes('jpg')) {
                    <i kbq-icon="kbq-image_16"></i>
                } @else if (file.file.name.includes('txt')) {
                    <i kbq-icon="kbq-file-text-o_16"></i>
                }
            </ng-template>
        </kbq-multiple-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleWithCustomIconExample {
    files: KbqFileItem[] = [
        { file: createMockFile('image.jpg') },
        { file: createMockFile('file.txt') }
    ];
}
```

### Selecting folders or files

The component can be configured to allow selecting only files, only folders, or both files and folders.

**Example: File-upload allowed** (`file-upload-allowed`)

```ts file-upload-allowed-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIcon } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-allowed-example',
    imports: [
        KbqMultipleFileUploadComponent,
        KbqIcon
    ],
    template: `
        <kbq-multiple-file-upload [allowed]="'mixed'">
            <ng-template #kbqFileIcon let-file>
                @if (!file.hasError) {
                    <i kbq-icon="kbq-file-text-o_16"></i>
                } @else {
                    <i kbq-icon="kbq-triangle-exclamation_16"></i>
                }
            </ng-template>
        </kbq-multiple-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadAllowedExample {}
```

### Disabled state

In the disabled state, the component does not receive focus and drag-and-drop functionality does not work.

**Example: File-upload multiple disabled** (`file-upload-multiple-disabled`)

```ts file-upload-multiple-disabled-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';

@Component({
    selector: 'file-upload-multiple-disabled-example',
    imports: [KbqMultipleFileUploadComponent],
    template: `
        <kbq-multiple-file-upload [disabled]="true" />
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleDisabledExample {}
```

### Error

**Example: File-upload multiple error** (`file-upload-multiple-error`)

```ts file-upload-multiple-error-example.ts
import { ChangeDetectionStrategy, Component, Injectable } from '@angular/core';
import {
    AbstractControl,
    FormControl,
    FormGroupDirective,
    NgForm,
    ReactiveFormsModule,
    Validators
} from '@angular/forms';
import { ErrorStateMatcher, kbqErrorStateMatcherProvider } from '@koobiq/components/core';
import { KbqFileItem, KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';

@Injectable()
export class CustomErrorStateMatcher implements ErrorStateMatcher {
    isErrorState(control: AbstractControl | null, _form: FormGroupDirective | NgForm | null): boolean {
        return !!control?.invalid;
    }
}

@Component({
    selector: 'file-upload-multiple-error-example',
    imports: [
        ReactiveFormsModule,
        KbqIconModule,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <kbq-multiple-file-upload inputId="file-upload-multiple-error-overview" [formControl]="fileUpload">
            <ng-template #kbqFileIcon let-file>
                @if (!file.hasError) {
                    <i kbq-icon="kbq-file-text-o_16"></i>
                } @else {
                    <i kbq-icon="kbq-triangle-exclamation_16"></i>
                }
            </ng-template>
        </kbq-multiple-file-upload>
    `,
    providers: [
        kbqErrorStateMatcherProvider(CustomErrorStateMatcher)
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleErrorExample {
    protected readonly fileUpload = new FormControl<KbqFileItem | null>(null, Validators.required);
}
```

When an invalid file is selected, only the corresponding item in the list receives the error state, not the entire component. Error messages are displayed below the uploader.

**Example: File-upload multiple error filled** (`file-upload-multiple-error-filled`)

```ts file-upload-multiple-error-filled-example.ts
import { afterNextRender, ChangeDetectionStrategy, ChangeDetectorRef, Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormArray, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { FileValidators, KbqFileTypeSpecifier } from '@koobiq/components/core';
import { KbqFileItem, KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqHint } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';

const SIZE_IN_MB = 1;

const MAX_FILE_SIZE = SIZE_IN_MB * 2 ** 20;

const createBlobPart = (sizeInMB: number): BlobPart => {
    const chunk = 'a'.repeat(1024 * 1024);

    return Array(sizeInMB).fill(chunk).join('');
};

const createMockFile = (fileName: string = 'Filename.txt', options?: FilePropertyBag & { size?: number }) =>
    new File([createBlobPart(options?.size ?? 2)] satisfies BlobPart[], fileName, options);

@Component({
    selector: 'file-upload-multiple-error-filled-example',
    imports: [
        KbqIconModule,
        KbqHint,
        KbqMultipleFileUploadComponent,
        ReactiveFormsModule
    ],
    template: `
        <form [formGroup]="formMultiple">
            <kbq-file-upload
                #kbqFileUpload
                multiple
                formControlName="fileUpload"
                [accept]="accept"
                (itemRemoved)="onFileRemoved($event)"
                (itemsAdded)="onFilesAdded($event)"
            >
                <ng-template #kbqFileIcon>
                    <i kbq-icon="kbq-file-text-o_16"></i>
                </ng-template>

                @for (control of fileList.controls; track $index) {
                    <ng-container ngProjectAs="kbq-hint">
                        @if (control.errors) {
                            <kbq-hint color="error">
                                @if (control.errors) {
                                    <span>{{ control.value?.file?.name }} —&nbsp;</span>
                                }
                                @if (control.hasError('fileExtensionMismatch')) {
                                    <span>{{ errorMessages.fileExtensionMismatch }}</span>
                                    @if (control.hasError('fileExtensionMismatch') && control.hasError('maxFileSize')) {
                                        <span>.</span>
                                    }
                                }
                                @if (control.hasError('maxFileSize')) {
                                    {{ errorMessages.maxFileSize }}
                                }
                            </kbq-hint>
                        }
                    </ng-container>
                }

                <kbq-hint>{{ acceptedOutput }}. No more than 1 MB</kbq-hint>
            </kbq-file-upload>
        </form>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleErrorFilledExample {
    protected accept: KbqFileTypeSpecifier = ['.doc', '.docx', '.pdf', '.txt'];
    protected acceptedOutput: string = this.accept
        .map((typeSpecifier) => typeSpecifier.replace('.', '').toUpperCase())
        .join(', ');

    errorMessages = {
        fileExtensionMismatch: 'Provide valid extension',
        maxFileSize: 'The file size limit has been exceeded'
    };
    protected readonly cdr = inject(ChangeDetectorRef);

    protected readonly formMultiple = new FormGroup({
        fileUpload: new FormControl<FileList | KbqFileItem[]>([]),
        fileList: new FormArray<FormControl<KbqFileItem | null>>([])
    });

    protected get fileUpload() {
        return this.formMultiple.controls.fileUpload;
    }

    protected get fileList() {
        return this.formMultiple.controls.fileList;
    }

    constructor() {
        this.fileList.statusChanges.pipe(takeUntilDestroyed()).subscribe(() => {
            this.fileList.controls.forEach((control) => {
                if (control?.value && 'hasError' in control.value) {
                    control.value.hasError = control.invalid;
                }
            });
        });

        afterNextRender(() => {
            const filesToAdd = [
                { file: createMockFile('file1.txt', { size: 2 }), hasError: false },
                { file: createMockFile('file2.docx', { size: 0 }), hasError: false },
                { file: createMockFile('file3.pdf', { size: 3 }), hasError: false }
            ];

            this.fileUpload.setValue(filesToAdd);
            this.onFilesAdded(filesToAdd);
            this.cdr.detectChanges();
        });
    }

    onFileRemoved([
        _,
        index
    ]: [
        KbqFileItem,
        number
    ]): void {
        this.fileList.removeAt(index);
    }

    onFilesAdded($event: KbqFileItem[]): void {
        for (const fileItem of $event.slice()) {
            this.fileList.push(
                new FormControl(fileItem, [
                    FileValidators.isCorrectExtension(this.accept),
                    FileValidators.maxFileSize(MAX_FILE_SIZE)
                ])
            );
        }
    }
}
```

### Height of the selected items list

By default, the component grows in height when many files are selected. However, you can set a maximum height, after which scrolling will appear.

**Example: File-upload multiple with max height** (`file-upload-multiple-with-max-height`)

```ts file-upload-multiple-with-max-height-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqFileItem, KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';

const createBlobPart = (sizeInMB: number): BlobPart => {
    const chunk = 'a'.repeat(1024 * 1024);

    return Array(sizeInMB).fill(chunk).join('');
};

const createMockFile = (fileName: string = 'Filename.txt', options?: FilePropertyBag & { size?: number }) =>
    new File([createBlobPart(options?.size ?? 2)] satisfies BlobPart[], fileName, options);

@Component({
    selector: 'file-upload-multiple-with-max-height-example',
    imports: [
        KbqIconModule,
        KbqMultipleFileUploadComponent,
        FormsModule
    ],
    template: `
        <kbq-multiple-file-upload [(ngModel)]="files">
            <ng-template #kbqFileIcon>
                <i kbq-icon="kbq-file-text-o_16"></i>
            </ng-template>
        </kbq-multiple-file-upload>
    `,
    styles: `
        :host {
            .kbq-multiple-file-upload {
                --kbq-file-upload-size-multiple-max-height: 201px;
            }
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleWithMaxHeightExample {
    files: KbqFileItem[] = [
        { file: createMockFile('project_alpha.txt', { size: 1 }) },
        { file: createMockFile('report_final.docx', { size: 2 }) },
        { file: createMockFile('presentation_overview.pptx', { size: 3 }) },
        { file: createMockFile('data_analysis.csv', { size: 4 }) },
        { file: createMockFile('summary_notes.pdf', { size: 5 }) }
    ];
}
```

It is also possible to set a fixed height for the uploader.

**Example: File-upload multiple with fixed height** (`file-upload-multiple-with-fixed-height`)

```ts file-upload-multiple-with-fixed-height-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqFileItem, KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';

const createBlobPart = (sizeInMB: number): BlobPart => {
    const chunk = 'a'.repeat(1024 * 1024);

    return Array(sizeInMB).fill(chunk).join('');
};

const createMockFile = (fileName: string = 'Filename.txt', options?: FilePropertyBag & { size?: number }) =>
    new File([createBlobPart(options?.size ?? 2)] satisfies BlobPart[], fileName, options);

@Component({
    selector: 'file-upload-multiple-with-fixed-height-example',
    imports: [
        KbqIconModule,
        KbqMultipleFileUploadComponent,
        FormsModule
    ],
    template: `
        <kbq-multiple-file-upload [(ngModel)]="files">
            <ng-template #kbqFileIcon>
                <i kbq-icon="kbq-file-text-o_16"></i>
            </ng-template>
        </kbq-multiple-file-upload>
    `,
    styles: `
        :host {
            .kbq-multiple-file-upload {
                --kbq-file-upload-size-multiple-big-container-min-height: 232px;
                --kbq-file-upload-size-multiple-min-height: 160px;
                --kbq-file-upload-size-multiple-max-height: 160px;
            }
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleWithFixedHeightExample {
    files: KbqFileItem[] = [
        { file: createMockFile('project_report.docx', { size: 1 }) },
        { file: createMockFile('Name', { size: 2 }) },
        { file: createMockFile('meeting_notes.txt', { size: 3 }) },
        { file: createMockFile('presentation_slide.pptx', { size: 4 }) },
        { file: createMockFile('research_paper.pdf', { size: 5 }) }
    ];
}
```

### Full-screen file uploader

The user can drag files onto the page, and at that moment a full-screen upload overlay will be shown.

**Example: File-upload Dropzone** (`file-upload-dropzone`)

```ts file-upload-dropzone-example.ts
import { ChangeDetectionStrategy, Component, DestroyRef, inject, model, signal, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqContentPanelModule } from '@koobiq/components/content-panel';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';
import {
    KbqFileItem,
    KbqFullScreenDropzoneService,
    KbqMultipleFileUploadComponent
} from '@koobiq/components/file-upload';
import { KbqIcon } from '@koobiq/components/icon';
import { KbqToggleChange, KbqToggleComponent } from '@koobiq/components/toggle';

@Component({
    selector: 'file-upload-dropzone-example',
    imports: [
        KbqMultipleFileUploadComponent,
        KbqContentPanelModule,
        KbqEmptyStateModule,
        KbqToggleComponent,
        FormsModule,
        KbqIcon
    ],
    template: `
        <kbq-toggle class="layout-margin-bottom-l" [checked]="isActive()" (change)="onChange($event)">
            activate fullscreen dropzone
        </kbq-toggle>

        @if (files().length) {
            <kbq-file-upload multiple [(ngModel)]="files">
                <ng-template #kbqFileIcon>
                    <i kbq-icon="kbq-file-text-o_16"></i>
                </ng-template>
            </kbq-file-upload>
        } @else {
            <kbq-empty-state>
                <div kbq-empty-state-text>Drop files here</div>
            </kbq-empty-state>
        }
    `,
    styles: `
        :host {
            .kbq-empty-state {
                height: 128px;
            }

            .kbq-multiple-file-upload {
                --kbq-file-upload-size-multiple-min-height: 120px;
                --kbq-file-upload-size-multiple-max-height: 120px;
            }
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadDropzoneExample {
    protected readonly isActive = signal(false);
    protected readonly files = model<KbqFileItem[]>([]);

    protected readonly fileUpload = viewChild(KbqMultipleFileUploadComponent);

    protected readonly dropzoneService = inject(KbqFullScreenDropzoneService);
    protected readonly destroyRef = inject(DestroyRef);

    constructor() {
        const sub = this.dropzoneService.filesDropped.subscribe((files) => {
            this.files.set(files.map((file) => ({ file })));
        });

        this.dropzoneService.init({ title: 'Drop files here' });

        this.destroyRef.onDestroy(() => {
            sub.unsubscribe();
            this.dropzoneService.stop();
        });
    }

    onChange({ checked }: KbqToggleChange) {
        this.isActive.set(checked);
        this.dropzoneService.disabled.set(!checked);
    }
}
```

### Upload area

The drag-and-drop area can be not only the entire screen or the File Upload component itself within a form, but also a separate part of the screen.

**Example: File-upload local dropzone** (`file-upload-local-dropzone`)

```ts file-upload-local-dropzone-example.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqContentPanelModule } from '@koobiq/components/content-panel';
import { KbqLocalDropzone, KbqSingleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIcon } from '@koobiq/components/icon';
import { KbqSidebar, KbqSidebarClosed, KbqSidebarOpened, SidebarPositions } from '@koobiq/components/sidebar';

@Component({
    selector: 'file-upload-local-dropzone-example',
    imports: [
        KbqLocalDropzone,
        KbqButtonModule,
        KbqContentPanelModule,
        KbqSidebar,
        KbqSidebarOpened,
        KbqSidebarClosed,
        KbqSingleFileUploadComponent,
        KbqIcon
    ],
    template: `
        <main>
            <div>
                Manchester United, one of the most iconic football clubs in the world, is renowned for its rich history,
                legendary players, and unparalleled success.
            </div>
        </main>

        <kbq-sidebar
            #rightSidebar="kbqSidebar"
            kbqLocalDropzone
            [opened]="true"
            [position]="position.Right"
            [kbqConnectedTo]="fileUploadLocal"
        >
            <div kbq-sidebar-opened width="100%" minWidth="350px" maxWidth="350px">
                <div class="layout-margin-bottom-m">Drag your files on sidepanel</div>

                <kbq-file-upload #fileUploadLocal>
                    <i kbq-icon="kbq-file-text-o_16"></i>
                </kbq-file-upload>
            </div>
            <div kbq-sidebar-closed width="100px"></div>
        </kbq-sidebar>
    `,
    styles: `
        :host {
            display: flex;
            height: 248px;
        }

        main {
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            gap: var(--kbq-size-m);
            padding: var(--kbq-size-m);
        }

        .kbq-sidebar {
            background: var(--kbq-background-bg);
            box-shadow: var(--kbq-shadow-overlay);
            padding: var(--kbq-size-xxl);
            border-radius: var(--kbq-size-border-radius);
            height: 100%;
            box-sizing: border-box;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadLocalDropzoneExample {
    protected readonly value = signal(false);
    readonly position = SidebarPositions;
}
```

### Adding files

By default, every new selection is added to the files you already have — this makes it easy to build up a list over several picks.

If you want a new selection to replace the previous one instead — the way a regular system file dialog works — use `addStrategy="replace"`.

**Example: File-upload multiple add strategy** (`file-upload-multiple-add-strategy`)

```ts file-upload-multiple-add-strategy-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-multiple-add-strategy-example',
    imports: [
        KbqIconModule,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <kbq-multiple-file-upload [addStrategy]="'replace'">
            <ng-template #kbqFileIcon>
                <i kbq-icon="kbq-file-text-o_16"></i>
            </ng-template>
        </kbq-multiple-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleAddStrategyExample {}
```

### Accepted file types

`accept` is forwarded to the `accept` attribute of the native file input, exactly like the platform one: it filters what the operating system dialog offers, and nothing more. The user can still switch the dialog to "All files", and a dragged file never meets it at all.

To reject a file, validate the control. `FileValidators.isCorrectExtension` takes the same list, so both can be fed from one field, and the rejected file lands in the list with an error message the user can read — see the validation examples on the Examples tab.

A selection the component itself discards is reported through `(rejected)`: files past the first for the single uploader, and duplicates of files already in the list for the multiple one with the default `concat` strategy.

### Accessibility and keyboard

The tab order is the browse link, then the remove control of every selected file, in list order. `Delete` and `Backspace` remove the file whose control has focus; after a removal focus moves to the control that took its place, or to the file input when the list is empty.

The component announces additions and removals in a live region of its own, in the wording of the active locale; to replace that wording, use `[localeOverrides]` or `kbqFileUploadLocaleConfigurationProvider`.

## API

### Components

#### KbqDropzoneContent (component)

```ts
@Component({ selector: 'kbq-dropzone-content' })
class KbqDropzoneContent {}
```

#### KbqFileLoader (component)

Loader component to trigger Browser API.

```ts
@Component({ selector: '[kbqFileLoader]', exportAs: 'kbqFileLoader' })
class KbqFileLoader {
    readonly disabled = input<boolean>(false);
    readonly multiple = input<boolean | null>(null);
    readonly accept = input<string | null>(null);
    readonly for = input<string | null>(null);
    readonly onlyDirectory = input<boolean | null>(null);
    readonly describedBy = input<string | null>(null);
    readonly invalid = input<boolean>(false);
    readonly fileChange = output<Event>();
    readonly input: Signal<ElementRef<HTMLInputElement>>;
}
```

- `[disabled]`: `boolean` — Whether file input selectable or not
- `[multiple]`: `boolean | null` — Selection mode for file input
- `[accept]`: `string | null` — File type specifiers
- `[for]`: `string | null` — id for file input
- `[onlyDirectory]`: `boolean | null` — Reflects webkitdirectory attribute,
  which indicates that elements can only select directories instead of files.
  See [`HTMLInputElement: webkitdirectory property`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory).
- `[describedBy]`: `string | null` — Ids of the elements describing the file input, written to its `aria-describedby`.
- `[invalid]`: `boolean` — Whether the file input holds a value the form rejects, reported through `aria-invalid`.
- `(fileChange)`: `Event` — Event fires when file selected in file-picker.
- `input`: `Signal<ElementRef<HTMLInputElement>>` — Underlying file input element

#### KbqFileUploadEmptyState (component)

Component that displays an empty state for file upload areas.

```ts
@Component({ selector: 'kbq-file-upload-empty-state' })
class KbqFileUploadEmptyState extends KbqEmptyState {
    readonly title = input<string>();
    readonly caption = input<string | TemplateRef<unknown>>();
    readonly size = input<KbqDefaultSizes>('big');
    readonly errorColor = input<boolean>(false);
    readonly alignTop = input<boolean>(false);
}
```

- `[title]`: `string` — Title text to display below the upload icon
- `[caption]`: `string | TemplateRef<unknown>` — Caption text or template to display below the title
- `[size]`: `KbqDefaultSizes` — Same input as `KbqEmptyState.size`, redeclared only to default to `big` instead of `normal`.
  It used to be a `super()`-time assignment, which an `input()` cannot take.
- `[errorColor]`: `boolean` — Whether the title, the text and the projected icon render in the error color. Defaults to `false`.
- `[alignTop]`: `boolean` — Whether the content sits at the top of the host instead of its center. Defaults to `false`.

#### KbqMultipleFileUploadComponent (component)

```ts
@Component({ selector: 'kbq-multiple-file-upload,kbq-file-upload[multiple]' })
class KbqMultipleFileUploadComponent extends KbqFileUploadBase implements ControlValueAccessor {
    readonly progressMode = input<ProgressSpinnerMode>('determinate');
    readonly accept = input<string[]>();
    readonly size = input<'compact' | 'default'>('default');
    readonly inputId = input<string>(
        `kbq-multiple-file-upload-${nextMultipleFileUploadUniqueId++}`
    );
    errorStateMatcher = input<ErrorStateMatcher | undefined>();
    files = model<KbqFileItem[] | undefined>();
    readonly allowed = input<KbqFileUploadAllowedTypeValues>(
        KbqFileUploadAllowedType.File
    );
    readonly fullScreenDropZone = input<KbqDropzoneData | boolean>();
    readonly addStrategy = input<KbqFileUploadAddStrategyValues>(
        KbqFileUploadAddStrategy.Concat
    );
    readonly filesAdded = output<KbqFileItem[]>();
    readonly fileRemoved = output<[ KbqFileItem, number ]>();
    readonly rejected = output<File[]>();
    errorState: boolean;
    readonly localeConfiguration: Signal<KbqFileUploadLocaleConfiguration>;
}
```

- `[progressMode]`: `ProgressSpinnerMode` — A value responsible for progress spinner type.
  Loading logic depends on selected mode
- `[accept]`: `string[]` — Array of file type specifiers
- `[inputId]`: `string` — custom ID for the file input element.
- `[errorStateMatcher]`: `ErrorStateMatcher | undefined` — An object used to control the error state of the component.
- `[(files)]`: `KbqFileItem[] | undefined` — The selected files.

  Writing it is the model→view direction, same as `writeValue()`: it fills the list and tells
  nobody. `cvaOnChange` and `(filesChange)` belong to the paths a user drives — see
  `onFileAdded()` and `deleteFile()` — so a `[files]` binding cannot make the form look edited.
- `[allowed]`: `KbqFileUploadAllowedTypeValues` — Determines which kind of items the upload component can accept. Defaults to `'file'`.
- `[fullScreenDropZone]`: `KbqDropzoneData | boolean` — Controls whether to display fullscreen dropzone.
  Provide configuration object to enable, or undefined to disable.
- `[addStrategy]`: `KbqFileUploadAddStrategyValues` — Controls how newly selected/dropped files are merged into the existing list.
  `concat` accumulates files across interactions, skipping files that duplicate ones already present.
  `replace` discards the previous selection, mirroring native input file multiple behavior. Defaults to `'concat'`.
- `[localeOverrides]`: `KbqPartialLocaleData | undefined` — Localized strings for this element and its subtree, keyed by locale section. Every key is optional at
  every depth.
- `[id]`: `string | null` — id for file input
- `[disabled]`: `boolean | null` — Whether file input selectable or not
- `(filesAdded)`: `KbqFileItem[]` — Emits an event containing a chunk of files added to the file list.
  Useful when handling added files, skipping filtering file list.
- `(fileRemoved)`: `[ KbqFileItem, number ]` — Emits an event containing a tuple of file and file's index when removed from the file list.
  Useful when handle removed files, skipping filtering file list.
- `(rejected)`: `File[]` — Emits the files a selection or a drop discarded — with the default `concat` strategy, the ones
  that duplicate a file already in the list. `accept` is not part of this: it only filters the OS
  dialog.
- `(itemRemoved)`: `[T, number]` — Emits an event containing a tuple of file and file's index when removed from the file list.
  Useful when handle removed files, skipping filtering file list.
- `(itemsAdded)`: `T[]` — Emits array of items that were added to the list.
- `errorState`: `boolean` — Tracks whether the component is in an error state based on the control, parent form,
  and `errorStateMatcher`, triggering visual updates and state changes if needed.
- `localeConfiguration`: `Signal<KbqFileUploadLocaleConfiguration>` — Localized labels of both upload flavours, following the active locale.

#### KbqSingleFileUploadComponent (component)

```ts
@Component({
    selector: 'kbq-single-file-upload,kbq-file-upload:not([multiple])'
})
class KbqSingleFileUploadComponent extends KbqFileUploadBase implements ControlValueAccessor {
    readonly progressMode = input<ProgressSpinnerMode>('determinate');
    readonly accept = input<string[]>();
    readonly inputId = input<string>(
        `kbq-single-file-upload-${nextSingleFileUploadUniqueId++}`
    );
    errorStateMatcher = input<ErrorStateMatcher | undefined>();
    file = model<KbqFileItem | null | undefined>();
    readonly showFileSize = input<boolean>(true);
    readonly allowed = input<KbqFileUploadAllowedTypeValues>(
        KbqFileUploadAllowedType.File
    );
    readonly fullScreenDropZone = input<KbqDropzoneData | boolean>();
    readonly rejected = output<File[]>();
    errorState: boolean;
    readonly localeConfiguration: Signal<KbqFileUploadLocaleConfiguration>;
}
```

- `[progressMode]`: `ProgressSpinnerMode` — A value responsible for progress spinner type.
  Loading logic depends on selected mode
- `[accept]`: `string[]` — Array of file type specifiers
- `[errorStateMatcher]`: `ErrorStateMatcher | undefined` — An object used to control the error state of the component.
- `[(file)]`: `KbqFileItem | null | undefined` — The selected file, or `null`.

  Writing it is the model→view direction, same as `writeValue()`: it fills the list and tells
  nobody. `cvaOnChange` and `(fileChange)` belong to the paths a user drives — see `addFile()`
  and `deleteItem()` — so a `[file]` binding cannot make the form look edited.
- `[showFileSize]`: `boolean` — Controls whether to display the file size information. Defaults to `true`.
- `[allowed]`: `KbqFileUploadAllowedTypeValues` — Determines which kind of items the upload component can accept. Defaults to `'file'`.
- `[fullScreenDropZone]`: `KbqDropzoneData | boolean` — Controls whether to display fullscreen dropzone.
  Provide configuration object to enable, or undefined to disable.
- `[localeOverrides]`: `KbqPartialLocaleData | undefined` — Localized strings for this element and its subtree, keyed by locale section. Every key is optional at
  every depth.
- `[id]`: `string | null` — id for file input
- `[disabled]`: `boolean | null` — Whether file input selectable or not
- `(rejected)`: `File[]` — Emits the files a selection or a drop discarded: everything past the first one, since the
  component holds a single file. `accept` is not part of this — it only filters the OS dialog.
- `errorState`: `boolean` — Tracks whether the component is in an error state based on the control, parent form,
  and `errorStateMatcher`, triggering visual updates and state changes if needed.
- `localeConfiguration`: `Signal<KbqFileUploadLocaleConfiguration>` — Localized labels of both upload flavours, following the active locale.

### Directives

#### KbqDrop (directive)

```ts
@Directive()
class KbqDrop {
    readonly disabled = model<boolean>(false);
    readonly filesDropped = output<KbqFile[]>();
}
```

- `[(disabled)]`: `boolean` — Controls whether drag-and-drop functionality is enabled.
  When true, all drag events are filtered out and ignored.
- `(filesDropped)`: `KbqFile[]` — Emits an event when file items were dropped.

#### KbqFileDropDirective (directive)

```ts
@Directive({ selector: '[kbqFileDrop]', exportAs: 'kbqFileDrop' })
class KbqFileDropDirective extends KbqDrop {
    readonly disabled = model<boolean>(false);
    readonly filesDropped = output<KbqFile[]>();
    onDragEnter(event: DragEvent): void;
}
```

- `[(disabled)]`: `boolean` — Controls whether drag-and-drop functionality is enabled.
  When true, all drag events are filtered out and ignored.
- `(filesDropped)`: `KbqFile[]` — Emits an event when file items were dropped.

#### KbqFileList (directive)

Responsible for list update (add/remove)

```ts
@Directive({ selector: '[kbqFileList]', exportAs: 'kbqFileList' })
class KbqFileList<T> {
    readonly list = model<T[]>([]);
    readonly itemRemoved = output<[T, number]>();
    readonly itemsAdded = output<T[]>();
    add(item: T): void;
    addArray(items: T[]): void;
    replace(items: T[]): void;
    remove(item: T): T[];
    removeAt(index: number): T[];
}
```

- `[(list)]`: `T[]` — Current list of items.
- `(itemRemoved)`: `[T, number]` — Emits an event containing a tuple of file and file's index when removed from the file list.
  Useful when handle removed files, skipping filtering file list.
- `(itemsAdded)`: `T[]` — Emits array of items that were added to the list.
- `add()`: `void` — Adds a single item to the list and emits event
- `addArray()`: `void` — Adds multiple items to the list and emits event
- `replace()`: `void` — Replaces the current list with the specified items and emits event.
- `remove()`: `T[]` — Removes the first occurrence of the specified item. Returns removed items and emits event.
- `removeAt()`: `T[]` — Removes item at specified index. Returns removed items and emits event.
  An index outside the list removes nothing, emits nothing and leaves the list untouched.

#### KbqFileUploadContext (directive)

File upload context.

```ts
@Directive({
    selector: '[kbqFileUploadContext]',
    exportAs: 'kbqFileUploadContext'
})
class KbqFileUploadContext {
    readonly id = input<string | null>(null);
    readonly disabled = model<boolean | null>(null);
    readonly multiple = input<boolean | null>(null);
    readonly accept = input<string | null>(null);
    readonly onlyDirectory = input<boolean | null>(null);
}
```

- `[id]`: `string | null` — id for file input
- `[(disabled)]`: `boolean | null` — Whether file input selectable or not
- `[multiple]`: `boolean | null` — Selection mode for file input
- `[accept]`: `string | null` — File type specifiers
- `[onlyDirectory]`: `boolean | null` — Reflects webkitdirectory attribute, which indicates that elements can only select directories instead of files.

#### KbqLocalDropzone (directive)

Directive that turns an element into a local drag-and-drop zone.

Displays an overlay over the host element on drag enter, handles drag events,
and emits dropped files to a connected file upload component.

```ts
@Directive({ selector: '[kbqLocalDropzone]', exportAs: 'kbqLocalDropzone' })
class KbqLocalDropzone extends KbqDrop {
    readonly connectedTo = input<KbqSingleFileUploadComponent | KbqMultipleFileUploadComponent>(
        undefined,
        { alias: 'kbqConnectedTo' }
    );
    readonly disabled = model<boolean>(false);
    readonly filesDropped = output<KbqFile[]>();
    open(config?: KbqDropzoneData): void;
    close(): void;
}
```

- `[kbqConnectedTo]`: `KbqSingleFileUploadComponent | KbqMultipleFileUploadComponent` — File upload component to connect dropped files to
- `[(disabled)]`: `boolean` — Controls whether drag-and-drop functionality is enabled.
  When true, all drag events are filtered out and ignored.
- `(filesDropped)`: `KbqFile[]` — Emits an event when file items were dropped.
- `open()`: `void` — Opens the dropzone overlay positioned over the host element.
- `close()`: `void` — Closes and disposes the overlay.

### Services

#### KbqFullScreenDropzoneService (service)

Service that provides full-screen drag-and-drop overlay functionality.

```ts
@Injectable({ providedIn: 'root' })
class KbqFullScreenDropzoneService extends KbqDrop {
    readonly disabled = model<boolean>(false);
    readonly filesDropped = output<KbqFile[]>();
    init(config?: KbqDropzoneData): void;
    stop(): void;
    open(config?: KbqDropzoneData): void;
    close(): void;
}
```

- `disabled`: `boolean` — Controls whether drag-and-drop functionality is enabled.
  When true, all drag events are filtered out and ignored.
- `filesDropped`: `KbqFile[]` — Emits an event when file items were dropped.
- `init()`: `void` — Initializes drag-and-drop event listeners on the document body.
  - `config`: `KbqDropzoneData` — Dropzone configuration
- `stop()`: `void` — Stops all drag-and-drop event listeners and cleans up subscriptions.
- `open()`: `void` — Opens the dropzone overlay.
  - `config`: `KbqDropzoneData` — Dropzone configuration
- `close()`: `void` — Closes and disposes the overlay.

### Types

#### KbqDropzoneData (type)

Dropzone overlay content configuration.

```ts
type KbqDropzoneData = Partial<{
    caption: string;
    size: KbqDefaultSizes;
    title: string;
    autoCapture: boolean;
}>;
```

- `caption?`: `string` — Optional caption text displayed below the title.
- `size?`: `KbqDefaultSizes` — Visual size of the dropzone empty state content.
- `title?`: `string` — Title text displayed in the dropzone; falls back to localized default.
- `autoCapture?`: `boolean` — Whether focus should be automatically captured when the overlay opens.

#### KbqFile (interface)

```ts
interface KbqFile extends File {
    fullPath: string;
    readonly lastModified: number;
    readonly name: string;
    readonly webkitRelativePath: string;
    readonly size: number;
    readonly type: string;
    arrayBuffer(): Promise<ArrayBuffer>;
    bytes(): Promise<Uint8Array<ArrayBuffer>>;
    slice(start?: number, end?: number, contentType?: string): Blob;
    stream(): ReadableStream<Uint8Array<ArrayBuffer>>;
    text(): Promise<string>;
}
```

- `lastModified`: `number` — The **`lastModified`** read-only property of the File interface provides the last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight).

  [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified)
- `name`: `string` — The **`name`** read-only property of the File interface returns the name of the file represented by a File object.

  [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name)
- `webkitRelativePath`: `string` — The **`webkitRelativePath`** read-only property of the File interface contains a string which specifies the file's path relative to the directory selected by the user in an input element with its `webkitdirectory` attribute set.

  [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/webkitRelativePath)
- `size`: `number` — The **`size`** read-only property of the Blob interface returns the size of the Blob or File in bytes.

  [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size)
- `type`: `string` — The **`type`** read-only property of the Blob interface returns the MIME type of the file.

  [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type)
- `arrayBuffer()`: `Promise<ArrayBuffer>` — The **`arrayBuffer()`** method of the Blob interface returns a Promise that resolves with the contents of the blob as binary data contained in an ArrayBuffer.

  [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/arrayBuffer)
- `bytes()`: `Promise<Uint8Array<ArrayBuffer>>` — The **`bytes()`** method of the Blob interface returns a Promise that resolves with a Uint8Array containing the contents of the blob as an array of bytes.

  [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/bytes)
- `slice()`: `Blob` — The **`slice()`** method of the Blob interface creates and returns a new `Blob` object which contains data from a subset of the blob on which it's called.

  [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice)
- `stream()`: `ReadableStream<Uint8Array<ArrayBuffer>>` — The **`stream()`** method of the Blob interface returns a ReadableStream which upon reading returns the data contained within the `Blob`.

  [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/stream)
- `text()`: `Promise<string>` — The **`text()`** method of the string containing the contents of the blob, interpreted as UTF-8.

  [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text)

#### KbqFileItem (interface)

```ts
interface KbqFileItem {
    file: File;
    hasError?: boolean;
    loading?: BehaviorSubject<boolean>;
    progress?: BehaviorSubject<number>;
}
```

#### KbqFileUploadAddStrategyValues (type)

Allowed add strategies for the upload component.

```ts
type KbqFileUploadAddStrategyValues = KbqEnumValues<KbqFileUploadAddStrategy>;
```

#### KbqFileUploadAllowedTypeValues (type)

Allowed upload modes for the upload component.

```ts
type KbqFileUploadAllowedTypeValues = KbqEnumValues<KbqFileUploadAllowedType>;
```

### Enums

#### KbqFileUploadAddStrategy (enum)

Strategy for merging newly selected/dropped files into the existing file list.

```ts
enum KbqFileUploadAddStrategy {
    Concat = 'concat',
    Replace = 'replace'
}
```

- `Concat`: `'concat'` — Accumulates files across interactions, skipping files that duplicate ones already present.
- `Replace`: `'replace'` — Discards the previous selection, mirroring native `<input multiple>` behavior.

#### KbqFileUploadAllowedType (enum)

Upload modes enum.

```ts
enum KbqFileUploadAllowedType {
    File = 'file',
    Folder = 'folder',
    Mixed = 'mixed'
}
```

### Constants

#### KBQ_DROPZONE_DATA (const)

Injection token that can be used to access the data that was passed in to a modal.

```ts
const KBQ_DROPZONE_DATA: InjectionToken<KbqDropzoneData>;
```

#### KBQ_FILE_UPLOAD_LOCALE_CONFIGURATION (const)

Localization configuration provider for the labels the file upload components render themselves.

```ts
const KBQ_FILE_UPLOAD_LOCALE_CONFIGURATION: InjectionToken<KbqFileUploadLocaleConfiguration>;
```

#### KBQ_MULTIPLE_FILE_UPLOAD_DEFAULT_CONFIGURATION (const)

```ts
const KBQ_MULTIPLE_FILE_UPLOAD_DEFAULT_CONFIGURATION: KbqMultipleFileUploadLocaleConfiguration;
```

#### KBQ_SINGLE_FILE_UPLOAD_DEFAULT_CONFIGURATION (const)

```ts
const KBQ_SINGLE_FILE_UPLOAD_DEFAULT_CONFIGURATION: KbqFileUploadLocaleConfiguration['single'];
```

#### kbqFileUploadLocaleConfigurationProvider (const)

Utility provider of `KBQ_FILE_UPLOAD_LOCALE_CONFIGURATION`. Only the labels you pass are overridden;
the rest keep following the active locale.

```ts
const kbqFileUploadLocaleConfigurationProvider: (
    configuration: KbqDeepPartial<KbqFileUploadLocaleConfiguration>
) => Provider;
```

## Examples

### Component Localization

Deriving `[localeOverrides]` from the active locale keeps custom labels following `setLocale()`. To change them across the whole application instead, register them as locale data through `KBQ_LOCALE_DATA` in the root providers, where `KbqLocaleService` reads it.

**Example: File-upload multiple custom text** (`file-upload-multiple-custom-text-overview`)

```ts file-upload-multiple-custom-text-overview-example.ts
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { KBQ_LOCALE_SERVICE, KbqMultipleFileUploadLocaleConfiguration } from '@koobiq/components/core';
import { KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';
import { of, skip } from 'rxjs';
import { enUSFileUploadLocaleData } from '../en-US';
import { esLAFileUploadLocaleData } from '../es-LA';
import { ptBRFileUploadLocaleData } from '../pt-BR';
import { ruRUFileUploadLocaleData } from '../ru-RU';
import { tkTMFileUploadLocaleData } from '../tk-TM';

const localeData: Record<string, KbqMultipleFileUploadLocaleConfiguration> = {
    'en-US': enUSFileUploadLocaleData,
    'es-LA': esLAFileUploadLocaleData,
    'pt-BR': ptBRFileUploadLocaleData,
    'ru-RU': ruRUFileUploadLocaleData,
    'tk-TM': tkTMFileUploadLocaleData
};

@Component({
    selector: 'file-upload-multiple-custom-text-overview-example',
    imports: [
        KbqIconModule,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <kbq-multiple-file-upload [localeOverrides]="localeOverrides()">
            <ng-template #kbqFileIcon>
                <i kbq-icon="kbq-file-o_16"></i>
            </ng-template>
        </kbq-multiple-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleCustomTextOverviewExample {
    private readonly exampleDefaultLocale = 'en-US';
    protected readonly localeId = toSignal(
        inject(KBQ_LOCALE_SERVICE, { optional: true })?.changes.pipe(skip(1)) ?? of(this.exampleDefaultLocale),
        { initialValue: this.exampleDefaultLocale }
    );
    // Recomputed from the active locale rather than provided once, so the labels follow `setLocale()`.
    // `KBQ_LOCALE_DATA` would be the other way to say this, but it is read only by `KbqLocaleService`, from
    // the injector that created it — a component-level provider for it never reaches the root service.
    protected readonly localeOverrides = computed(() => ({
        fileUpload: { multiple: localeData[this.localeId()] ?? localeData[this.exampleDefaultLocale] }
    }));
}
```

```ts ../en-US.ts
import { KbqMultipleFileUploadLocaleConfiguration } from '@koobiq/components/core';

export const enUSFileUploadLocaleData: KbqMultipleFileUploadLocaleConfiguration = {
    captionText: 'Drag here or {{ browseLink }}',
    captionTextOnlyFolder: 'Drag here or {{ browseLinkFolder }}',
    captionTextWithFolder: 'Drag here or {{ browseLink }} or {{ browseLinkFolderMixed }}',
    captionTextWhenSelected: 'Drag more files or {{ browseLink }}',
    captionTextForCompactSize: 'Drag here or {{ browseLink }}',
    browseLink: 'choose',
    browseLinkFolder: 'choose folder',
    browseLinkFolderMixed: 'choose folder',
    title: 'Add photos'
};
```

```ts ../es-LA.ts
import { KbqMultipleFileUploadLocaleConfiguration } from '@koobiq/components/core';

export const esLAFileUploadLocaleData: KbqMultipleFileUploadLocaleConfiguration = {
    captionText: 'Arrastre aquí o {{ browseLink }}',
    captionTextWhenSelected: 'Arrastre más archivos aquí o {{ browseLink }}',
    captionTextOnlyFolder: 'Arrastre aquí o {{ browseLinkFolder }}',
    captionTextWithFolder: 'Arrastre aquí o {{ browseLink }} o {{ browseLinkFolderMixed }}',
    captionTextForCompactSize: 'Arrastre archivos o {{ browseLink }}',
    browseLink: 'elija',
    browseLinkFolder: 'carpeta',
    browseLinkFolderMixed: 'carpeta',
    title: 'Agregar fotos'
};
```

```ts ../pt-BR.ts
import { KbqMultipleFileUploadLocaleConfiguration } from '@koobiq/components/core';

export const ptBRFileUploadLocaleData: KbqMultipleFileUploadLocaleConfiguration = {
    captionText: 'Arrastar aqui ou {{ browseLink }}',
    captionTextWhenSelected: 'Arrastar arquivos ou {{ browseLink }}',
    captionTextWithFolder: 'Arrastar aqui ou {{ browseLink }} ou {{ browseLinkFolderMixed }}',
    captionTextOnlyFolder: 'Arrastar aqui ou {{ browseLinkFolder }}',
    captionTextForCompactSize: 'Arrastar arquivos ou {{ browseLink }}',
    browseLink: 'escolher',
    browseLinkFolder: 'pasta',
    browseLinkFolderMixed: 'pasta',
    title: 'Adicionar fotos'
};
```

```ts ../ru-RU.ts
import { KbqMultipleFileUploadLocaleConfiguration } from '@koobiq/components/core';

export const ruRUFileUploadLocaleData: KbqMultipleFileUploadLocaleConfiguration = {
    captionText: 'или {{ browseLink }}',
    captionTextOnlyFolder: 'или {{ browseLinkFolder }}',
    captionTextWithFolder: 'или {{ browseLink }} или {{ browseLinkFolderMixed }}',
    captionTextWhenSelected: 'Перетащите еще или {{ browseLink }}',
    captionTextForCompactSize: 'Перетащите файлы или {{ browseLink }}',
    browseLink: 'выберите файлы',
    browseLinkFolder: 'выберите папку',
    browseLinkFolderMixed: 'папку',
    title: 'Добавьте фотографии'
};
```

```ts ../tk-TM.ts
import { KbqMultipleFileUploadLocaleConfiguration } from '@koobiq/components/core';

/**
 * Mirrors the strings the library ships for `tk-TM` rather than inventing new ones — the example only
 * needs a complete entry for every locale the picker offers.
 */
export const tkTMFileUploadLocaleData: KbqMultipleFileUploadLocaleConfiguration = {
    captionText: 'Şu ýere geçiriň ýa-da {{ browseLink }}',
    captionTextOnlyFolder: 'Şu ýere geçiriň ýa-da {{ browseLinkFolder }}',
    captionTextWithFolder: 'Şu ýere geçiriň ýa-da {{ browseLink }} ýa-da {{ browseLinkFolderMixed }}',
    captionTextWhenSelected: 'Ýene geçiriň ýa-da {{ browseLink }}',
    captionTextForCompactSize: 'Faýllary geçiriň ýa-da {{ browseLink }}',
    browseLink: 'saýlaň',
    browseLinkFolder: 'bukja',
    browseLinkFolderMixed: 'bukja',
    title: 'Faýl ýükläň'
};
```

Binding `[localeOverrides]` changes only the labels you pass, for that one component. The rest stay as they are and keep following the selected language. The value is keyed by locale section, so the labels go under `fileUpload.single` or `fileUpload.multiple`.

**Example: File-upload custom text via input example** (`file-upload-custom-text-via-input`)

```ts file-upload-custom-text-via-input-example.ts
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import {
    KBQ_LOCALE_SERVICE,
    KbqBaseFileUploadLocaleConfiguration,
    KbqMultipleFileUploadLocaleConfiguration
} from '@koobiq/components/core';
import { KbqMultipleFileUploadComponent, KbqSingleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';
import { of, skip } from 'rxjs';

const localeData = {
    'en-US': {
        single: {
            captionText: 'Drop file here or {{ browseLink }}',
            browseLink: 'secure upload'
        } satisfies Partial<KbqBaseFileUploadLocaleConfiguration>,

        multiple: {
            captionText: 'Drop reports here or {{ browseLink }}',
            captionTextWhenSelected: 'Add more evidence or {{ browseLink }}',
            captionTextForCompactSize: 'Attach logs or {{ browseLink }}',
            browseLink: 'secure upload',
            title: 'Submit security files'
        } satisfies Partial<KbqMultipleFileUploadLocaleConfiguration>
    },

    'es-LA': {
        single: {
            captionText: 'Arrastra archivo aquí o {{ browseLink }}',
            browseLink: 'subida segura'
        } satisfies Partial<KbqBaseFileUploadLocaleConfiguration>,

        multiple: {
            captionText: 'Arrastra reportes aquí o {{ browseLink }}',
            captionTextWhenSelected: 'Agrega evidencia o {{ browseLink }}',
            captionTextForCompactSize: 'Adjunta logs o {{ browseLink }}',
            browseLink: 'subida segura',
            title: 'Enviar archivos de seguridad'
        } satisfies Partial<KbqMultipleFileUploadLocaleConfiguration>
    },

    'pt-BR': {
        single: {
            captionText: 'Arraste arquivo aqui ou {{ browseLink }}',
            browseLink: 'upload seguro'
        } satisfies Partial<KbqBaseFileUploadLocaleConfiguration>,

        multiple: {
            captionText: 'Arraste relatórios aqui ou {{ browseLink }}',
            captionTextWhenSelected: 'Adicione evidências ou {{ browseLink }}',
            captionTextForCompactSize: 'Anexe logs ou {{ browseLink }}',
            browseLink: 'upload seguro',
            title: 'Enviar arquivos de segurança'
        } satisfies Partial<KbqMultipleFileUploadLocaleConfiguration>
    },

    'ru-RU': {
        single: {
            captionText: 'Перетащите файл сюда или {{ browseLink }}',
            browseLink: 'безопасная загрузка'
        } satisfies Partial<KbqBaseFileUploadLocaleConfiguration>,

        multiple: {
            captionText: 'Перетащите отчёты сюда или {{ browseLink }}',
            captionTextWhenSelected: 'Добавьте доказательства или {{ browseLink }}',
            captionTextForCompactSize: 'Прикрепите логи или {{ browseLink }}',
            browseLink: 'безопасная загрузка',
            title: 'Загрузка файлов безопасности'
        } satisfies Partial<KbqMultipleFileUploadLocaleConfiguration>
    },

    'tk-TM': {
        single: {
            captionText: 'Faýly şu ýere taşlaň ýa-da {{ browseLink }}',
            browseLink: 'howpsuz ýükleme'
        } satisfies Partial<KbqBaseFileUploadLocaleConfiguration>,

        multiple: {
            captionText: 'Hasabatlary şu ýere taşlaň ýa-da {{ browseLink }}',
            captionTextWhenSelected: 'Has köp subutnamalary goşuň ýa-da {{ browseLink }}',
            captionTextForCompactSize: 'Loglary goşuň ýa-da {{ browseLink }}',
            browseLink: 'howpsuz ýükleme',
            title: 'Howpsuzlyk faýllaryny iber'
        } satisfies Partial<KbqMultipleFileUploadLocaleConfiguration>
    }
};

@Component({
    selector: 'file-upload-custom-text-via-input-example',
    imports: [
        KbqIconModule,
        KbqSingleFileUploadComponent,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <kbq-file-upload multiple [localeOverrides]="multipleLocaleOverrides()">
            <ng-template #kbqFileIcon>
                <i kbq-icon="kbq-file-text-o_16"></i>
            </ng-template>
        </kbq-file-upload>

        <kbq-file-upload [localeOverrides]="singleLocaleOverrides()">
            <ng-template #kbqFileIcon>
                <i kbq-icon="kbq-file-text-o_16"></i>
            </ng-template>
        </kbq-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-column layout-gap-l'
    }
})
export class FileUploadCustomTextViaInputExample {
    private readonly exampleDefaultLocale = 'en-US';
    protected readonly localeId = toSignal(
        inject(KBQ_LOCALE_SERVICE, { optional: true })?.changes.pipe(skip(1)) ?? of(this.exampleDefaultLocale),
        { initialValue: this.exampleDefaultLocale }
    );
    // The binding takes a partial of the whole locale data, keyed by section, so the labels go under the
    // `fileUpload` section and the flavour they belong to.
    protected readonly multipleLocaleOverrides = computed(() => {
        const selectedLocaleData = localeData[this.localeId()] ?? localeData[this.exampleDefaultLocale];

        return { fileUpload: { multiple: selectedLocaleData.multiple } };
    });

    protected readonly singleLocaleOverrides = computed(() => {
        const selectedLocaleData = localeData[this.localeId()] ?? localeData[this.exampleDefaultLocale];

        return { fileUpload: { single: selectedLocaleData.single } };
    });
}
```

### Indeterminate progress indicator

An example of file upload with indeterminate progress:

**Example: File-upload indeterminate loading** (`file-upload-indeterminate-loading-overview`)

```ts file-upload-indeterminate-loading-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
    KbqFileItem,
    KbqMultipleFileUploadComponent,
    KbqSingleFileUploadComponent
} from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';
import { timer } from 'rxjs';
import { take } from 'rxjs/operators';

@Component({
    selector: 'file-upload-indeterminate-loading-overview-example',
    imports: [
        KbqIconModule,
        KbqSingleFileUploadComponent,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <kbq-file-upload progressMode="indeterminate" (fileChange)="onFileChange($event)">
            <i kbq-icon="kbq-file-o_16"></i>
        </kbq-file-upload>

        <kbq-file-upload multiple progressMode="indeterminate" (filesChange)="onFilesChange($event)">
            <ng-template #kbqFileIcon>
                <i kbq-icon="kbq-file-o_16"></i>
            </ng-template>
        </kbq-file-upload>

        @if (isLoading) {
            <p class="kbq-text-big">Immediately load to backend...</p>
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-column layout-gap-l'
    }
})
export class FileUploadIndeterminateLoadingOverviewExample {
    isLoading: boolean;

    onFileChange(file: KbqFileItem | null) {
        if (!file) {
            return;
        }

        const fakeBackendResponse = timer(1500);

        file.loading?.next(true);
        this.isLoading = true;
        fakeBackendResponse.pipe(take(1)).subscribe(() => {
            file.loading?.next(false);
            this.isLoading = false;
        });
    }

    onFilesChange(files: KbqFileItem[] | null) {
        if (!files?.length) {
            return;
        }

        const fakeBackendResponse = timer(1500);

        this.isLoading = true;
        files.forEach((file) => file.loading?.next(true));

        fakeBackendResponse.pipe(take(1)).subscribe(() => {
            files.forEach((file) => file.loading?.next(false));
            this.isLoading = false;
        });
    }
}
```

### Signals

An example of a file uploader using [`signal`](https://angular.dev/guide/signals).
After uploading, the file is highlighted as having an issue — this is a simulation of file processing.

**Example: File-upload single** (`file-upload-single-with-signal`)

```ts file-upload-single-with-signal-example.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
    KbqFileItem,
    KbqMultipleFileUploadComponent,
    KbqSingleFileUploadComponent
} from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-single-with-signal-example',
    imports: [KbqIconModule, KbqSingleFileUploadComponent, KbqMultipleFileUploadComponent],
    template: `
        <div class="layout-margin-bottom-l">
            <kbq-file-upload [file]="file()" (fileChange)="onSingleChange($event)">
                <i kbq-icon="kbq-file-o_16"></i>
            </kbq-file-upload>
        </div>

        <kbq-file-upload multiple [files]="files()" (filesChange)="onMultipleChange($event)">
            <i kbq-icon="kbq-file-o_16"></i>
        </kbq-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadSingleWithSignalExample {
    readonly file = signal<KbqFileItem | null>(null);
    readonly files = signal<KbqFileItem[]>([]);

    onSingleChange($event: KbqFileItem | null) {
        if ($event) {
            this.file.set({ ...$event, hasError: true });

            return;
        }

        this.file.set($event);
    }

    onMultipleChange($event: KbqFileItem[]) {
        const updatedItems = $event.map((fileItem) => ({ ...fileItem, hasError: true }));

        this.files.set(updatedItems);
    }
}
```

### Reactive forms

An example of a file uploader using [`FormControl`](https://angular.dev/api/forms/FormControl).

**Example: File-upload with control value accessor** (`file-upload-cva-overview`)

```ts file-upload-cva-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { KbqFileItem, KbqSingleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-cva-overview-example',
    imports: [
        ReactiveFormsModule,
        KbqIconModule,
        KbqSingleFileUploadComponent
    ],
    template: `
        <div class="kbq-form-horizontal">
            <div class="kbq-form__row layout-margin-bottom-m">
                <label class="kbq-form__label flex-20">Single file-upload with formControl</label>
                <kbq-file-upload class="kbq-form__control flex-80" [formControl]="control">
                    <i kbq-icon="kbq-file-o_16"></i>
                </kbq-file-upload>
            </div>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadCvaOverviewExample {
    control = new FormControl<KbqFileItem | null>(null);
}
```

### Validation: additional examples

The examples use [FileValidators](https://github.com/koobiq/angular-components/blob/main/packages/components/core/forms/validators.ts), a set of static methods for validating file upload fields.

#### Required field

- **Single File**: An example of a file uploader that ensures a file must be uploaded.

**Example: File upload single required reactive validation example** (`file-upload-single-required-reactive-validation`)

```ts file-upload-single-required-reactive-validation-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { ShowOnFormSubmitErrorStateMatcher } from '@koobiq/components/core';
import { KbqFileItem, KbqSingleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-single-required-reactive-validation-example',
    imports: [
        ReactiveFormsModule,
        KbqFormFieldModule,
        KbqButtonModule,
        KbqIconModule,
        KbqSingleFileUploadComponent
    ],
    template: `
        <form [formGroup]="form" (ngSubmit)="onSubmit()">
            <kbq-file-upload
                #kbqFileUpload
                class="layout-margin-bottom-s"
                formControlName="fileUpload"
                [errorStateMatcher]="customErrorStateMatcher"
                [progressMode]="'indeterminate'"
            >
                <i kbq-icon="kbq-file-o_16"></i>

                @if (kbqFileUpload.invalid && form.controls.fileUpload.hasError('required')) {
                    <kbq-hint color="error">File required</kbq-hint>
                }
            </kbq-file-upload>
            <button class="layout-margin-top-m" kbq-button type="submit">Submit</button>
        </form>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadSingleRequiredReactiveValidationExample {
    protected readonly customErrorStateMatcher = new ShowOnFormSubmitErrorStateMatcher();

    protected readonly form = new FormGroup({
        fileUpload: new FormControl<KbqFileItem | null>(null, Validators.required)
    });

    onSubmit() {
        console.log('perform action on submit');
    }
}
```

- **Multiple Files**: An example of a file uploader that requires multiple files to be uploaded.

**Example: File upload multiple required reactive validation example** (`file-upload-multiple-required-reactive-validation`)

```ts file-upload-multiple-required-reactive-validation-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { ShowOnFormSubmitErrorStateMatcher } from '@koobiq/components/core';
import { KbqFileItem, KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-multiple-required-reactive-validation-example',
    imports: [
        ReactiveFormsModule,
        KbqFormFieldModule,
        KbqButtonModule,
        KbqIconModule,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <form [formGroup]="formMultiple" (ngSubmit)="onSubmit()">
            <kbq-file-upload
                #kbqFileUpload
                class="layout-margin-bottom-s"
                formControlName="fileUpload"
                multiple
                [errorStateMatcher]="customErrorStateMatcher"
                [progressMode]="'indeterminate'"
            >
                <ng-template #kbqFileIcon>
                    <i kbq-icon="kbq-file-o_16"></i>
                </ng-template>
                @if (kbqFileUpload.invalid && formMultiple.controls.fileUpload.hasError('required')) {
                    <kbq-hint color="error">File required</kbq-hint>
                }
                @if (
                    kbqFileUpload.invalid && formMultiple.controls.fileUpload.getError('minlength');
                    as minLengthError
                ) {
                    <kbq-hint color="error">
                        {{ minLengthError.actualLength }} / {{ minLengthError.requiredLength }} files provided.
                    </kbq-hint>
                }
            </kbq-file-upload>
            <button class="layout-margin-top-m" kbq-button type="submit">Submit</button>
        </form>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleRequiredReactiveValidationExample {
    protected readonly customErrorStateMatcher = new ShowOnFormSubmitErrorStateMatcher();

    protected readonly formMultiple = new FormGroup({
        fileUpload: new FormControl<FileList | KbqFileItem[]>([], [Validators.required, Validators.minLength(2)])
    });

    onSubmit() {
        console.log('perform action on submit');
    }
}
```

#### File size validation

- **Single File**: An example of uploading a single file with Reactive Forms-based validation.

**Example: File-upload single validation reactive forms** (`file-upload-single-validation-reactive-forms-overview`)

```ts file-upload-single-validation-reactive-forms-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { FileValidators } from '@koobiq/components/core';
import { KbqSingleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';

const MAX_FILE_SIZE = 5 * 2 ** 20;

@Component({
    selector: 'file-upload-single-validation-reactive-forms-overview-example',
    imports: [
        ReactiveFormsModule,
        KbqFormFieldModule,
        KbqIconModule,
        KbqSingleFileUploadComponent
    ],
    template: `
        <form [formGroup]="formGroup">
            <kbq-file-upload class="layout-margin-bottom-s" formControlName="fileControl">
                @if (!formGroup.get('fileControl')?.errors) {
                    <i kbq-icon="kbq-file-o_16"></i>
                } @else {
                    <i kbq-icon="kbq-triangle-exclamation_16"></i>
                }

                @if (formGroup.get('fileControl')?.hasError('maxFileSize')) {
                    <kbq-hint color="error">The file size must not exceed the limit</kbq-hint>
                }

                <kbq-hint>The file size should not exceed 5 MB</kbq-hint>
            </kbq-file-upload>
        </form>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadSingleValidationReactiveFormsOverviewExample {
    formGroup = new FormGroup({
        fileControl: new FormControl(null, FileValidators.maxFileSize(MAX_FILE_SIZE))
    });
}
```

- **Multiple Files**: An example of uploading multiple files with Reactive Forms and built-in validation.

**Example: File-upload multiple default validation reactive forms** (`file-upload-multiple-default-validation-reactive-forms-overview`)

```ts file-upload-multiple-default-validation-reactive-forms-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormArray, FormControl, ReactiveFormsModule } from '@angular/forms';
import { FileValidators } from '@koobiq/components/core';
import { KbqFileItem, KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';

const MAX_FILE_SIZE = 5 * 2 ** 20;

@Component({
    selector: 'file-upload-multiple-default-validation-reactive-forms-overview-example',
    imports: [
        ReactiveFormsModule,
        KbqFormFieldModule,
        KbqIconModule,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <kbq-file-upload
            multiple
            [progressMode]="'indeterminate'"
            (fileRemoved)="onFileRemoved($event)"
            (filesAdded)="onFilesAdded($event)"
        >
            <ng-template #kbqFileIcon let-file>
                @if (!file.hasError) {
                    <i kbq-icon="kbq-file-o_16"></i>
                } @else {
                    <i kbq-icon="kbq-triangle-exclamation_16"></i>
                }
            </ng-template>

            <kbq-hint>The file size should not exceed 5 MB</kbq-hint>

            @for (control of fileList.controls; track $index) {
                <kbq-hint color="error">
                    @if (control.hasError('maxFileSize')) {
                        {{ control.value?.file?.name }} — {{ maxFileSizeErrorMessage }}
                    }
                </kbq-hint>
            }
        </kbq-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleDefaultValidationReactiveFormsOverviewExample {
    maxFileSizeErrorMessage = 'The file size has exceeded the limit';

    fileList = new FormArray<FormControl<KbqFileItem | null>>([]);

    constructor() {
        this.fileList.statusChanges.subscribe(() => {
            this.fileList.controls.forEach((control) => {
                if (control?.value && 'hasError' in control.value) {
                    control.value.hasError = control.invalid;
                }
            });
        });
    }

    onFileRemoved([
        _,
        index
    ]: [
        KbqFileItem,
        number
    ]) {
        this.fileList.removeAt(index);
    }

    onFilesAdded($event: KbqFileItem[]) {
        for (const fileItem of $event.slice()) {
            this.fileList.push(new FormControl(fileItem, FileValidators.maxFileSize(MAX_FILE_SIZE)));
        }
    }
}
```

#### File type or extension validation

- **Single file**: example of uploading a single file using `Reactive Forms` with validation.

**Example: File upload single accept validation** (`file-upload-single-accept-validation`)

```ts file-upload-single-accept-validation-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { FileValidators, KbqFileTypeSpecifier } from '@koobiq/components/core';
import { KbqSingleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-single-accept-validation-example',
    imports: [
        ReactiveFormsModule,
        KbqFormFieldModule,
        KbqIconModule,
        KbqSingleFileUploadComponent
    ],
    template: `
        <form [formGroup]="formGroup">
            <kbq-file-upload class="layout-margin-bottom-s" formControlName="fileControl" [accept]="accept">
                @if (!formGroup.get('fileControl')?.errors) {
                    <i kbq-icon="kbq-file-o_16"></i>
                } @else {
                    <i kbq-icon="kbq-triangle-exclamation_16"></i>
                }

                @if (formGroup.get('fileControl')?.hasError('fileExtensionMismatch')) {
                    <kbq-hint color="error">Provide valid extension</kbq-hint>
                }

                <kbq-hint>File with TXT extension is allowed</kbq-hint>
            </kbq-file-upload>
        </form>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadSingleAcceptValidationExample {
    protected accept: KbqFileTypeSpecifier = ['.txt'];

    protected readonly formGroup = new FormGroup({
        fileControl: new FormControl(null, FileValidators.isCorrectExtension(this.accept))
    });
}
```

- **Multiple files**: Example of uploading multiple files using `Reactive Forms` with validation.

**Example: File upload multiple accept validation** (`file-upload-multiple-accept-validation`)

```ts file-upload-multiple-accept-validation-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormArray, FormControl, ReactiveFormsModule } from '@angular/forms';
import { FileValidators, KbqFileTypeSpecifier } from '@koobiq/components/core';
import { KbqFileItem, KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-multiple-accept-validation-example',
    imports: [
        ReactiveFormsModule,
        KbqFormFieldModule,
        KbqIconModule,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <kbq-file-upload multiple (fileRemoved)="onFileRemoved($event)" (filesAdded)="onFilesAdded($event)">
            <ng-template #kbqFileIcon let-file>
                @if (!file.hasError) {
                    <i kbq-icon="kbq-file-o_16"></i>
                } @else {
                    <i kbq-icon="kbq-triangle-exclamation_16"></i>
                }
            </ng-template>

            <kbq-hint>Files with TXT extension are allowed</kbq-hint>

            @for (control of fileList.controls; track $index) {
                <kbq-hint color="error">
                    @if (control.hasError('fileExtensionMismatch')) {
                        {{ control.value?.file?.name }} - {{ fileExtensionMismatchErrorMessage }}
                    }
                </kbq-hint>
            }
        </kbq-file-upload>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleAcceptValidationExample {
    protected accept: KbqFileTypeSpecifier = ['.txt'];
    protected fileExtensionMismatchErrorMessage = 'Provide valid extension';

    protected readonly fileList = new FormArray<FormControl<KbqFileItem | null>>([]);

    constructor() {
        this.fileList.statusChanges.subscribe(() => {
            this.fileList.controls.forEach((control) => {
                if (control?.value && 'hasError' in control.value) {
                    control.value.hasError = control.invalid;
                }
            });
        });
    }

    onFileRemoved([
        _,
        index
    ]: [
        KbqFileItem,
        number
    ]): void {
        this.fileList.removeAt(index);
    }

    onFilesAdded($event: KbqFileItem[]): void {
        for (const fileItem of $event.slice()) {
            this.fileList.push(new FormControl(fileItem, FileValidators.isCorrectExtension(this.accept)));
        }
    }
}
```

#### Mixed validation: required and extension

- **Single file**: example of uploading a single file using `Reactive Forms` with validation.

**Example: File upload single mixed validation example** (`file-upload-single-mixed-validation`)

```ts file-upload-single-mixed-validation-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { FileValidators, KbqFileTypeSpecifier, ShowOnFormSubmitErrorStateMatcher } from '@koobiq/components/core';
import { KbqFileItem, KbqSingleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-single-mixed-validation-example',
    imports: [
        ReactiveFormsModule,
        KbqFormFieldModule,
        KbqButtonModule,
        KbqIconModule,
        KbqSingleFileUploadComponent
    ],
    template: `
        <form [formGroup]="form" (ngSubmit)="onSubmit()">
            <kbq-file-upload
                #kbqFileUpload
                class="layout-margin-bottom-s"
                formControlName="fileUpload"
                [errorStateMatcher]="customErrorStateMatcher"
                [progressMode]="'indeterminate'"
            >
                <i kbq-icon="kbq-file-o_16"></i>

                @if (kbqFileUpload.invalid && form.controls.fileUpload.hasError('required')) {
                    <kbq-hint color="error">File required</kbq-hint>
                }

                @if (form.controls.fileUpload.hasError('fileExtensionMismatch')) {
                    <kbq-hint color="error">Provide valid extension</kbq-hint>
                }

                <kbq-hint>Files with TXT extension are allowed</kbq-hint>
            </kbq-file-upload>
            <button class="layout-margin-top-m" kbq-button type="submit">Submit</button>
        </form>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadSingleMixedValidationExample {
    protected readonly customErrorStateMatcher = new ShowOnFormSubmitErrorStateMatcher();
    protected readonly accept: KbqFileTypeSpecifier = ['.txt'];

    protected readonly form = new FormGroup({
        fileUpload: new FormControl<KbqFileItem | null>(null, [
            Validators.required,
            FileValidators.isCorrectExtension(['.txt'])
        ])
    });

    onSubmit(): void {
        console.log('perform action on submit');
    }
}
```

- **Multiple files**: Example of uploading multiple files using `Reactive Forms` with validation.

**Example: File upload multiple mixed validation example** (`file-upload-multiple-mixed-validation`)

```ts file-upload-multiple-mixed-validation-example.ts
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { FileValidators, KbqFileTypeSpecifier, ShowOnFormSubmitErrorStateMatcher } from '@koobiq/components/core';
import { KbqFileItem, KbqMultipleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'file-upload-multiple-mixed-validation-example',
    imports: [
        ReactiveFormsModule,
        KbqFormFieldModule,
        KbqButtonModule,
        KbqIconModule,
        KbqMultipleFileUploadComponent
    ],
    template: `
        <form [formGroup]="formMultiple">
            <kbq-file-upload
                #kbqFileUpload
                class="layout-margin-bottom-s"
                multiple
                formControlName="fileUpload"
                [errorStateMatcher]="customErrorStateMatcher"
                [progressMode]="'indeterminate'"
                (fileRemoved)="onFileRemoved($event)"
                (filesAdded)="onFilesAdded($event)"
            >
                <ng-template #kbqFileIcon>
                    <i kbq-icon="kbq-file-o_16"></i>
                </ng-template>

                @if (kbqFileUpload.invalid && formMultiple.controls.fileUpload.hasError('required')) {
                    <kbq-hint color="error">File required</kbq-hint>
                }

                @for (control of fileList.controls; track $index) {
                    <kbq-hint color="error">
                        @if (control.hasError('fileExtensionMismatch')) {
                            {{ control.value?.file?.name }} - {{ fileExtensionMismatchErrorMessage }}
                        }
                    </kbq-hint>
                }

                <kbq-hint>Files with TXT extension are allowed</kbq-hint>
            </kbq-file-upload>
            <button class="layout-margin-top-m" kbq-button type="submit">Submit</button>
        </form>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadMultipleMixedValidationExample {
    protected readonly customErrorStateMatcher = new ShowOnFormSubmitErrorStateMatcher();
    protected readonly accept: KbqFileTypeSpecifier = ['.txt'];
    protected readonly fileExtensionMismatchErrorMessage = 'Provide valid extension';
    protected readonly cdr = inject(ChangeDetectorRef);

    protected readonly formMultiple = new FormGroup({
        fileUpload: new FormControl<FileList | KbqFileItem[]>([], Validators.required),
        fileList: new FormArray<FormControl<KbqFileItem | null>>([], Validators.required)
    });

    protected get fileList() {
        return this.formMultiple.controls.fileList;
    }

    constructor() {
        this.fileList.statusChanges.pipe(takeUntilDestroyed()).subscribe(() => {
            this.fileList.controls.forEach((control) => {
                if (control?.value && 'hasError' in control.value) {
                    control.value.hasError = control.invalid;
                }
            });
        });
    }

    onFileRemoved([
        _,
        index
    ]: [
        KbqFileItem,
        number
    ]): void {
        this.fileList.removeAt(index);
    }

    onFilesAdded($event: KbqFileItem[]): void {
        for (const fileItem of $event.slice()) {
            this.fileList.push(new FormControl(fileItem, FileValidators.isCorrectExtension(this.accept)));
        }
    }
}
```

#### Asynchronous validation

- Example of content check while uploading a single file using `Reactive Forms`.

**Example: File upload single async validation** (`file-upload-single-async-validation`)

```ts file-upload-single-async-validation-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
    AbstractControl,
    AsyncValidatorFn,
    FormControl,
    FormGroup,
    ReactiveFormsModule,
    ValidationErrors
} from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { ShowOnFormSubmitErrorStateMatcher } from '@koobiq/components/core';
import { KbqFileItem, KbqSingleFileUploadComponent } from '@koobiq/components/file-upload';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { Observable } from 'rxjs';

const MAX_LINE_COUNT = 100;

function readFileAsText(file: File): Observable<string> {
    return new Observable((observer) => {
        const reader = new FileReader();

        reader.onload = () => {
            observer.next(reader.result as string);
            observer.complete();
        };

        reader.onerror = () => {
            observer.error(reader.error);
        };

        reader.readAsText(file);
    });
}

function maxLinesValidator(maxLines: number): AsyncValidatorFn {
    return (control: AbstractControl<KbqFileItem | null>): Observable<ValidationErrors | null> => {
        return new Observable((observer) => {
            if (!control.value) {
                observer.next(null);
                observer.complete();

                return;
            }

            const subscription = readFileAsText(control.value.file).subscribe({
                next: (content) => {
                    const lines = content.split('\n').length;

                    observer.next(lines > maxLines ? { maxLines: { max: maxLines, actual: lines } } : null);
                    observer.complete();
                },
                error: () => {
                    observer.next({ fileReadError: true });
                    observer.complete();
                }
            });

            return () => subscription.unsubscribe();
        });
    };
}

@Component({
    selector: 'file-upload-single-async-validation-example',
    imports: [
        ReactiveFormsModule,
        KbqFormFieldModule,
        KbqButtonModule,
        KbqIconModule,
        KbqSingleFileUploadComponent
    ],
    template: `
        <form [formGroup]="form" (ngSubmit)="onSubmit()">
            <kbq-file-upload
                #kbqFileUpload
                class="layout-margin-bottom-s"
                formControlName="fileUpload"
                [errorStateMatcher]="errorStateMatcher"
                [progressMode]="'indeterminate'"
                [accept]="['.txt']"
            >
                @if (form.controls.fileUpload.pending) {
                    <i kbq-icon="kbq-spinner_16"></i>
                } @else if (kbqFileUpload.invalid) {
                    <i kbq-icon="kbq-triangle-exclamation_16"></i>
                } @else {
                    <i kbq-icon="kbq-file-o_16"></i>
                }

                @if (form.controls.fileUpload.hasError('maxLines')) {
                    <kbq-hint color="error">
                        File exceeds {{ form.controls.fileUpload.errors?.['maxLines'].max }} lines ({{
                            form.controls.fileUpload.errors?.['maxLines'].actual
                        }}
                        found)
                    </kbq-hint>
                }

                @if (form.controls.fileUpload.hasError('fileReadError')) {
                    <kbq-hint color="error">Failed to read file content</kbq-hint>
                }

                <kbq-hint>Only TXT files with up to {{ maxLines }} lines are allowed</kbq-hint>
            </kbq-file-upload>

            <button class="layout-margin-top-m" kbq-button type="submit" [disabled]="form.pending">Submit</button>
        </form>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadSingleAsyncValidationExample {
    protected readonly maxLines = MAX_LINE_COUNT;
    protected readonly errorStateMatcher = new ShowOnFormSubmitErrorStateMatcher();

    protected readonly form = new FormGroup({
        fileUpload: new FormControl<KbqFileItem | null>(null, { asyncValidators: maxLinesValidator(MAX_LINE_COUNT) })
    });

    onSubmit(): void {
        console.log('perform action on submit');
    }
}
```

### Primitives

This example demonstrates the use of file upload component primitives.

**Example: File upload primitive** (`file-upload-primitive`)

```ts file-upload-primitive-example.ts
import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
import { KbqBadge } from '@koobiq/components/badge';
import { KbqButton, KbqButtonCssStyler } from '@koobiq/components/button';
import { KbqEmptyState } from '@koobiq/components/empty-state';
import {
    KbqFile,
    KbqFileDropDirective,
    KbqFileItem,
    KbqFileList,
    KbqFileLoader,
    KbqFileUploadContext
} from '@koobiq/components/file-upload';
import { KbqIcon, KbqIconItem } from '@koobiq/components/icon';
import { BehaviorSubject } from 'rxjs';

/**
 @title File upload primitive
 */
@Component({
    selector: 'file-upload-primitive-example',
    imports: [
        KbqFileDropDirective,
        KbqFileUploadContext,
        KbqFileLoader,
        KbqButton,
        KbqButtonCssStyler,
        KbqFileList,
        KbqIcon,
        KbqBadge,
        KbqEmptyState,
        KbqIconItem
    ],
    template: `
        <div
            kbqFileUploadContext
            kbqFileDrop
            multiple
            [id]="'custom-file-upload'"
            [disabled]="false"
            (filesDropped)="onFilesDropped($event)"
        >
            <kbq-empty-state size="big" style="min-height: 216px">
                <i
                    kbq-empty-state-icon
                    kbq-icon-item="kbq-arrow-up-from-line_16"
                    [big]="true"
                    [color]="'contrast'"
                    [fade]="true"
                ></i>
                <div kbq-empty-state-title>Click to upload or drag and drop</div>
                <div kbq-empty-state-actions>
                    <label
                        #ref="kbqFileLoader"
                        kbqFileLoader
                        (click)="ref.input().nativeElement.click()"
                        (fileChange)="onFileClicked($event)"
                    >
                        <button kbq-button [color]="'theme'" [kbqStyle]="'transparent'">Choose a file</button>
                    </label>
                </div>
            </kbq-empty-state>
            <div
                #list="kbqFileList"
                kbqFileList
                (itemRemoved)="onItemRemoved($event)"
                (itemsAdded)="onItemsAdded($event)"
                (listChange)="onListChange($event)"
            >
                @for (item of list.list(); track item) {
                    <div class="list-item">
                        <kbq-badge>{{ item.file.name }}</kbq-badge>
                        <button
                            kbq-button
                            aria-label="Delete"
                            [kbqStyle]="'transparent'"
                            [color]="'contrast'"
                            (click)="list.removeAt($index)"
                        >
                            <i kbq-icon="kbq-trash_16"></i>
                        </button>
                    </div>
                }
            </div>
        </div>
    `,
    styles: `
        .kbq-file-drop {
            min-height: 300px;
            width: 500px;
            background: var(--kbq-background-card);
            border: 1px dashed var(--kbq-line-contrast-less);
            border-radius: 8px;
            margin-bottom: var(--kbq-size-m);
        }

        .kbq-file-list {
            display: flex;
            width: 100%;

            &:not(:empty) {
                padding-top: var(--kbq-size-s);
            }
        }

        .list-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            width: 100%;
            box-shadow: var(--kbq-shadow-card);
            padding: var(--kbq-size-s);
            margin: var(--kbq-size-m);
            border-radius: var(--kbq-size-border-radius);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadPrimitiveExample {
    readonly list = viewChild.required(KbqFileList);

    onFileClicked({ target }: Event) {
        if (!(target instanceof HTMLInputElement)) return;
        const fileToAdd = target.files?.item(0);

        if (fileToAdd) {
            this.list().add({
                file: fileToAdd,
                hasError: false,
                loading: new BehaviorSubject<boolean>(false),
                progress: new BehaviorSubject<number>(0)
            });
        }

        target.value = null as any;
    }

    onFilesDropped(files: KbqFile[] | null): void {
        if (!files) return;

        this.list().addArray(
            Array.from(files).map((file: File) => ({
                file,
                hasError: false,
                loading: new BehaviorSubject<boolean>(false),
                progress: new BehaviorSubject<number>(0)
            }))
        );
    }

    onListChange($event: KbqFileItem[]) {
        console.log('onListChange: ', $event);
    }

    onItemRemoved($event: [KbqFileItem, number]) {
        console.log('onItemRemoved: ', $event);
    }

    onItemsAdded($event: KbqFileItem[]) {
        console.log('onItemsAdded: ', $event);
    }
}
```

### Dropzone

**Example: File-upload Dropzone** (`file-upload-dropzone`)

```ts file-upload-dropzone-example.ts
import { ChangeDetectionStrategy, Component, DestroyRef, inject, model, signal, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqContentPanelModule } from '@koobiq/components/content-panel';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';
import {
    KbqFileItem,
    KbqFullScreenDropzoneService,
    KbqMultipleFileUploadComponent
} from '@koobiq/components/file-upload';
import { KbqIcon } from '@koobiq/components/icon';
import { KbqToggleChange, KbqToggleComponent } from '@koobiq/components/toggle';

@Component({
    selector: 'file-upload-dropzone-example',
    imports: [
        KbqMultipleFileUploadComponent,
        KbqContentPanelModule,
        KbqEmptyStateModule,
        KbqToggleComponent,
        FormsModule,
        KbqIcon
    ],
    template: `
        <kbq-toggle class="layout-margin-bottom-l" [checked]="isActive()" (change)="onChange($event)">
            activate fullscreen dropzone
        </kbq-toggle>

        @if (files().length) {
            <kbq-file-upload multiple [(ngModel)]="files">
                <ng-template #kbqFileIcon>
                    <i kbq-icon="kbq-file-text-o_16"></i>
                </ng-template>
            </kbq-file-upload>
        } @else {
            <kbq-empty-state>
                <div kbq-empty-state-text>Drop files here</div>
            </kbq-empty-state>
        }
    `,
    styles: `
        :host {
            .kbq-empty-state {
                height: 128px;
            }

            .kbq-multiple-file-upload {
                --kbq-file-upload-size-multiple-min-height: 120px;
                --kbq-file-upload-size-multiple-max-height: 120px;
            }
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadDropzoneExample {
    protected readonly isActive = signal(false);
    protected readonly files = model<KbqFileItem[]>([]);

    protected readonly fileUpload = viewChild(KbqMultipleFileUploadComponent);

    protected readonly dropzoneService = inject(KbqFullScreenDropzoneService);
    protected readonly destroyRef = inject(DestroyRef);

    constructor() {
        const sub = this.dropzoneService.filesDropped.subscribe((files) => {
            this.files.set(files.map((file) => ({ file })));
        });

        this.dropzoneService.init({ title: 'Drop files here' });

        this.destroyRef.onDestroy(() => {
            sub.unsubscribe();
            this.dropzoneService.stop();
        });
    }

    onChange({ checked }: KbqToggleChange) {
        this.isActive.set(checked);
        this.dropzoneService.disabled.set(!checked);
    }
}
```
