diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.spec.ts b/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.spec.ts
index 56cc720f74..474d244c78 100644
--- a/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.spec.ts
+++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.spec.ts
@@ -1,5 +1,6 @@
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing';
+import { Validators } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing';
@@ -7,6 +8,7 @@ import { SkyIdService, SkyLiveAnnouncerService } from '@skyux/core';
import {
SkyHelpTestingController,
SkyHelpTestingModule,
+ provideSkyFileReaderTesting,
} from '@skyux/core/testing';
import { SkyFileItem } from '../shared/file-item';
@@ -15,6 +17,7 @@ import { SkyFileDropChange } from './file-drop-change';
import { SkyFileDropComponent } from './file-drop.component';
import { SkyFileDropModule } from './file-drop.module';
import { SkyFileLink } from './file-link';
+import { ReactiveFileDropTestComponent } from './fixtures/reactive-file-drop.component.fixture';
describe('File drop component', () => {
/** Simple test component with tabIndex */
@@ -322,7 +325,11 @@ describe('File drop component', () => {
expect(dragOverPropStopped).toBe(true);
}
- function triggerDrop(files: any, dropDebugEl: DebugElement): void {
+ function triggerDrop(
+ fixture: ComponentFixture
,
+ files: any,
+ dropDebugEl: DebugElement,
+ ): void {
let dropPropStopped = false;
let dropPreventDefault = false;
const fileLength = files ? files.length : 0;
@@ -504,7 +511,7 @@ describe('File drop component', () => {
testClick(false);
});
- it('should load and emit files on file change event', () => {
+ it('should load and emit files on file change event', async () => {
let filesChangedActual: SkyFileDropChange | undefined;
componentInstance.filesChanged.subscribe(
@@ -512,6 +519,7 @@ describe('File drop component', () => {
);
setupStandardFileChangeEvent();
+ await fixture.whenStable();
expect(filesChangedActual?.files.length).toBe(2);
expect(filesChangedActual?.files[0].url).toBe('url');
@@ -526,7 +534,7 @@ describe('File drop component', () => {
expect(liveAnnouncerSpy.calls.count()).toBe(2);
});
- it('should load and emit files on file change event when file reader has an error and aborts', () => {
+ it('should load and emit files on file change event when file reader has an error and aborts', async () => {
let filesChangedActual: SkyFileDropChange | undefined;
componentInstance.filesChanged.subscribe(
@@ -550,19 +558,16 @@ describe('File drop component', () => {
},
]);
- fixture.detectChanges();
-
fileReaderSpy.abortCallbacks[0]();
-
fileReaderSpy.loadCallbacks[1]({
target: {
result: 'anotherUrl',
},
});
-
fileReaderSpy.errorCallbacks[2]();
fixture.detectChanges();
+ await fixture.whenStable();
expect(filesChangedActual?.files.length).toBe(1);
expect(filesChangedActual?.files[0].url).toBe('anotherUrl');
@@ -601,7 +606,7 @@ describe('File drop component', () => {
expect(inputEl.nativeElement.getAttribute('accept')).toBe('image/png');
});
- it('should allow the user to specify a min file size', () => {
+ it('should allow the user to specify a min file size', async () => {
let filesChangedActual: SkyFileDropChange | undefined;
componentInstance.filesChanged.subscribe(
@@ -612,6 +617,7 @@ describe('File drop component', () => {
fixture.detectChanges();
setupStandardFileChangeEvent();
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(1);
expect(filesChangedActual?.rejectedFiles[0].file.name).toBe('foo.txt');
@@ -628,7 +634,7 @@ describe('File drop component', () => {
expect(liveAnnouncerSpy.calls.count()).toBe(1);
});
- it('should respect a default min file size of 0', () => {
+ it('should respect a default min file size of 0', async () => {
let filesChangedActual: SkyFileDropChange | undefined;
componentInstance.filesChanged.subscribe(
@@ -636,6 +642,7 @@ describe('File drop component', () => {
);
const spy = setupStandardFileChangeEvent();
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(0);
expect(filesChangedActual?.files.length).toBe(2);
@@ -659,6 +666,7 @@ describe('File drop component', () => {
fixture.detectChanges();
setupStandardFileChangeEvent(undefined, spy);
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(1);
expect(filesChangedActual?.rejectedFiles[0].file.name).toBe('foo.txt');
@@ -682,6 +690,7 @@ describe('File drop component', () => {
fixture.detectChanges();
setupStandardFileChangeEvent(undefined, spy);
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(0);
expect(filesChangedActual?.files.length).toBe(2);
@@ -698,7 +707,7 @@ describe('File drop component', () => {
expect(liveAnnouncerSpy.calls.count()).toBe(2);
});
- it('should allow the user to specify a max file size', () => {
+ it('should allow the user to specify a max file size', async () => {
let filesChangedActual: SkyFileDropChange | undefined;
componentInstance.filesChanged.subscribe(
@@ -709,6 +718,7 @@ describe('File drop component', () => {
fixture.detectChanges();
setupStandardFileChangeEvent();
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(1);
expect(filesChangedActual?.rejectedFiles[0].file.name).toBe('woo.txt');
@@ -725,7 +735,7 @@ describe('File drop component', () => {
expect(liveAnnouncerSpy.calls.count()).toBe(1);
});
- it('should respect a default max file size of 500000', () => {
+ it('should respect a default max file size of 500000', async () => {
let filesChangedActual: SkyFileDropChange | undefined;
componentInstance.filesChanged.subscribe(
@@ -733,6 +743,7 @@ describe('File drop component', () => {
);
const spy = setupStandardFileChangeEvent();
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(0);
expect(filesChangedActual?.files.length).toBe(2);
@@ -756,6 +767,7 @@ describe('File drop component', () => {
fixture.detectChanges();
setupStandardFileChangeEvent(undefined, spy);
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(1);
expect(filesChangedActual?.rejectedFiles[0].file.name).toBe('woo.txt');
@@ -779,6 +791,7 @@ describe('File drop component', () => {
fixture.detectChanges();
setupStandardFileChangeEvent(undefined, spy);
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(0);
expect(filesChangedActual?.files.length).toBe(2);
@@ -795,7 +808,7 @@ describe('File drop component', () => {
expect(liveAnnouncerSpy.calls.count()).toBe(2);
});
- it('should allow the user to specify a validation function', () => {
+ it('should allow the user to specify a validation function', async () => {
let filesChangedActual: SkyFileDropChange | undefined;
componentInstance.filesChanged.subscribe(
@@ -817,6 +830,7 @@ describe('File drop component', () => {
fixture.detectChanges();
setupStandardFileChangeEvent();
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(1);
expect(filesChangedActual?.rejectedFiles[0].file.name).toBe('woo.txt');
@@ -833,7 +847,7 @@ describe('File drop component', () => {
expect(liveAnnouncerSpy.calls.count()).toBe(1);
});
- it('should allow the user to specify accepted types', () => {
+ it('should allow the user to specify accepted types', async () => {
let filesChangedActual: SkyFileDropChange | undefined;
componentInstance.filesChanged.subscribe(
@@ -845,6 +859,7 @@ describe('File drop component', () => {
fixture.detectChanges();
setupStandardFileChangeEvent();
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(1);
expect(filesChangedActual?.rejectedFiles[0].file.name).toBe('woo.txt');
@@ -861,7 +876,7 @@ describe('File drop component', () => {
expect(liveAnnouncerSpy.calls.count()).toBe(1);
});
- it('should reject a file with no type when accepted types are defined', () => {
+ it('should reject a file with no type when accepted types are defined', async () => {
let filesChangedActual: SkyFileDropChange | undefined;
componentInstance.filesChanged.subscribe(
@@ -885,6 +900,7 @@ describe('File drop component', () => {
];
setupStandardFileChangeEvent(files);
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(2);
expect(filesChangedActual?.rejectedFiles[1].file.name).toBe('woo.txt');
@@ -900,7 +916,7 @@ describe('File drop component', () => {
expect(liveAnnouncerSpy.calls.count()).toBe(0);
});
- it('should allow the user to specify accepted type with wildcards', () => {
+ it('should allow the user to specify accepted type with wildcards', async () => {
let filesChangedActual: SkyFileDropChange | undefined;
componentInstance.filesChanged.subscribe(
@@ -912,6 +928,7 @@ describe('File drop component', () => {
fixture.detectChanges();
setupStandardFileChangeEvent();
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(0);
@@ -928,7 +945,7 @@ describe('File drop component', () => {
expect(liveAnnouncerSpy.calls.count()).toBe(2);
});
- it('should load files and set classes on drag and drop', () => {
+ it('should load files and set classes on drag and drop', async () => {
let filesChangedActual: SkyFileDropChange | undefined;
componentInstance.filesChanged.subscribe(
@@ -965,7 +982,7 @@ describe('File drop component', () => {
validateDropClasses(true, false, dropElWrapper);
- triggerDrop(files, dropDebugEl);
+ triggerDrop(fixture, files, dropDebugEl);
validateDropClasses(false, false, dropElWrapper);
@@ -976,6 +993,7 @@ describe('File drop component', () => {
});
fixture.detectChanges();
+ await fixture.whenStable();
expect(filesChangedActual?.rejectedFiles.length).toBe(0);
expect(filesChangedActual?.files.length).toBe(1);
@@ -1043,7 +1061,7 @@ describe('File drop component', () => {
triggerDragOver(undefined, dropDebugEl);
validateDropClasses(true, false, dropElWrapper);
- triggerDrop(invalidFiles, dropDebugEl);
+ triggerDrop(fixture, invalidFiles, dropDebugEl);
validateDropClasses(false, false, dropElWrapper);
},
);
@@ -1071,7 +1089,7 @@ describe('File drop component', () => {
triggerDragEnter('sky-drop', dropDebugEl);
triggerDragOver(files, dropDebugEl);
- triggerDrop(files, dropDebugEl);
+ triggerDrop(fixture, files, dropDebugEl);
expect(fileReaderSpy.loadCallbacks.length).toBe(2);
});
@@ -1098,7 +1116,7 @@ describe('File drop component', () => {
triggerDragEnter('sky-drop', dropDebugEl);
triggerDragOver(files, dropDebugEl);
- triggerDrop(files, dropDebugEl);
+ triggerDrop(fixture, files, dropDebugEl);
expect(fileReaderSpy.loadCallbacks.length).toBe(0);
});
@@ -1123,7 +1141,7 @@ describe('File drop component', () => {
triggerDragEnter('sky-drop', dropDebugEl);
triggerDragOver(files, dropDebugEl);
- triggerDrop(files, dropDebugEl);
+ triggerDrop(fixture, files, dropDebugEl);
expect(fileReaderSpy.loadCallbacks.length).toBe(0);
});
@@ -1361,3 +1379,215 @@ describe('File drop component', () => {
helpController.expectCurrentHelpKey('helpKey.html');
});
});
+
+describe('File drop reactive component', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [SkyFileDropModule],
+ providers: [provideSkyFileReaderTesting()],
+ });
+ fixture = TestBed.createComponent(ReactiveFileDropTestComponent);
+ fixture.detectChanges();
+ });
+
+ it('should mark control as touched on file `drop` event', () => {
+ expect(fixture.componentInstance.fileDrop.touched).toBeFalse();
+ const dropEl = fixture.debugElement.query(By.css('.sky-file-drop'));
+
+ const dropEvent = {
+ dataTransfer: {},
+ stopPropagation: function (): void {},
+ preventDefault: function (): void {},
+ };
+
+ dropEl.triggerEventHandler('drop', dropEvent);
+ expect(fixture.componentInstance.fileDrop.touched).toBeTrue();
+ });
+
+ it('should mark control as touched on file drop clicked', () => {
+ expect(fixture.componentInstance.fileDrop.touched).toBeFalse();
+ const dropEl = fixture.nativeElement.querySelector('.sky-file-drop');
+
+ dropEl.click();
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.fileDrop.touched).toBeTrue();
+ });
+
+ it('should mark control as touched on link added', () => {
+ expect(fixture.componentInstance.fileDrop.touched).toBeFalse();
+ const linkButton = fixture.debugElement.query(
+ By.css('.sky-file-drop-link button'),
+ );
+ const linkEl = fixture.debugElement.query(
+ By.css('.sky-file-drop-link input'),
+ );
+
+ linkEl.triggerEventHandler('input', { target: { value: 'link.url' } });
+ fixture.detectChanges();
+
+ linkButton.nativeElement.click();
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.fileDrop.touched).toBeTrue();
+ });
+
+ it('should mark control as touched on link blur', () => {
+ expect(fixture.componentInstance.fileDrop.touched).toBeFalse();
+ const linkEl = fixture.nativeElement.querySelector(
+ '.sky-file-drop-link input',
+ );
+
+ SkyAppTestUtility.fireDomEvent(linkEl, 'blur');
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.fileDrop.touched).toBeTrue();
+ });
+
+ it('should set file drop to required using form control', () => {
+ fixture.componentInstance.labelText = 'File Drop';
+ fixture.componentInstance.fileDrop.addValidators(Validators.required);
+ fixture.detectChanges();
+
+ const label = fixture.nativeElement.querySelector(
+ '.sky-file-drop-label-text',
+ );
+
+ expect(label.classList.contains('sky-control-label-required')).toBeTrue();
+ });
+
+ it('should show required error', () => {
+ fixture.componentInstance.labelText = 'testing';
+ fixture.detectChanges();
+ const linkInput = fixture.nativeElement.querySelector(
+ '.sky-file-drop-link input',
+ );
+ SkyAppTestUtility.fireDomEvent(linkInput, 'blur');
+ fixture.detectChanges();
+
+ const requiredError = fixture.nativeElement.querySelector(
+ "sky-form-error[errorName='required']",
+ );
+ expect(requiredError).toBeVisible();
+ });
+
+ describe('form control value', () => {
+ it('should set value', async () => {
+ const file: SkyFileItem = {
+ file: new File([], 'foo.bar', { type: 'image/png' }),
+ url: 'foo.bar.bar',
+ };
+
+ const link: SkyFileLink = {
+ url: 'foo.foo',
+ };
+
+ fixture.componentInstance.fileDrop.setValue([file, link]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(fixture.componentInstance.fileDrop.value.length).toBe(2);
+ });
+
+ it('should not add invalid files', async () => {
+ fixture.componentInstance.fileDrop.setValue([
+ {
+ file: new File([], 'foo.bar', { type: 'image/png' }),
+ url: 'foo.bar.bar',
+ },
+ {
+ file: undefined,
+ url: 'foo.bar.bar',
+ },
+ {
+ url: 'foo.bar.bar',
+ },
+
+ {
+ url: undefined,
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(fixture.componentInstance.fileDrop.value.length).toBe(2);
+ });
+
+ it('should handle no valid files uploaded', async () => {
+ fixture.componentInstance.fileDrop.setValue('anything');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(fixture.componentInstance.fileDrop.value).toBe(null);
+
+ fixture.componentInstance.fileDrop.setValue([
+ {
+ file: undefined,
+ url: 'foo.bar.bar',
+ },
+ {
+ url: undefined,
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(fixture.componentInstance.fileDrop.value).toBe(null);
+ });
+
+ it('should handle rejecting all files', async () => {
+ fixture.componentInstance.acceptedTypes = 'image/png';
+ fixture.detectChanges();
+
+ fixture.componentInstance.fileDrop.setValue([
+ {
+ file: new File([], 'foo.foo', { type: 'abcd/png' }),
+ url: 'foo.bar.bar',
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(fixture.componentInstance.fileDrop.value).toBe(null);
+ });
+
+ it('should handle accepting some files', async () => {
+ fixture.componentInstance.acceptedTypes = 'image/png';
+ fixture.detectChanges();
+
+ fixture.componentInstance.fileDrop.setValue([
+ {
+ url: 'foo.bar.bar',
+ },
+ {
+ file: new File([], 'foo.foo', { type: 'abcd/png' }),
+ url: 'foo.bar.bar',
+ },
+ ]);
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(fixture.componentInstance.fileDrop.value.length).toBe(1);
+ });
+
+ it('should not set the form control value before handle files is complete', async () => {
+ fixture.componentInstance.fileDrop.setValue([
+ {
+ file: new File([], 'foo.bar', { type: 'image/png' }),
+ url: 'foo.bar.bar',
+ },
+ {
+ file: undefined,
+ url: 'foo.bar.bar',
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(fixture.componentInstance.fileDrop.value.length).toBe(1);
+ });
+ });
+});
diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.ts b/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.ts
index 84b4200f36..47c3e4a465 100644
--- a/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.ts
+++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.ts
@@ -12,8 +12,17 @@ import {
booleanAttribute,
inject,
} from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { SkyIdModule, SkyLiveAnnouncerService } from '@skyux/core';
+import {
+ ControlValueAccessor,
+ FormsModule,
+ NgControl,
+ Validators,
+} from '@angular/forms';
+import {
+ SkyFileReaderService,
+ SkyIdModule,
+ SkyLiveAnnouncerService,
+} from '@skyux/core';
import { SkyIdService } from '@skyux/core';
import { SkyHelpInlineModule } from '@skyux/help-inline';
import { SkyLibResourcesService } from '@skyux/i18n';
@@ -71,7 +80,7 @@ const MIN_FILE_SIZE_DEFAULT = 0;
SkyThemeModule,
],
})
-export class SkyFileDropComponent implements OnDestroy {
+export class SkyFileDropComponent implements OnDestroy, ControlValueAccessor {
/**
* Fires when users add or remove files.
*/
@@ -254,22 +263,84 @@ export class SkyFileDropComponent implements OnDestroy {
#_minFileSize = MIN_FILE_SIZE_DEFAULT;
+ #notifyTouched: (() => void) | undefined;
+ #notifyChange:
+ | ((_: (SkyFileItem | SkyFileLink)[] | undefined | null) => void)
+ | undefined;
+ #_uploadedFiles: (SkyFileItem | SkyFileLink)[] = [];
+
readonly #fileAttachmentService = inject(SkyFileAttachmentService);
+ readonly #fileReaderSvc = inject(SkyFileReaderService);
readonly #liveAnnouncerSvc = inject(SkyLiveAnnouncerService);
readonly #resourcesSvc = inject(SkyLibResourcesService);
readonly #idSvc = inject(SkyIdService);
protected errorId = this.#idSvc.generateId();
+
+ protected ngControl = inject(NgControl, { optional: true });
+
protected rejectedFiles: SkyFileItem[] = [];
+ constructor() {
+ if (this.ngControl) {
+ this.ngControl.valueAccessor = this;
+ }
+ }
+
public ngOnDestroy(): void {
this.filesChanged.complete();
this.linkChanged.complete();
this.linkInputBlur.complete();
}
+ public writeValue(value: unknown): void {
+ if (Array.isArray(value)) {
+ const linkUploads: SkyFileLink[] = [];
+ const fileUploads: SkyFileItem[] = [];
+
+ value.forEach((file) => {
+ if ('url' in file && file.url !== undefined) {
+ if (!('file' in file)) {
+ linkUploads.push(file);
+ } else if ('file' in file && file.file !== undefined) {
+ fileUploads.push(file);
+ }
+ }
+ });
+
+ if (!(linkUploads.length > 0) && !(fileUploads.length > 0)) {
+ this.#notifyChange?.(null);
+ } else {
+ this.#_uploadedFiles = [];
+
+ if (linkUploads.length > 0) {
+ linkUploads.forEach((file) => {
+ this.uploadLink(file);
+ });
+ }
+ if (fileUploads.length > 0) {
+ // this prevents FormControl from setting an invalid value before the async
+ // processes in #handleFile is complete
+ this.#notifyChange?.(null);
+ this.#handleFiles(fileUploads);
+ }
+ }
+ } else {
+ this.#notifyChange?.(null);
+ }
+ }
+
+ public registerOnChange(fn: any): void {
+ this.#notifyChange = fn;
+ }
+
+ public registerOnTouched(fn: () => void): void {
+ this.#notifyTouched = fn;
+ }
+
public dropClicked(): void {
if (!this.noClick && this.inputEl) {
+ this.#notifyTouched?.();
this.inputEl.nativeElement.click();
}
}
@@ -329,6 +400,8 @@ export class SkyFileDropComponent implements OnDestroy {
dropEvent.stopPropagation();
dropEvent.preventDefault();
+ this.#notifyTouched?.();
+
this.#enterEventTarget = undefined;
this.rejectedOver = false;
this.acceptedOver = false;
@@ -363,18 +436,33 @@ export class SkyFileDropComponent implements OnDestroy {
public addLink(event: Event): void {
event.preventDefault();
- this.linkChanged.emit({ url: this.linkUrl } as SkyFileLink);
+ this.uploadLink({ url: this.linkUrl } as SkyFileLink);
+ this.linkUrl = undefined;
+ this.#notifyTouched?.();
+ }
+
+ protected uploadLink(file: SkyFileLink): void {
+ this.linkChanged.emit(file);
+ this.#_uploadedFiles?.push(file);
+ this.#notifyChange?.(this.#_uploadedFiles);
this.#announceState(
'skyux_file_attachment_file_upload_link_added',
- this.linkUrl,
+ file.url,
);
- this.linkUrl = undefined;
}
public onLinkBlur(): void {
+ this.#notifyTouched?.();
this.linkInputBlur.emit();
}
+ protected get isRequired(): boolean {
+ return (
+ this.required ||
+ (this.ngControl?.control?.hasValidator(Validators.required) ?? false)
+ );
+ }
+
#announceState(resourceString: string, ...args: any[]): void {
this.#resourcesSvc
.getString(resourceString, ...args)
@@ -408,20 +496,22 @@ export class SkyFileDropComponent implements OnDestroy {
totalFiles: number,
): void {
rejectedFileArray.push(file);
+ this.#notifyChange?.(
+ this.#_uploadedFiles.length > 0 ? this.#_uploadedFiles : null,
+ );
this.#emitFileChangeEvent(totalFiles, rejectedFileArray, validFileArray);
}
- #loadFile(
+ async #loadFile(
fileDrop: SkyFileDropComponent,
file: SkyFileItem,
validFileArray: SkyFileItem[],
rejectedFileArray: SkyFileItem[],
totalFiles: number,
- ): void {
- const reader = new FileReader();
+ ): Promise {
+ try {
+ file.url = await this.#fileReaderSvc.readAsDataURL(file.file);
- reader.addEventListener('load', (event: any) => {
- file.url = event.target.result;
validFileArray.push(file);
fileDrop.#emitFileChangeEvent(
totalFiles,
@@ -432,34 +522,35 @@ export class SkyFileDropComponent implements OnDestroy {
'skyux_file_attachment_file_upload_file_added',
file.file.name,
);
- });
-
- reader.addEventListener('error', () => {
+ this.#_uploadedFiles?.push(file);
+ this.#notifyChange?.(this.#_uploadedFiles);
+ } catch {
fileDrop.#filesRejected(
file,
validFileArray,
rejectedFileArray,
totalFiles,
);
- });
-
- reader.addEventListener('abort', () => {
- fileDrop.#filesRejected(
- file,
- validFileArray,
- rejectedFileArray,
- totalFiles,
- );
- });
-
- reader.readAsDataURL(file.file);
+ }
}
- #handleFiles(files?: FileList | null): void {
- if (files) {
+ #handleFiles(fileList?: FileList | null | SkyFileItem[]): void {
+ if (fileList) {
const validFileArray: SkyFileItem[] = [];
const rejectedFileArray: SkyFileItem[] = [];
- const totalFiles = files.length;
+ const totalFiles = fileList.length;
+
+ let files: SkyFileItem[] = [];
+
+ if ('item' in fileList) {
+ for (let index = 0; index < fileList.length; index++) {
+ files.push({
+ file: fileList.item(index),
+ } as SkyFileItem);
+ }
+ } else {
+ files = fileList;
+ }
const processedFiles = this.#fileAttachmentService.checkFiles(
files,
@@ -478,7 +569,7 @@ export class SkyFileDropComponent implements OnDestroy {
totalFiles,
);
} else {
- this.#loadFile(
+ void this.#loadFile(
this,
file,
validFileArray,
diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.html b/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.html
new file mode 100644
index 0000000000..666dcf4c48
--- /dev/null
+++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.html
@@ -0,0 +1,12 @@
+
+
+{{fileDrop.value | json}} @for (file of fileDrop.value; track file) {
+
+}
diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.ts b/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.ts
new file mode 100644
index 0000000000..eef8088cef
--- /dev/null
+++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.ts
@@ -0,0 +1,42 @@
+import { CommonModule } from '@angular/common';
+import { Component, inject } from '@angular/core';
+import {
+ FormBuilder,
+ FormControl,
+ FormGroup,
+ FormsModule,
+ ReactiveFormsModule,
+ Validators,
+} from '@angular/forms';
+
+import { SkyFileItem } from '../../shared/file-item';
+import { SkyFileDropModule } from '../file-drop.module';
+import { SkyFileLink } from '../file-link';
+
+@Component({
+ imports: [SkyFileDropModule, FormsModule, ReactiveFormsModule, CommonModule],
+ selector: 'sky-file-drop-reactive-test',
+ standalone: true,
+ templateUrl: './reactive-file-drop.component.fixture.html',
+})
+export class ReactiveFileDropTestComponent {
+ public fileDrop: FormControl = new FormControl(
+ undefined,
+ Validators.required,
+ );
+ public formGroup: FormGroup = inject(FormBuilder).group({
+ fileDrop: this.fileDrop,
+ });
+ public labelText: string | undefined;
+ public acceptedTypes: string | undefined;
+
+ public deleteFile(file: SkyFileItem | SkyFileLink): void {
+ const index = this.fileDrop.value.indexOf(file);
+ if (index !== -1) {
+ this.fileDrop.value?.splice(index, 1);
+ }
+ if (this.fileDrop.value.length === 0) {
+ this.fileDrop.setValue(null);
+ }
+ }
+}