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

AngularEnthusiast
6 min readApr 4, 2020
ProgressBar

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

I am sharing a working solution of how you could preview and then upload a file in an Angular project to a Node server and also view the progress of the upload.

Lets begin with the Angular project elements.

We have 2 components=>

ComponentA which has a simple form to upload and preview the image file.

ProgressComponent which has a progress bar to show the percentage of completion of upload.

We have 2 services=>

ServiceA to connect to the Node server and submit the data.

InterceptorService to report the progress of the upload.

ComponentA template:

<h3>Upload progress</h3><form [formGroup]=”fileUpload”><input type=”file” multiple #photoselector formControlName=”profilepicSelector” enctype=”multipart/form-data” accept=”image/*” name=”photos” (change)=”preview($event)”><button type=”button” [disabled]=”fileUpload.invalid” (click)=”upload($event)”>Upload Photo</button></form><div id=”imagePreview”><img *ngFor=”let x of imageList” class=”img-responsive img-circle” [src]=”x” (click)=”openBrowse($event)” alt=” Update Photo”></div><app-progress [ratio]=”ratio”></app-progress>

<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 code:

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

Below is the reusable ProgressComponent:

<app-progress [ratio]=”ratio”></app-progress>

ComponentA class:

export class ComponentA implements OnInit {constructor(private serv:ServiceA) { }@ViewChild(‘photoselector’) photoselector:ElementRef;imageList:any[];ratio:number=0;fileUpload=new FormGroup({
profilepicSelector:new FormControl(“”,[Validators.required])
})
ngOnInit() {
this.serv.returnProgressObservable().subscribe((data)=>this.ratio=data); //subscribing to the subject
}preview(event){ //preview an image
const list=event.target.files;
let images=[];
//Temporary variable to hold the images since "this" cannot be //accessed inside onload
for(let i=0;i<list.length;i++){
const reader=new FileReader();
reader.onload=function(e)
{
images.push(e.target.result);
}
reader.readAsDataURL(list[i]);
this.imageList=images;
}
}
openBrowse(event)
{ //reselecting an image
this.photoselector.nativeElement.click();
}
upload(event){ //upload the previewed images
this.serv.uploadPhoto(this.photoselector.nativeElement.files).subscribe(data=>console.log(data));
}
}

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.

We are pushing all the results into an array imageList and iterating over it as shown below so that the images can be previewed.

<div id=”imagePreview”><img *ngFor=”let x of imageList” class=”img-responsive img-circle” [src]=”x” (click)=”openBrowse($event)” alt=” Update Photo”></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.

When you are satisfied with the previewed image, you can click on the Upload button which calls the upload(). This function calls a uploadPhoto() in the service to send the data to the server.

Image preview

ServiceA:

export class ServiceA {constructor(private http:HttpClient) { }private progressSub=new BehaviorSubject(0);returnProgressObservable(){
return this.progressSub.asObservable();
}
returnProgress(data)
{
this.progressSub.next(data);
}
uploadPhoto(photo)
{
var formdata:any=new FormData();
for(var i=0;i<photo.length;i++)
{
formdata.append(“uploads”,photo[i]);
}
return this.http.post(‘http://localhost:8080/uploadPhoto',formdata,{reportProgress:true}).pipe(catchError(err=>throwError(err)));
}
}

In this service,Subject plays an important role in making the progress bar implementation work.

ComponentA subscribes to the subject on load.

ngOnInit() {
this.serv.returnProgressObservable().subscribe((data)=>this.ratio=data);
}

The subject sends the progress ratio(which we shall see later)to its subscribers. ComponentA passes this ratio to the ProgressComponent to calculate the progress percentage.

reportProgress must be set to true to expose progress events.

In uploadPhoto() 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.

formdata.append(“uploads”,photo[i]);

In the above code, the first argument is the key. The 2nd argument is the value i.e an array of objects with the selected file details.

InterceptorService:

export class InterceptorService implements HttpInterceptor {constructor(private serv:ServiceA) { }intercept(request:HttpRequest<any>,next:HttpHandler):Observable<HttpEvent<any>>{return next.handle(request).pipe(tap(
event=>{
if(event.type==HttpEventType.DownloadProgress)
{
this.serv.returnProgress(event.loaded/event.total);
}
if(event.type==HttpEventType.UploadProgress)
{
this.serv.returnProgress(event.loaded/event.total);
}
}))}}

event.load is the no of bytes loaded.

event.total is the total no of bytes that needs to be loaded. This number is set from the value assigned to Content-Length header sent by the server. If the header isnt set/sent by the server,event.total shall be 0 or undefined.

event.load/event.total gives the progress ratio.

We are calling the returnProgress() on ServiceA to call next() on the Subject instance,passing the progress ratio as argument.

ComponentA which is subscribed to the subject receives this ratio and passes it to the ProgressComponent for using it in the progress bar.

Since we posting data to the server, the event will be a HttpEventType.UploadProgress.

ProgressComponent Template:

<div id=”progressContainer”>
<div #prog id=”progress”>
</div>
</div>
<p #percent></p>

ProgressComponent Class: ProgressComponent receives the progress ratio from Component via property binding.We are just multiplying this ratio by 100 to get the percentage. We are using this percentage to fix the width of the progress bar.

export class ProgressComponent implements OnInit {constructor() { }@Input() ratio:number=0;@ViewChild(‘prog’)prog:ElementRef;@ViewChild(‘percent’)percent:ElementRef;ngOnChanges(changes:SimpleChanges){this.prog.nativeElement.style.width=(changes.ratio.currentValue*100)+”%”;this.percent.nativeElement.innerHTML=(changes.ratio.currentValue*100)+”% loaded”;}ngOnInit() {}}

As you can see, we have 2 div tags. The outer div is just a container.We shall be modifying the width of the inner div. You could add a different background color to the inner div to show the user the change in the width.

#progressContainer{
width:100%;
background-color:rgb(243, 111, 111);
}
#progress{
width:0%;
background-color: green;
height:30px;
}

Finally lets get to Node server

//server.js
const express=require(‘express’);
const cors=require(‘cors’);const multer=require(‘multer’);//To enable file uploadconst app=express();app.use(cors());//To prevent CORS errorsconst photoUpload=multer({storage:multer.diskStorage({destination:function(req,file,fun)
{
fun(null,’./assets/images’); //set the destination path
},
filename:function(req,file,fun)
{
fun(null,file.originalname); //set the destination filename
}
})}).array(‘uploads’);
app.post(‘/uploadPhoto’,function(req,res)
{
photoUpload(req,res,function(err)
{
if (err instanceof multer.MulterError) {
console.log(err); //Multer error
} else if (err) {
console.log(err); //Some other error
}
})
res.send({message:”file uploaded”});
})
app.listen(8080,function(){
console.log(“app listening on port 8080”);
})

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,’./assets/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 let me know your comments:)

--

--

AngularEnthusiast

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