Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Typescript

Build a File Upload Component with Angular Material

5.00/5 (2 votes)
22 Mar 2018MIT4 min read 52.6K  
We will walk through Angular, Angular CLI and Angular Material to build a file upload component which does not exist in Angular Material

Introduction

Angular is a popular and powerful front-end framework for JavaScript web application development. With Angular Material, you can build web application with adaptability to different devices. This opened a neo-era of web application development. It's a pity that a file upload control is not included in Angular Material so let's build it ourselves. DIY IT!

Get Ready

Before we go into the actual code, you need to install NPM on your machine. Please download it from Node.js website. After you install it, run the command below from your terminal, command line, or powershell. It will install Angulai CLI globally on your machine.

npm install -g "@angular/cli"

After Angular CLI has been installed, run the command below to check the version.

ng --version

Result output will be like this:

Image 1

For more help about "ng", you can use "ng --help". I do not spend too much on it. Let's start our Angular project. Run the command below in your workspace folder.

ng new angular-material-file-upload

It creates an Angular project file structure and downloads all module dependencies.

Image 2

Open the project folder in your IDE, here I am using Visual Studio Code. It is lightweight but robust and fast.

Image 3

Now, let's add Angular Material dependencies in package.json. After it, run "npm install" in your project folder to download all modules.

JavaScript
"dependencies": {
      "@angular/animations": "^5.2.0",
      "@angular/common": "^5.2.0",
      "@angular/compiler": "^5.2.0",
      "@angular/core": "^5.2.0",
      "@angular/forms": "^5.2.0",
      "@angular/http": "^5.2.0",
      "@angular/platform-browser": "^5.2.0",
      "@angular/platform-browser-dynamic": "^5.2.0",
      "@angular/router": "^5.2.0",
      "@angular/cdk": "^5.2.4",
      "@angular/material": "^5.2.4",
      "core-js": "^2.4.1",
      "rxjs": "^5.5.6",
      "zone.js": "^0.8.19"
}

Image 4

Dive into Code

All preparation is done, let us create our file upload component by using command "ng generate". Run the command below under project folder.

ng generate component material-file-upload

It generates a new component with its folder under "app".

Image 5

Before we use Angular Material, we will need to import required modules into "app.module.ts".

JavaScript
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule, MatIconModule, MatProgressBarModule } from '@angular/material';
...
imports: [
    BrowserModule,
    HttpClientModule,
    BrowserAnimationsModule,
    MatButtonModule,
    MatIconModule,
    MatProgressBarModule
],
...

Modules have been imported so we will get our hands wet and code our new component. Below is the template code.

HTML
<button mat-button color="warn" (click)="onClick()">
      <mat-icon>file_upload</mat-icon>
      {{text}}
</button>
<br/>
<ul>
      <li *ngFor="let file of files" [@fadeInOut]="file.state">
            <mat-progress-bar [value]="file.progress"></mat-progress-bar>
            <span id="file-label">
            {{file.data.name}} 
            <a title="Retry" (click)="retryFile(file)" *ngIf="file.canRetry">
            <mat-icon>refresh</mat-icon></a>
            <a title="Cancel" (click)="cancelFile(file)" *ngIf="file.canCancel">
            <mat-icon>cancel</mat-icon></a>
            </span>
      </li>
</ul>
<input type="file" id="fileUpload" name="fileUpload" multiple="multiple" 
 accept="{{accept}}" style="display:none;"/>

Let's walk through the code a little bit. We have a button to trigger file selection, and actual element input[type="file"] will be hidden. Here, we use "mat-button" directive to render the button as a material button. To bind event of controls, we use (event_name)=event_handler(). To bind properties of control, we use [property_name]="value" or property_name="{{value}}". "value" itself is declared in component class. For iteration, we use *ngFor="let a of array". For flow control, we use *ngIf="boolean_expression". [@fadeInOut] is for animation effect. "fadeInOut" is the trigger name we defined in component class. We will see it in the next snippet.

The implemetation is actually simple. Whenever user selects a file, component will add it to the queue (array) to upload. If something interrupts the uploading process, user can redo it. Last, user can cancel uploading at any time before it is accomplished.

Here is the component class code.

JavaScript
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { HttpClient, HttpResponse, HttpRequest, 
         HttpEventType, HttpErrorResponse } from '@angular/common/http';
import { Subscription } from 'rxjs/Subscription';
import { of } from 'rxjs/observable/of';
import { catchError, last, map, tap } from 'rxjs/operators';

@Component({
      selector: 'app-material-file-upload',
      templateUrl: './material-file-upload.component.html',
      styleUrls: ['./material-file-upload.component.css'],
      animations: [
            trigger('fadeInOut', [
                  state('in', style({ opacity: 100 })),
                  transition('* => void', [
                        animate(300, style({ opacity: 0 }))
                  ])
            ])
      ]
})
export class MaterialFileUploadComponent implements OnInit {

