File Uploads With Angular and RxJS | by Bobby Galli | May, 2022

Add a powerful, elegant file upload control to your Angular application

Magic Internet Person Delivering Files to the Cloud (Khakimullin Aleksandr)

Data transfer is a ubiquitous part of software applications. The File Upload control permeates the technology ecosystem from Apple to Zoom and is a component that many interact with daily. This tutorial demonstrates how to build a file upload client with RxJS, Angular, and Bootstrap.

Most Angular file upload tutorials subscribe to observables and manage subscriptions explicitly. In this article we will explore using, bindCallback, scan, mergeMap, takeWhile, and the AsyncPipe to create a fully-reactive upload control complete with progress bars and subscriptions that take care of themselves.

A companion repo for this article can be found here.

In order to develop our Angular file upload component we’ll need a backend that capable of handling file uploads, downloads, and returning a list of files that have been uploaded.

To get started, please clone the server repo:

git clone https://github.com/bobbyg603/upload-server.git

Install the package’s dependencies and start the server so that we have something we can use to develop our file upload component:

npm i && npm start

To begin, let’s create a new Angular application using the Angular CLI being sure to choose scss as your stylesheet format:

ng new uploads-client && cd uploads-client

We can leverage a few third-party libraries to greatly simplify the creation of a real-world file upload component. Let’s install Bootstrap, ng-bootstrap and ngx-file-drop. We’ll also install Bootstrap’s dependency @popperjs/core:

npm i bootstrap @popperjs/core @ng-bootstrap/ng-bootstrap @bugsplat/ngx-file-drop --legacy-peer-deps

Add the @angular/localize polyfill for Bootstrap by running the following terminal command:

ng add @angular/localize

Finally, import Bootstrap’s scss into your styles.scss file:

@import "~bootstrap/scss/bootstrap";

Files Table

The easiest place to start is to get the list of files from the server and display them in a table. Create a new files component to display our list of files:

ng g c files

Add a new instance of FilesComponent to your app.component.html template:

<app-files></app-files>

Let’s add an interface that represents the data we will want to display. Create a new file files/files-table-entry.ts:

export interface FilesTableEntry {
url: string,
name: string,
uploaded: Date,
size: number
}

In files.component.htmladd a table that displays a collection of files:

Files Component Table

To make the UI more interesting, let’s provide some placeholder data in files.component.ts:

Files Component Placeholder Data

List Uploaded Files

So far we’ve built a table for displaying files and populated it with some dummy data. Let’s display the real list of files by making a GET request to the /files endpoint on our server.

Add HttpClientModule to the imports array in app.module.ts:

import { HttpClientModule } from '@angular/common/http';@NgModule({
declarations: [
AppComponent,
FilesComponent
],
imports: [
BrowserModule,
HttpClientModule
],
bootstrap: [AppComponent]
})
export class AppModule { }

Inject HttpClient into the constructor of app.component.ts. In ngOnInit make a GET request to /files — be sure to start your Express server if it’s not already running!

GET Files in App Component

Pass files$ as an input to FilesComponent using Angular’s AsyncPipe:

<app-files [files]="files$ | async"></app-files>

If you did everything correctly your app should now look something like this:

Files Table Checkpoint

File Selection

Before we can upload files we need a way to allow the user to specify which files they would like to upload. To get started, create a new file-drop.component.ts component for selecting files:

ng g c file-drop

Our third-party NgxFileDropComponent allows our user to drag-and-drop files into our web app or specify files to upload via the system file picker. To use NgxFileDropComponent we first need to add NgxFileDropModule to our application’s app.module.ts:

import { NgxFileDropModule } from '@bugsplat/ngx-file-drop';@NgModule({
declarations: [
AppComponent,
FilesComponent,
UploadComponent
],
imports: [
BrowserModule,
HttpClientModule,
NgxFileDropModule
],
bootstrap: [AppComponent]
})
export class AppModule { }

Add ngx-file-drop and a basic ng-template to file-drop.component.html so that we can drag and drop files into our app or choose files via the system file picker:

File Drop Template

In file-drop.component.tscreate a onFilesDropped function that will serve as a handler for the onFileDrop event. Let’s also create a filesDropped output that we will use to relay the event to our AppComponent:

File Drop Component

Add the FileDropComponent and a handler for filesDropped events to your app.component.html template:

<app-file-drop class="d-block my-4" (filesDropped)="onFilesDropped($event)"></app-file-drop>
<app-files [files]="files$ | async"></app-files>

Add an onFilesDropped handler to your AppComponent:

OnFilesDropped Handler in App Component

At this point you should have built an application that resembles the following:

File Drop Checkpoint

Getting the File Object

Before we can start the file upload we’ll need to create an observable stream that emits each file from the NgxFileDropEntry array. Getting the File object from NgxFileDropEntry is a bit tricky because it’s passed as a parameter into a callback function.

Fortunately, RxJS has bindCallback which we can use to transform the function that takes a callback into an observable:

There’s a lot going on in the bindCallback snippet above.

First, the from operator is used to take an array of NgxFileDropEntry Items and emit them one by one allowing us to operate on each item individually.

Next, the items are piped into mergeMapthis allows us to map each NgxFileDropEntry into a new observable without canceling any previous inner subscriptions. Each NgxFileDropEntry will eventually map to an upload operation that emits multiple progress events over time.

