Skip to content

Instantly share code, notes, and snippets.

@stuartaccent
Last active March 18, 2021 16:24
Show Gist options
  • Save stuartaccent/51afc6b17d89d4dc6f3968ede5d789b6 to your computer and use it in GitHub Desktop.
Save stuartaccent/51afc6b17d89d4dc6f3968ede5d789b6 to your computer and use it in GitHub Desktop.
Angular 7 file uploader with queue and progress using HttpClient, example https://stackblitz.com/edit/angular-7-file-upload-queue
<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);
}
}
@stuartaccent
Copy link
Author

You may have to fiddle with the request headers depending on your api

@stuartaccent
Copy link
Author

stuartaccent commented Sep 15, 2017

added ability to cancel a request and a few minor tweaks

@stuartaccent
Copy link
Author

changed statuses to enums

@Ks89
Copy link

Ks89 commented Jan 9, 2018

very useful, thank u.

@Ks89
Copy link

Ks89 commented Jan 9, 2018

@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

@Ks89
Copy link

Ks89 commented Jan 9, 2018

I'm sorry for the stupid question. It's working also with multipart without to change anything. Thanks 👍

@stuartaccent
Copy link
Author

hey @Ks89, np, glad u found it useful.

@opensensesolutions
Copy link

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.

@stuartaccent
Copy link
Author

@Richardtugwell
Copy link

Good stuff - many thanks! Plugged into an app and worked perfectly OOTB

@alexytiger
Copy link

Thank you! Very helpful.

@ganeshmuthuvelu
Copy link

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?

@stuartaccent
Copy link
Author

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.

@Resington
Copy link

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....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment