Angular Material: File Upload component tutorial
In this tutorial, we show an example of implementation for an input component that uses Angular Material and it's independent from the backend. This code could help you to develop your own upload file component.
In the second part of the tuorial, we create the unit tests for this component using Karma and Jasmine.
Create and test a file upload component in Angular
The Angular Material Team expressed multiple times that an implementation of input type="file"
or a file upload component won't be provided because every implementation will be too specific to the backend.
We intentionally don't support the file input, because the way it's implemented depends on how your backend is set up.
References:
input type="file" #24044
Add Support for type="file" for MatInput or Create Mat File Chooser component #25231
File Upload in Angular - Tutorial
This is the typescript code. We store the selected files in the FileList object.
We don't have a lot o options, this is the type of object returned by the <input>
component (mdn reference).
In the fileLabel
string we store the name of the selected file. If there are multiple selected files we will show a generic label with the number of selected files, e.g.: 3 files selected.
multipleFilesAccepted
allows us to select more than one file.
When the user selects the files these are stored in the FileList
object and an upload icon will be shown. If the user confirms clicking the icon onUploadFiles
the component emits an event with the file list. In our environment, we use Java to store the files, but this implementation is completely independent of any backend.
@Component({
selector:'app-file-upload',
templateUrl:'./file-upload.component.html'
})
export class FileUploadComponent {
@Input() placeholder = "Attach files";
@Output() uploadFiles: EventEmitter<FileList> = new EventEmitter<FileList>();
@ViewChild('inputForm') readonly inputForm!: any;
files!: FileList;
fileLabel = '';
multipleFilesAccepted = true;
onSelectFiles(event: any): void {
this.files = event.target.files ?? null;
this.filesLabel = this.getFilesLabel();
}
getFilesName() {
return this.filesLabel;
}
getFilesLabel(): string {
const filesSelected = this.files?.length;
switch (filesSelected) {
case 0: return 'No file selected';
case 1: return this.files[0].name;
default: return `${this.files.length} files selected`;
}
}
onUploadFiles(): void {
this.uploadFiles.next(this.files);
this.inputForm.nativeElement.reset();
}
}
The HTML
The html
is a standard matInput
component, the styling is done by the configuration of your application. We don't need to define specific styling. The input
is hidden and not visible to the user.
<form #inputForm>
<mat-form-field>
<mat-icon matIconPrefix matTooltip="Choose file(s)..." (click)="fileInput.click()">
attach_file</mat-icon>
<input matInput readonly
[value]="getFilesName()"
[placeholder]="placeholder">
<mat-icon matIconSuffix
color="primary"
matTooltip="Upload"
*ngIf="files && files.length > 0"
(click)="onUploadFiles()">
upload><mat-icon>
</mat-form-field>
<input hidden type="file" (change)="onSelectedFiles($event)"
#fileInput
[multiple]="multipleFilesAccepted">
</form>
Tests
Here are the tests with Karma and Jasmine, to verify that the component works correctly.
We created 2 tests, one with only one file uploaded and the second with
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FileUploadComponent } from './file-upload.component';
import { By } from "@angular/platform-browser";
describe('FileUploadComponent', () => {
let component: FileUploadComponent;
let fixture: ComponentFixture<FileUploadComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FileUploadComponent]
});
fixture = TestBed.createComponent(FileUploadComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('#uploadfiles() upload 1 file', () => {
spyOn(component, 'onSelectFiles').and.callThrough();
fixture.detectChanges()
const file = new File(['example file content'], 'myFile.txt');
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
const inputFileDebug = fixture.debugElement.query(By.css('input[type="file"]'));
const inputFile: HTMLInputElement = inputFileDebug.nativeElement;
inputFile.files = dataTransfer.files;
const changeEvent = new Event('change');
inputFile.dispatchEvent(changeEvent);
fixture.detectChanges();
expect(component.getFilesLabel()).toEqual("myFile.txt")
expect(component.onSelectFiles).toHaveBeenCalled();
expect(component.files.length).toEqual(1);
component.onUploadFiles();
// the files are removed from the component after the upload
expect(component.files.length).toEqual(0)
});
it('#uploadfiles() multiple files', () => {
spyOn(component, 'onSelectFiles').and.callThrough();
fixture.detectChanges()
const fileOne = new File(['example file content'], 'myFile.txt');
const fileTwo = new File(['2nd example file content'], 'myFile2.txt');
const dataTransfer = new DataTransfer();
dataTransfer.items.add(fileOne);
dataTransfer.items.add(fileTwo);
const inputFileDebug = fixture.debugElement.query(By.css('input[type="file"]'));
const inputFile: HTMLInputElement = inputFileDebug.nativeElement;
inputFile.files = dataTransfer.files;
const changeEvent = new Event('change');
inputFile.dispatchEvent(changeEvent);
fixture.detectChanges();
expect(component.getFilesLabel()).toEqual("2 files selected")
expect(component.onSelectFiles).toHaveBeenCalled();
expect(component.files.length).toEqual(2);
component.onUploadFiles();
// the files are removed from the component after the upload
expect(component.files.length).toEqual(0)
});
});
Follow demo
Video or images.