When you have an observable you would like to map to another observable, switchMap is the operator of choice in most cases because it automatically cancels inner subscriptions. In this case, however, we want to maintain the inner subscriptions so they continue streaming progress for each file that is being uploaded. We’ll come back to this in a bit.

Finally, we use bindCallback to create an observable from a function that passes the result of an async operation to a callback. Unfortunately there’s a typing issue in TypeScript’s es5 lib that I don’t fully understand. To workaround the issue the result of bindCallback is cast to any. This works but feels a little dirty — if anyone knows a better solution here I would love to hear about it in the comments!

File Uploads

Now that we’ve transformed the File object from NgxFileDropEntry to an observable let’s use Angular’s HttpClient to upload the files and return progress events.

Here’s what our App component’s onFilesDropped function should look like:

Notice we are now mapping file$ to the result of httpClient.post and this time, canceling the inner subscription with switchMap. We cancel inner subscriptions here because HttpClient emits a bunch of progress events and all we care about is the value of the latest event.

We use reportProgress: true and observe: 'events' to indicate to HttpClient that we want it to emit progress values ​​as files are uploaded.

If everything is working correctly you should see several progress events logged to the developer console:

Upload Progress Events

The events we are most interested in have type HttpEventType.UploadProgress or type: 1.

Filtering Events

For now, let’s create a type-guard. The type guard will both allow us to filter out events that are not progress events, and indicate to type script that the type of the input to the next operator will be HttpUploadProgressEvent:

Type Guard for HttpUploadProgressEvents

Use the type guard to the filter operator so that other events are filtered out of the observable stream:

return file$
.pipe(
switchMap(file => this.uploadFile(file)
.pipe(
filter(isHttpProgressEvent)
)
);

Now you should only see type: 1 Events displayed in the developer console when you drag files into your application or select them via the system file picker:

Please note that there aren’t many upload events because you’re uploading to a server hosted on your local machine. You will see more upload events when the server is hosted across the internet.

Completing the Upload Observable Stream

Before we get too far ahead of ourselves, we need to make a small change to our observable stream to ensure that it gets finalized at the correct time. Remember how mergeMap requires us to manage our inner subscriptions? The RxJS docs recommend using one of the take operators to manage the completion of inner subscriptions.

When an upload operation is complete, HttpClient emits an event of type HttpEventType.Response or { type: 4 }. Let’s use the takeWhile operator to complete the subscription when the upload operation emits a response:

return file$
.pipe(
switchMap(file => this.uploadFile(file)
.pipe(
takeWhile(event => event.type !== HttpEventType.Response),
filter(isHttpProgressEvent)
)
);

You should now be able to upload files to your server — nice! In the final section we’ll add progress bars to the file uploads.

Upload Progress Accumulator

Now that we’re uploading files and getting a stream of upload events we need to massage these events into a collection we can work with in the UI:

File Upload Progress Interface

The scan operator is similar to the reduce but instead of manipulating arrays it will reduce values ​​emitted from an observable stream into an array or object.

We want to keep a running collection that maps each file to its most recent progress value. With large collections, it’s much faster to index into the collection using a string value that to search the collection for the correct index.

Let’s give each file a unique string for an id property that we can use to quickly index into our collection of files and update their associated progress:

const id = (Math.random() + 1).toString(36).substring(2);

We can use the loaded and total values ​​to generate our progress value. First, we’ll map each progress event to the FileUploadProgress interface. Next, we’ll use the scan operator with our id we defined in the ancestor function scope to save our progress values ​​to an accumulator. Finally, we’ll convert the accumulator to an array of values ​​so that it’s easy to display in the UI:

Phew! That was a lot, but we’re almost done.

Upload Progress Bars

We’re going to use the progress bars to display the progress of each upload to the user. To get started, add NgbProgressbarModule to app.module.ts:

import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap';@NgModule({
declarations: [
AppComponent,
FilesComponent,
UploadComponent
],
imports: [
BrowserModule,
HttpClientModule,
NgbProgressbarModule,
NgxFileDropModule
],
bootstrap: [AppComponent]
})
export class AppModule { }

Add an UploadsComponent so that we can display the upload progress bars:

ng g c uploads

Copy the following to uploads.component.ts:

Uploads Component

Add the following snippet to uploads.component.html:

Add the UploadsComponent to our app.component.html template:

<app-file-drop class="d-block my-4" (filesDropped)="onFilesDropped($event)"></app-file-drop>
<app-uploads [uploads]="uploads$ | async"></app-uploads>
<app-files [files]="files$ | async"></app-files>

Fantastic! If everything was wired up correctly you should see something like this:

Upload Progress Bars

Refreshing the List

The last piece of the puzzle is to fetch a new list of uploaded files when the upload has completed. This can be accomplished using a BehaviorSubject and the finalize operator.

A BehaviorSubject can be used in app.component.ts to dictate when the files$ observable is refreshed:

The finalize operator gets called when an observable stream completes. Let’s have getFilesSubject emit an event in the function that gets called by finalize so that the list of files gets refreshed when the upload is done:

App Component in Full

Thanks for following along! If you did everything correctly your application should look something like this:

File Upload with Progress and Refresh

In the future, I hope to release part 2 of this tutorial that explains how you can add a modal dialog, move the upload logic into a “smart” component, add files to an upload that is already in progress, and make the app look more professional.

Here’s a sneak peek of what a future tutorial might look like:

Future File Upload
Want to Connect?If you found the information in this tutorial useful please follow me on GitHub, and subscribe to my YouTube channel.

Leave a Comment