-
-
Save stuartaccent/51afc6b17d89d4dc6f3968ede5d789b6 to your computer and use it in GitHub Desktop.
<div class="row"> | |
<div class="col-md-3"> | |
<h3>Select files</h3> | |
<input type="file" #fileInput multiple (change)="addToQueue()" /> | |
</div> | |
<div class="col-md-9"> | |
<h3>Upload queue</h3> | |
<table class="table-headed table-striped"> | |
<thead> | |
<tr> | |
<th class="text-left">Name</th> | |
<th class="text-right">Size</th> | |
<th class="text-left">Progress</th> | |
<th class="text-left">Status</th> | |
<th class="text-right">Actions</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr *ngFor="let item of queue | async"> | |
<td>{{ item?.file?.name }}</td> | |
<td class="text-right">{{ item?.file?.size/1024/1024 | number:'.2' }} MB</td> | |
<td>{{ item.progress + ' %' }}</td> | |
<td> | |
<span *ngIf="item.isPending()" class="tag tag-default"></span> | |
<span *ngIf="item.isSuccess()" class="tag tag-success"></span> | |
<span *ngIf="item.inProgress()" class="tag tag-warning"></span> | |
<span *ngIf="item.isError()" class="tag tag-danger"></span> | |
</td> | |
<td class="text-right"> | |
<a tooltip="Upload file" (click)="item.upload()" *ngIf="item.isUploadable()"> | |
<i class="fa fa-upload"></i> | |
</a> | |
<a tooltip="Cancel upload" (click)="item.cancel()" *ngIf="item.inProgress()"> | |
<i class="fa fa-times-circle"></i> | |
</a> | |
<a tooltip="Remove from queue" (click)="item.remove()" *ngIf="!item.inProgress()"> | |
<i class="fa fa-trash"></i> | |
</a> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
<div> | |
<a class="button" (click)="uploader.clearQueue()">Clear queue</a> | |
<a class="button button-primary" (click)="uploader.uploadAll()">Upload all</a> | |
</div> | |
</div> | |
</div> |
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'; | |
import { FileQueueObject, FileUploaderService } from './file-uploader.service'; | |
import { Observable } from 'rxjs'; | |
@Component({ | |
selector: 'file-uploader, [file-uploader]', | |
templateUrl: 'file-uploader.component.html', | |
styleUrls: ['./file-uploader.component.css'] | |
}) | |
export class FileUploaderComponent implements OnInit { | |
@Output() onCompleteItem = new EventEmitter(); | |
@ViewChild('fileInput') fileInput; | |
queue: Observable<FileQueueObject[]>; | |
constructor(public uploader: FileUploaderService) { } | |
ngOnInit() { | |
this.queue = this.uploader.queue; | |
this.uploader.onCompleteItem = this.completeItem; | |
} | |
completeItem = (item: FileQueueObject, response: any) => { | |
this.onCompleteItem.emit({ item, response }); | |
} | |
addToQueue() { | |
const fileBrowser = this.fileInput.nativeElement; | |
this.uploader.addToQueue(fileBrowser.files); | |
} | |
} |
import * as _ from 'lodash'; | |
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpRequest, HttpResponse } from '@angular/common/http'; | |
import { Injectable, Output } from '@angular/core'; | |
import { BehaviorSubject, Subscription } from 'rxjs'; | |
import { HttpEventType } from '@angular/common/http'; | |
export enum FileQueueStatus { | |
Pending, | |
Success, | |
Error, | |
Progress | |
} | |
export class FileQueueObject { | |
public file: any; | |
public status: FileQueueStatus = FileQueueStatus.Pending; | |
public progress: number = 0; | |
public request: Subscription = null; | |
public response: HttpResponse<any> | HttpErrorResponse = null; | |
constructor(file: any) { | |
this.file = file; | |
} | |
// actions | |
public upload = () => { /* set in service */ }; | |
public cancel = () => { /* set in service */ }; | |
public remove = () => { /* set in service */ }; | |
// statuses | |
public isPending = () => this.status === FileQueueStatus.Pending; | |
public isSuccess = () => this.status === FileQueueStatus.Success; | |
public isError = () => this.status === FileQueueStatus.Error; | |
public inProgress = () => this.status === FileQueueStatus.Progress; | |
public isUploadable = () => this.status === FileQueueStatus.Pending || this.status === FileQueueStatus.Error; | |
} | |
// tslint:disable-next-line:max-classes-per-file | |
@Injectable() | |
export class FileUploaderService { | |
public url: string = 'https://jsonplaceholder.typicode.com/posts'; | |
private _queue: BehaviorSubject<FileQueueObject[]>; | |
private _files: FileQueueObject[] = []; | |
constructor(private http: HttpClient) { | |
this._queue = <BehaviorSubject<FileQueueObject[]>>new BehaviorSubject(this._files); | |
} | |
// the queue | |
public get queue() { | |
return this._queue.asObservable(); | |
} | |
// public events | |
public onCompleteItem(queueObj: FileQueueObject, response: any): any { | |
return { queueObj, response }; | |
} | |
// public functions | |
public addToQueue(data: any) { | |
// add file to the queue | |
_.each(data, (file: any) => this._addToQueue(file)); | |
} | |
public clearQueue() { | |
// clear the queue | |
this._files = []; | |
this._queue.next(this._files); | |
} | |
public uploadAll() { | |
// upload all except already successfull or in progress | |
_.each(this._files, (queueObj: FileQueueObject) => { | |
if (queueObj.isUploadable()) { | |
this._upload(queueObj); | |
} | |
}); | |
} | |
// private functions | |
private _addToQueue(file: any) { | |
const queueObj = new FileQueueObject(file); | |
// set the individual object events | |
queueObj.upload = () => this._upload(queueObj); | |
queueObj.remove = () => this._removeFromQueue(queueObj); | |
queueObj.cancel = () => this._cancel(queueObj); | |
// push to the queue | |
this._files.push(queueObj); | |
this._queue.next(this._files); | |
} | |
private _removeFromQueue(queueObj: FileQueueObject) { | |
_.remove(this._files, queueObj); | |
} | |
private _upload(queueObj: FileQueueObject) { | |
// create form data for file | |
const form = new FormData(); | |
form.append('file', queueObj.file, queueObj.file.name); | |
// upload file and report progress | |
const req = new HttpRequest('POST', this.url, form, { | |
reportProgress: true, | |
}); | |
// upload file and report progress | |
queueObj.request = this.http.request(req).subscribe( | |
(event: any) => { | |
if (event.type === HttpEventType.UploadProgress) { | |
this._uploadProgress(queueObj, event); | |
} else if (event instanceof HttpResponse) { | |
this._uploadComplete(queueObj, event); | |
} | |
}, | |
(err: HttpErrorResponse) => { | |
if (err.error instanceof Error) { | |
// A client-side or network error occurred. Handle it accordingly. | |
this._uploadFailed(queueObj, err); | |
} else { | |
// The backend returned an unsuccessful response code. | |
this._uploadFailed(queueObj, err); | |
} | |
} | |
); | |
return queueObj; | |
} | |
private _cancel(queueObj: FileQueueObject) { | |
// update the FileQueueObject as cancelled | |
queueObj.request.unsubscribe(); | |
queueObj.progress = 0; | |
queueObj.status = FileQueueStatus.Pending; | |
this._queue.next(this._files); | |
} | |
private _uploadProgress(queueObj: FileQueueObject, event: any) { | |
// update the FileQueueObject with the current progress | |
const progress = Math.round(100 * event.loaded / event.total); | |
queueObj.progress = progress; | |
queueObj.status = FileQueueStatus.Progress; | |
this._queue.next(this._files); | |
} | |
private _uploadComplete(queueObj: FileQueueObject, response: HttpResponse<any>) { | |
// update the FileQueueObject as completed | |
queueObj.progress = 100; | |
queueObj.status = FileQueueStatus.Success; | |
queueObj.response = response; | |
this._queue.next(this._files); | |
this.onCompleteItem(queueObj, response.body); | |
} | |
private _uploadFailed(queueObj: FileQueueObject, response: HttpErrorResponse) { | |
// update the FileQueueObject as errored | |
queueObj.progress = 0; | |
queueObj.status = FileQueueStatus.Error; | |
queueObj.response = response; | |
this._queue.next(this._files); | |
} | |
} |
changed statuses to enums
very useful, thank u.
@stuartaccent I have a question. Is multipart/form-data
supported by your code?
I trying to call a server built with https://github.com/expressjs/multer
I'm sorry for the stupid question. It's working also with multipart without to change anything. Thanks 👍
hey @Ks89, np, glad u found it useful.
Very helpful, thank you!
I had to add this to the @component : providers: [FileUploaderService],
and in the service.ts I had to add: import { Subscription } from 'rxjs/Subscription';
and I ended up importing HttpClientModule in my main app to solve a StaticInjectorError.
To help beginners like myself you might also considering adding the stylesheet and the global vars file so it works right out of the box.
Have added an example at https://stackblitz.com/edit/angular-5-file-upload-queue
Good stuff - many thanks! Plugged into an app and worked perfectly OOTB
Thank you! Very helpful.
If the size of the selected files exceed 100 MB, it does not work - the file selection will not even list the files. Is there a limitation or restriction?
Hi @ganeshmuthuvelu,
sorry for the massive delay. Did u ever find out what this was? there is no limit that i know of.
The only thing that springs to mind that does have a 100mg limit for a request is Cloudflare.
Hi everyone...I am a beginner... Thanks a lot to Mr. Stuartaccent for your awesome code.. I have download your code and it works fine... When i change the URL to my cloud domain's URL it is showing this error.... "HttpErrorResponse {headers: {…}, status: 0, statusText: "Unknown Error",".. I have tried with localhost which has xamp server too. Do i need to run any code in my server part? If so what code should i run? Thanks in advance....
added ability to cancel a request and a few minor tweaks