Using a directive and a FormControl for a similar problem, my code looks like this:
directive:
// angular imports
import { Directive, HostBinding, HostListener, Output, EventEmitter } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
// models imports
import { FileHandle } from '../models';
@Directive({
selector: '[appFilesDrop]'
})
export class FilesDropDirective {
@Output() files: EventEmitter<FileHandle[]> = new EventEmitter();
@HostBinding('style.background') private background = '#eee';
constructor(
private sanitizer: DomSanitizer
) { }
@HostListener('dragover', ['$event']) public onDragOver(evt: DragEvent) {
evt.preventDefault();
evt.stopPropagation();
this.background = '#999';
}
@HostListener('dragleave', ['$event']) public onDragLeave(evt: DragEvent) {
evt.preventDefault();
evt.stopPropagation();
this.background = '#eee';
}
@HostListener('drop', ['$event']) public onDrop(evt: DragEvent) {
evt.preventDefault();
evt.stopPropagation();
this.background = '#eee';
const files: FileHandle[] = [];
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < evt.dataTransfer.files.length; i++) {
const file = evt.dataTransfer.files[i];
const url = this.sanitizer.bypassSecurityTrustUrl(window.URL.createObjectURL(file));
files.push({ file, url });
}
if (files.length > 0) {
this.files.emit(files);
}
}
}
models.ts:
export interface FileHandle {
url: SafeUrl;
file?: File;
}
html code:
<!-- Image upload field -->
<ng-container *ngIf="formField.type === 'filesUpload'" class="example-full-width image-field-container">
<mat-form-field
appearance="outline"
floatLabel="always"
class=".example-full-width"
>
<mat-label>{{ objectName }} {{formField.label}}</mat-label>
<!-- This block is only displayed if there's no image -->
<!-- it's necessary because we need a formControlName in a mat-form-field -->
<input
*ngIf="formGroup.controls[formField.key].value === null"
class="image-input"
matInput
[formControlName]="formField.key"
type="text"
appFilesDrop
[placeholder]="formField.placeholder"
autocomplete="off"
(files)="filesDropped($event, formField.key)"
readonly
>
<!-- Display errors if any -->
<mat-error
*ngIf="formGroup.controls[formField.key].hasError('required') && (formGroup.controls[formField.key].dirty || formGroup.controls[formField.key].touched)"
>
A {{formField.label.toLocaleLowerCase()}} is required
</mat-error>
<mat-error *ngIf="formGroup.controls[formField.key].hasError('tooLongArray')">
You must drop only one image, you dropped {{formGroup.controls[formField.key].errors.tooLongArray.length}} images.
</mat-error>
<mat-error *ngIf="formGroup.controls[formField.key].hasError('forbiddenMime')">
You must drop files of type {{formGroup.controls[formField.key].errors.forbiddenMime.forbidden.join(', ')}}.
All your files must be of type {{formGroup.controls[formField.key].errors.forbiddenMime.allowed.join(', ')}}
</mat-error>
<mat-error *ngIf="formGroup.controls[formField.key].hasError('tooLargeFile')">
The file(s) {{formGroup.controls[formField.key].errors.tooLargeFile.forbidden.join(', ')}} is/are too big.
The maximum allowed size is {{formGroup.controls[formField.key].errors.tooLargeFile.allowed/1024 | number}}kb
</mat-error>
<!-- If we have an image we need to display it. -->
<!-- We created a hidden mat-form-field and display the image -->
<ng-container *ngIf="formGroup.controls[formField.key].value !== null">
<div class="">
<input matInput [formControlName]="formField.key" style="visibility: hidden;" class="invisible">
<img [src]="formGroup.controls[formField.key].value[0].url" class="image-uploaded">
</div>
<button mat-mini-fab color="warn" (click)="removeFiles(formField.key)"><mat-icon>delete</mat-icon></button>
</ng-container>
</mat-form-field>
</ng-container>
The overall validation logic is a bit complex because I'm using a service which is generating the FormGroup dynamically but the logic is easy to understand even if you can't copy/paste it:
// file fields
case('filesUpload'): {
const options = field.options as FileFieldOptions;
// build the validators
validators.push(mimeTypeValidator(options.allowedMimes));
if (!options.multiple) { validators.push(arrayLengthValidator(1)); }
if (options.maxSize) { validators.push(fileSizeValidator(options.maxSize)); }
// get the initial value
if (!options.multiple && initialValue && initialValue[field.key]) {
// we only have 1 object, so we will have on url from the api and we can provide the formControl as a 1 object control
initialFieldValue = [{
url: initialValue[field.key]
}] as Array<FileHandle>;
}
break;
Forgot to add the example of validators:
For the file size validator:
// angular imports
import { ValidatorFn, AbstractControl, } from '@angular/forms';
// model imports
import { FileHandle } from '../models';
export function fileSizeValidator(maxFileSize: number): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
// validate when there's no value
if (control.value === null || control.value === undefined ) { return null; }
// validate when we only have a url (it comes from server)
if (control.value[0].file === undefined) { return null; }
const fileHandles = control.value as Array<FileHandle>;
const tooLargeFiles = fileHandles.filter(fileHandle => fileHandle.file.size > maxFileSize).map(fileHandle => fileHandle.file.name);
return tooLargeFiles.length > 0 ? {tooLargeFile: {forbidden: tooLargeFiles, allowed: maxFileSize}} : null;
};
}
and a MimeType validator:
// angular imports
import { ValidatorFn, AbstractControl, } from '@angular/forms';
// model imports
import { FileHandle } from '../models';
export function mimeTypeValidator(allowedMimeTypes: Array<string>): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
// validate when there's no value
if (control.value === null || control.value === undefined ) { return null; }
// validate when we only have a url (it comes from server)
if (control.value[0].file === undefined) { return null; }
const fileHandles = control.value as Array<FileHandle>;
const forbiddenMimes = fileHandles.map(fileHandle => fileHandle.file.type).filter(mime => !allowedMimeTypes.includes(mime));
return forbiddenMimes.length > 0 ? {forbiddenMime: {forbidden: forbiddenMimes, allowed: allowedMimeTypes}} : null;
};
}