File Uploads With Angular and RxJS

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

Bobby Galli
Better Programming

--

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

File Uploads 🧑‍💻

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.

Server ☁️

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

This is a REAL server and should not be left running when not in use!

Client 💻

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:

Files Component

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

Files Table Entry Interface

In files.component.html, add 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:

Adding HttpClient Module to App Module

Inject HttpClient into the constructor of app.component.ts. In the constructor, set files$ to the result of 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:

Subscribing with the Async Pipe

We use the AsyncPipe because it will automatically manage subscribing and unsubscribing to the files$ observable. If you find yourself calling subscribe on an observable in Angular be careful — forgetting to call unsubscribe can lead to a memory leak and degraded application performance!

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:

Adding NgxFileDrop Module to App Modules

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.ts, create 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

We use an Output here so that we can communicate from the FileDropComponent to AppComponent. Separation of concerns is fundamental software design principal that encourages clean, readable, and re-usable code.

Now that we‘ve created our Output in the FileDropComponent, add a handler for filesDropped events to your app.component.html template:

Adding the FileDrop Component to App Component

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:

Convert File Callback to Observable with BindCallback

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 mergeMap, this allows us to map each NgxFileDropEntry into a new observable without cancelling 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:

File Upload Snippet

Notice we are now mapping file$ to the result of httpClient.post and this time, flattening the observable stream and cancelling the inner subscription with switchMap. 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:

Accepting Only HttpProgress Events

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:

Filtered Upload Progress Events

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:

Take Until Upload Completes

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.

We’re want to 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. We’ll lean on the uuid package for this:

npm i uuid && npm i --save-dev @types/uuid

We’ll import uuid to app.component.ts using an alias so that it reads a little nicer:

Import V4 as UUID

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:

Upload Progress Accumulator

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:

Adding NgbProgressBar Module to App Module

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:

Upload Progress Bars Template

The UploadComponent uses *ngFor to create a div with the contained template for each upload in the uploads collection. When an upload fails, we add the text-danger class to the span that contains the upload’s name to color the text red. We add either a checkmark or an x next to the file name when the upload finishes depending on if it failed or not. Finally, we create a progress bar that is either red (danger), or green (success) and bind the [value] input to upload.progress so that the width of the progress bar is updated as the file uploads.

Now that we have completed the UploadsComponent let’s add it to our app.component.html template:

Adding Upload Progress to App Component

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:

Refresh Files Table

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:

Full App Component

Congratulations! 🎉

Thanks for following along! If you did everything correctly your application should looks 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.

--

--

Software developer at BugSplat. Big friendly giant, golf ball whacker guy, goofball, lover of tacos.