Angular Material: File Upload component tutorial

Updated: 2023-06-19

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.


You could be interested in

Right click context menu with Angular

Right click custom menu inside dynamic lists with Angular Material
2020-05-31

Enums in Angular templates

How to use enum in the html template
2019-01-21
Fullstack Angular / Java application quick start guide.
WebApp built by Marco using SpringBoot 3.2.4 and Java 21. Hosted in Switzerland (GE8).