Previewing,Uploading and Reporting Progress of files using Node and Angular

Angular&NodeEnthusiast
7 min readApr 4, 2020

--

You would have faced a situation when you want to upload a profile photo or multiple documents on a social networking website or a job portal. Its a good user experience when you are aware of the progress in the document upload.

Below is a short demo of the same. I have selected 9 files(1 JPEG and 8 GIF) and trying to upload them to a Node Express server in a slow 3G connection(simulated via Chrome dev tools). The slow connection is required to show the upload progress from 0 to 100%.

Angular Project

The angular project uses 2 Components: AppComponent and a ProgressComponent.

In the AppComponent, we have a simple form to select multiple images and preview them before uploading them.

In the ProgressComponent, we have a progress bar to show the percentage completion of upload of each image.

We have a FileUploadService to submit the form data to the Node server.

AppComponent Template:

<input type=”file”> enables us in selecting the file. We are restricting ourselves only to images(possible with the accept attribute).

We can select single or multiple files. multiple attribute makes it possible to select multiple files.

The enctype attribute tells us the encoding of the form data when submitted to the server. multipart/form-data is used mainly for uploading files. It is important to set this attribute as the value of this attribute is set in the “Content-Type” header.

Once an image/multiple images are selected, they can be previewed in the below section of code.

<div class=”imagePreview”>
<div *ngFor=”let x of imageList”>
<img class=”img-responsive img-circle” [src]=”x.file” (click)=”openBrowse($event)” alt=”Update Photo”>
<app-progress [ratio]=”x.ratio”></app-progress>
</div>
</div>

imageList is an array of objects. Each object contains the details of the image to be uploaded to the server. Below is a screenshot of the structure of the imageList property. Each object contains a file, ratio, name and size properties.

The file property is assigned to src property of the <img> element to enable preview of the image.

The ratio property is passed to the ProgressComponent as @Input(‘ratio’).

The name property is the name of the file. It is required to differentiate between the objects in the imageList array.

<app-progress> is the selector for the ProgressComponent. We are passing the image upload progress ratio as @Input(‘ratio’) to the ProgressComponent.

AppComponent Class

  1. Once a file is selected, a change event is triggered on <input type=”file”> and the preview() is called. We are using the object reader created by FileReader() to read the content of the files selected.readAsDataURL() reads the contents of the selected file and once reading is complete, the onload event is triggered, where a URL representing the file contents is available in a result property.
preview(event:any){
//preview an image

Array.from(event.target.files).forEach((file:any,fileIndex:number)=>{
const reader=new FileReader(); //a new reader object for each file
fromEvent(reader,’load’).subscribe((loadEvent:any)=>{
this.imageList.push({file:loadEvent.target.result,ratio:0,name:file.name,size:file.size}); //display the image once reader’s load event is triggered
})

reader?.readAsDataURL(file);
})
}

2. We are pushing the details of the file/files selected into the imageList array.

this.imageList.push({file:loadEvent.target.result,ratio:0,name:file.name,size:file.size}); //display the image once reader’s load event is triggered

Each object of the imageList array contains 4 properties: file,ratio,name and size. The file property contains the contents of the file in URL format. The ratio property is set to an initial value of 0. The name and size properties are set to the name and size of the file respectively.

If you recall, we are iterating over this imageList property in the template to preview all the images.

<div class=”imagePreview”>
<div *ngFor=”let x of imageList”>
<img class=”img-responsive img-circle” [src]=”x.file” (click)=”openBrowse($event)” alt=”Update Photo”>
<app-progress [ratio]=”x.ratio”></app-progress>
</div>
</div>

Once an image has been previewed and you feel that its not good enough, you could click on the image to re-select some other image. This is made possible via the openBrowse(),where we are explicitly creating a click on <input type=”file”> to open the file selector window. photoselector is a reference to <input type=”file”>.

openBrowse(event:any){ //reselecting an image
this.photoselector?.nativeElement.click();
}

When you are satisfied with the previewed image, you can click on the “Upload Photos” button which calls the upload() in the class. We are converting this.photoselector?.nativeElement.files from FileList into an array using Array.from(). We are then mapping each file in the array into an observable returned by the uploadPhoto() of the FileUploadService. We are then passing this array of observables into the rxjs forkjoin operator so that they can be executed in parallel i.e the files will be uploaded to the Node server in parallel.

upload(event:any){ //upload the previewed images
let obsv$:Observable<any>[]=
Array.from(this.photoselector?.nativeElement.files).map(
(file:any,fileIndex:number)=>this.fileuploadService.uploadPhoto(file).pipe(catchError(err=>{console.log(err);return EMPTY;})));

forkJoin(obsv$).subscribe((result)=>{
console.log(result)
});
}

3. In the ngOnInit() of the class, we are calling the getUploadingProgress() of the FileUploadService. As the name of the method suggests, this method returns live updates on the upload progress of each of the files. We are subscribing to the observable returned by this method.

