Show File Upload Progress With Multiple Files In Angular

Introduction

This article demonstrates the file upload with progress bar indicator showing how bytes or percentage has been completed.

Create Angular application

Let's create an angular application using angular cli.

We need to have node.js and angular cli installed on our pc to get started with angular application.

First we have to make sure that node.js is installed. You can download node.js from here.

In order to make sure that we have Angular CLI installed, open the command prompt and type the below command and press Enter:

npm install -g @angular/cli

Create a new project with choice of your name by entering name in command prompt like this:

ng new FileUploadProgress

Let's create a service for file upload. code of angular service looks like this:

import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient, HttpEventType } from '@angular/common/http';
import { map } from 'rxjs/operators';

@Injectable()
export class MediaService {
  fileSizeUnit: number = 1024;
  public isApiSetup = false;

  constructor(private http: HttpClient) {}

  getFileSize(fileSize: number): number {
    if (fileSize > 0) {
      if (fileSize < this.fileSizeUnit * this.fileSizeUnit) {
        fileSize = parseFloat((fileSize / this.fileSizeUnit).toFixed(2));
      } else if (
        fileSize <
        this.fileSizeUnit * this.fileSizeUnit * this.fileSizeUnit
      ) {
        fileSize = parseFloat(
          (fileSize / this.fileSizeUnit / this.fileSizeUnit).toFixed(2)
        );
      }
    }

    return fileSize;
  }

  getFileSizeUnit(fileSize: number) {
    let fileSizeInWords = 'bytes';

    if (fileSize > 0) {
      if (fileSize < this.fileSizeUnit) {
        fileSizeInWords = 'bytes';
      } else if (fileSize < this.fileSizeUnit * this.fileSizeUnit) {
        fileSizeInWords = 'KB';
      } else if (
        fileSize <
        this.fileSizeUnit * this.fileSizeUnit * this.fileSizeUnit
      ) {
        fileSizeInWords = 'MB';
      }
    }

    return fileSizeInWords;
  }

  uploadMedia(formData: any) {
    const headers = new HttpHeaders().set('Content-Type', 'application/json');

    return this.http
      .post(`http://yourapiurl`, formData, {
        headers,
        reportProgress: true,
        observe: 'events',
      })
      .pipe(
        map((event) => {
          switch (event.type) {
            case HttpEventType.UploadProgress:
              const progress = Math.round((100 * event.loaded) / event.total);
              return { status: 'progress', message: progress };

            case HttpEventType.Response:
              return event.body;
            default:
              return `Unhandled event: ${event.type}`;
          }
        })
      );
  }
}

Here, if you see we have used HttpClient to send POST request with multi-part form data to the server.

Please check uploadMedia method in the above code. HttpClient's post method's third parameter expects options object. The notable properties in our case are reportProgress and observe.

In order to get the progress reportProgress must be set to true and observe must be set to 'events'.

Here, inside map function of rxjs, we will check for the event type in swtich case statements. Rather than using integer values to compare, we can use  HttpEventType enum provided by the package @angular/common/http. In our case, only two enum values are useful such as UploadProgress and Response

In both cases, I have decided to use the common object having two properties such as status and message.

status can have two values such as 'progress' and 'completed', later one will be returned from the server which will be handled by case HttpEventType.Response.

Also, this service contains two other methods such as getFileSize and getFileSizeUnit. These methods will be used to show the file size and it's unit either bytes, KB or MB.

Let's see how to use this service into our component.

Let's create a new component and typescript code looks like this:

