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:
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.
Open the project folder in your IDE, here I am using Visual Studio Code. It is lightweight but robust and fast.
Now, let's add Angular Material dependencies in package.json. After it, run "npm install
" in your project folder to download all modules.
"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"
}
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".
Before we use Angular Material, we will need to import required modules into "app.module.ts".
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.
<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.
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 {
@Input() text = 'Upload';
@Input() param = 'file';
@Input() target = 'https://file.io';
@Input() accept = 'image/*';
@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.
<head>
...
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
</head>
Add default theme of Angular Material to styles.css.
@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.
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.
<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
Open your browser and navigate to http://localhost:4200/. You should see your file upload button as below:
Upload files to test.
After file is uploaded, you can see the output from console in Developer Tools.
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.