ngOnInit() {
this.fileuploadService.getUploadingProgress().pipe(
tap((result:any)=>{
let entry=this.imageList.find(x=>x.name === result.name);
entry ? entry.ratio = result.ratio: null;
})
).subscribe();
}

If you recall, the imageList array is an array of objects, where each object contains 4 properties: file,name,ratio and size. We are differentiating between the objects based on the name property.

In the code below, we are updating the ratio property of the object for which a live update has been received on the upload progress.

let entry=this.imageList.find(x=>x.name === result.name);
entry ? entry.ratio = result.ratio: null;

FileUploadService

  1. The uploadingProgressSub is a subject which plays an important role in providing live updates to the component on the upload progress of each file.
  2. We have already seen the getUploadingProgress() getting called in the ngOnInit() of the class. This method just returns the observable corresponding to the subject.
getUploadingProgress() {
return this.uploadingProgressSub.asObservable();
}

3. When the “Upload Photos” button is clicked, the upload() in the class is called, which in turn calls the uploadPhoto() in the service.

uploadPhoto(photo: any) {
var formdata: FormData = new FormData();
formdata.append(“uploads”, photo);

return this.http.post(`${environment.baseUrl}uploadPhoto`, formdata, { reportProgress: true, observe: ‘events’ }).pipe(
tap((event: any) => {
if (event.type == HttpEventType.UploadProgress) //1
{
this.setUploadingProgress({ ratio: event.loaded / event.total, name: photo.name, size: photo.size });
}
})
);
}
  1. We are creating a FormData object.It provides a way to easily construct a set of key/value pairs representing form fields and their values.
var formdata: FormData = new FormData();
formdata.append(“uploads”, photo);

2. Observe the additional options passed to the HTTP POST request.

this.http.post(`${environment.baseUrl}uploadPhoto`, formdata, { reportProgress: true, observe: ‘events’ })

Sometimes applications transfer large amounts of data and those transfers can take a long time. The image upload in this story is an example. We are giving the users a better experience by providing feedback on the progress of such transfers. We have set reportProgress to true, to enable tracking of progress events.

The observe value determines the return type, according to what you are interested in observing. An observe value of “events” returns an observable of the raw HttpEvent stream, including progress events by default.

3. Since we posting data to the server, the HttpEvent will be of type UploadProgress.

if (event.type == HttpEventType.UploadProgress) {
this.setUploadingProgress({ ratio: event.loaded / event.total, name: photo.name, size: photo.size });
}

event.load is the no of bytes of the file loaded.

event.total is the total no of bytes that needs to be loaded.

event.load/event.total gives the progress ratio of the file being uploaded.

We are calling the setUploadingProgress(), passing an object containing the progress ratio, name and size of the file.

setUploadingProgress(data: any) {
this.uploadingProgressSub.next(data);
}

In the setUploadingProgress(), we are passing this object to the uploadProgressSub subject.

If you recall, the AppComponent has subscribed to this subject in the ngOnInit() lifecycle hook.

ProgressComponent Template:

<div class=”container pending”>
<div class=”progress-container” [ngClass]=”ratio === 1 ? ‘complete’ : ‘pending’”>
<div [ngStyle]=”{width:progress}”>
</div>
<div class=”progress-message”>{{progress}} uploaded</div>
</div>
</div>

The “complete” and the “pending” CSS classes used are defined as below:

.complete{
background-color: lightgreen;
}

.pending{
background-color:rgb(243, 111, 111);
}

ProgressComponent Class: The class receives the @Input(‘ratio’) from the AppComponent via property binding. We are just multiplying this ratio by 100 to get the percentage and then rounding to off to the nearest whole number. We are using this percentage to fix the width of the progress bar.

Finally lets get to Node server project. The node server is listening for requests on port 3000.

index.js

We are using the Multer module to handle multipart/form-data. Its mainly used for uploading files. Please check this link for more details on this module.

const photoUpload = multer({
storage: multer.diskStorage({
destination: function (req, file, fun) {
fun(null, './public/images'); //set the destination path
},
filename: function (req, file, fun) {
fun(null, file.originalname); //set the destination filename
}
})
}).array('uploads');

In the above code, we are configuring the path on the server where I want to upload the files and also the filename with which I want to save the files.

I want the files to be saved with their original filename here.

array(“uploads”) means the module will look for single/multiple files with the field name “uploads”.

Please remember that in the FormData object,we have used the key name “uploads”.

If you want to upload just a single file, then you could also go for single(“uploads”). This wouldn’t allow multiple files to get uploaded.

Please find below the git repositories for the angular and node project.

--

--

Angular&NodeEnthusiast
Angular&NodeEnthusiast

Written by Angular&NodeEnthusiast

Loves Angular and Node. I wish to target issues that front end developers struggle with the most.

No responses yet