      /** Link text */
      @Input() text = 'Upload';
      /** Name used in form which will be sent in HTTP request. */
      @Input() param = 'file';
      /** Target URL for file uploading. */
      @Input() target = 'https://file.io';
      /** File extension that accepted, same as 'accept' of <input type="file" />. 
          By the default, it's set to 'image/*'. */
      @Input() accept = 'image/*';
      /** Allow you to add handler after its completion. Bubble up response text from remote. */
      @Output() complete = new EventEmitter<string>();

      private files: Array<FileUploadModel> = [];

      constructor(private _http: HttpClient) { }

      ngOnInit() {
      }

      onClick() {
            const fileUpload = document.getElementById('fileUpload') as HTMLInputElement;
            fileUpload.onchange = () => {
                  for (let index = 0; index < fileUpload.files.length; index++) {
                        const file = fileUpload.files[index];
                        this.files.push({ data: file, state: 'in', 
                          inProgress: false, progress: 0, canRetry: false, canCancel: true });
                  }
                  this.uploadFiles();
            };
            fileUpload.click();
      }

      cancelFile(file: FileUploadModel) {
            file.sub.unsubscribe();
            this.removeFileFromArray(file);
      }

      retryFile(file: FileUploadModel) {
            this.uploadFile(file);
            file.canRetry = false;
      }

      private uploadFile(file: FileUploadModel) {
            const fd = new FormData();
            fd.append(this.param, file.data);

            const req = new HttpRequest('POST', this.target, fd, {
                  reportProgress: true
            });

            file.inProgress = true;
            file.sub = this._http.request(req).pipe(
                  map(event => {
                        switch (event.type) {
                              case HttpEventType.UploadProgress:
                                    file.progress = Math.round(event.loaded * 100 / event.total);
                                    break;
                              case HttpEventType.Response:
                                    return event;
                        }
                  }),
                  tap(message => { }),
                  last(),
                  catchError((error: HttpErrorResponse) => {
                        file.inProgress = false;
                        file.canRetry = true;
                        return of(`${file.data.name} upload failed.`);
                  })
            ).subscribe(
                  (event: any) => {
                        if (typeof (event) === 'object') {
                              this.removeFileFromArray(file);
                              this.complete.emit(event.body);
                        }
                  }
            );
      }

      private uploadFiles() {
            const fileUpload = document.getElementById('fileUpload') as HTMLInputElement;
            fileUpload.value = '';

            this.files.forEach(file => {
                  this.uploadFile(file);
            });
      }

      private removeFileFromArray(file: FileUploadModel) {
            const index = this.files.indexOf(file);
            if (index > -1) {
                  this.files.splice(index, 1);
            }
      }

}

export class FileUploadModel {
      data: File;
      state: string;
      inProgress: boolean;
      progress: number;
      canRetry: boolean;
      canCancel: boolean;
      sub?: Subscription;
}

Let's take a close look at what we have composed. In @Component() annotation, we defined an animation trigger "fadeInOut". It achieves that the list item in page will fade out and disappear after file uploading is done. We give component four @Input() properties to allow customization and one @Output() event emitter to allow interaction after file has been uploaded. One thing to be mentioned here, I am using "https://file.io" as development target.

In onClick(), whenever user selects new files, it will push file to array "files" and fire uploading handler. In uploadFile(), we implemented the core logic handling file uploading by using Angular HttpClient. We adorned it with progress reporting, failure retry, and cancellation.

Final Touch

We will add default material theme and material icons reference. Add the code below to index.html.

HTML
<head>
      ...
      <link href="https://fonts.googleapis.com/icon?family=Material+Icons" 
      rel="stylesheet">
</head>

Add default theme of Angular Material to styles.css.

JavaScript
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
body {
      background: #eeeeee;
      font-family: Roboto, "Helvetica Neue", sans-serif;
}

Add component CSS to material-file-upload.component.css.

CSS
ul,
li {
      margin: 0;
      padding: 0;
      list-style: none;
}

#file-label {
      display: inline-flex;
      vertical-align: middle;
      font-size: 12px;
      line-height: 18px;
}

#file-label mat-icon {
      font-size: 18px;
      text-align: center;
}

#file-label a {
      cursor: pointer;
}

Remove generated content and add component to app.component.html. We bind the @Output() interaction event to onFileComplete() in app.component.ts.

HTML
<div style="text-align:center">
      <app-material-file-upload (complete)="onFileComplete($event)"></app-material-file-upload>
</div>

Below is onFileComplete() implemetation in app.component.ts.

onFileComplete(data: any) {
      console.log(data); // We just print out data bubbled up from event emitter.
}

Go back to your terminal, command line or powershell, run the command below:

npm start

Image 6

Open your browser and navigate to http://localhost:4200/. You should see your file upload button as below:

Image 7

Upload files to test.

Image 8

After file is uploaded, you can see the output from console in Developer Tools.

Image 9

Soar into the Sky

Angular is really a great framework choice for front-end web development. Kudos to Google and all code contributors. Its great features like routing, modularization, TypeScript adoption, dependency injection, etc. makes web application evolved to a new era. Hope you will enjoy it and produce your own components and applications.

License

This article, along with any associated source code and files, is licensed under The MIT License