import { Component, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { MediaService } from '../media.service';

@Component({
  selector: 'app-media',
  templateUrl: './media.component.html',
  styleUrls: ['./media.component.css'],
})
export class MediaComponent implements OnInit {
  uploadedMedia: Array<any> = [];

  constructor(private mediaService: MediaService) {}

  ngOnInit() {}

  onFileBrowse(event: Event) {
    const target = event.target as HTMLInputElement;
    this.processFiles(target.files);
  }
  processFiles(files) {
    for (const file of files) {
      var reader = new FileReader();
      reader.readAsDataURL(file); // read file as data url
      reader.onload = (event: any) => {
        // called once readAsDataURL is completed

        this.uploadedMedia.push({
          FileName: file.name,
          FileSize:
            this.mediaService.getFileSize(file.size) +
            ' ' +
            this.mediaService.getFileSizeUnit(file.size),
          FileType: file.type,
          FileUrl: event.target.result,
          FileProgessSize: 0,
          FileProgress: 0,
          ngUnsubscribe: new Subject<any>(),
        });

        this.startProgress(file, this.uploadedMedia.length - 1);
      };
    }
  }

  async startProgress(file, index) {
    let filteredFile = this.uploadedMedia
      .filter((u, index) => index === index)
      .pop();

    if (filteredFile != null) {
      let fileSize = this.mediaService.getFileSize(file.size);
      let fileSizeInWords = this.mediaService.getFileSizeUnit(file.size);
      if (this.mediaService.isApiSetup) {
        let formData = new FormData();
        formData.append('File', file);

        this.mediaService
          .uploadMedia(formData)
          .pipe(takeUntil(file.ngUnsubscribe))
          .subscribe(
            (res: any) => {
              if (res.status === 'progress') {
                let completedPercentage = parseFloat(res.message);
                filteredFile.FileProgessSize = `${(
                  (fileSize * completedPercentage) /
                  100
                ).toFixed(2)} ${fileSizeInWords}`;
                filteredFile.FileProgress = completedPercentage;
              } else if (res.status === 'completed') {
                filteredFile.Id = res.Id;

                filteredFile.FileProgessSize = fileSize + ' ' + fileSizeInWords;
                filteredFile.FileProgress = 100;
              }
            },
            (error: any) => {
              console.log('file upload error');
              console.log(error);
            }
          );
      } else {
        for (
          var f = 0;
          f < fileSize + fileSize * 0.0001;
          f += fileSize * 0.01
        ) {
          filteredFile.FileProgessSize = f.toFixed(2) + ' ' + fileSizeInWords;
          var percentUploaded = Math.round((f / fileSize) * 100);
          filteredFile.FileProgress = percentUploaded;
          await this.fakeWaiter(Math.floor(Math.random() * 35) + 1);
        }
      }
    }
  }

  fakeWaiter(ms: number) {
    return new Promise((resolve) => {
      setTimeout(resolve, ms);
    });
  }

  removeImage(idx: number) {
    this.uploadedMedia = this.uploadedMedia.filter((u, index) => index !== idx);
  }
}

Now, add below code in HTML file of this component:

<h2>File upload with progress bar indicator</h2>
<input type="file" accept=".jpg,.jpeg,.png" (change)="onFileBrowse($event)" />
<div class="media-upload-table-container" *ngIf="uploadedMedia.length > 0">
  <table class="media-upload-table table table-borderless">
    <thead>
      <tr>
        <th style="width: 246px"></th>
        <th class="media-progress-bar"></th>
        <th style="width: 100px;"></th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let media of uploadedMedia; let i = index">
        <td>
          <div class="d-flex flex-row align-items-center">
            <!-- <div style="margin-right: 8px;">
                <img  class="add-media-img" src="{{media.FileUrl}}">
              </div> -->
            <div class="media-file-name">
              <span style="word-wrap: break-word; white-space: pre-line">
                {{ media.FileName }}
              </span>
            </div>
          </div>
        </td>
        <td style="vertical-align:middle;">
          <div class="d-flex flex-column" style="margin-top: 18px;">
            <div>
              <div class="first-progress">
                <div
                  [ngStyle]="{ 'width.%': media.FileProgress }"
                  class="second-progress"
                ></div>
              </div>
            </div>
            <div class="text-center">
              {{ media.FileProgessSize }} of {{ media.FileSize }}
            </div>
          </div>
        </td>
        <td style="vertical-align:middle;text-align: right;">
          <div class="media-upload-check">
            <span *ngIf="media.FileProgress === 100"> Completed</span>
          </div>
        </td>
        <td style="vertical-align:middle;">
          <a class="remove-media-txt" (click)="removeImage(i)"> Remove </a>
        </td>
      </tr>
    </tbody>
  </table>
</div>

If you see in above typescript code of the new component we have just created, there is a method startProgress. Here, we are using uploadedMedia array to keep track of the media uploaded by the user and it will show the progress of each uploaded file in the grid.

If you have noticed, our media service typescript code has one boolean flag isApiSetup. Here. We are using this flag to decide whether we want to upload files on actual server or not.

If you want to use the actual API of server then follow below steps,

  • Set value of isApiSetup to true and we will use the file upload on the server. here when 'progress' or 'completed' status received we will calculate file size and update it in the relevant object in the list.
  • Also, file object in the list has ngUnsubscribe property which is used in takeUntil operator of rxjs. So once, the user removes the file before 100% completion, we will send some value to it. So, file upload on the server will be canceled.

If you want to use the just fake waitor to simulate the working of file upload,

  • Set value of isApiSetup to false and we will use fake waiter to emulate the file upload rather than uploading the file on the server. It is useful if there is no api developed for file upload but still you want to check and modify and design of progress bar, percentage, file size etc. so, you don't have to rely on the api to test and modify the design.
  • In this case, fakeWaitor method will return the promise which will be resolved after a certain amount of time in parameter and this will be continuously done until for loop variable has reached to value of original file size.

Now, we need to use our component and service both in our application and for that we need to update app.module.ts file (which will be generated automatically by angular cli when new project is created).

Typescript code for app.module.ts looks like this,

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { MediaComponent } from './media/media.component';
import { MediaService } from './media.service';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [BrowserModule, FormsModule, HttpClientModule],
  declarations: [AppComponent, MediaComponent],
  bootstrap: [AppComponent],
  providers: [MediaService],
})
export class AppModule {}

That’s it. Fire the following command to see the charm!

ng serve

Now, open your browser and visit this URL: http://localhost:4200/ to see it in action.

Here are a few snapshots of this application:

If you want to see the demo of this application in action and check the full source of the application, you can find it here.

Conclusion

In this article, we have learned about how to upload multiple files using angular with file upload progress indicator for each file in the list.

Happy Coding!


Similar Articles