File Uploads With Angular and RxJS

File Uploads With Angular and RxJS

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

Ā·

10 min read

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 ā˜ļø

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:

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

In files.component.html, add a table that displays a collection of files:

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

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:

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!

Pass files$ as an input to FilesComponent using Angularā€™s AsyncPipe:

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

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:

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:

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

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

Add an onFilesDropped handler to your AppComponent:

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 mergeMap, this 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, flattening the observable stream and canceling 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 TypeScript that the type of the input to the next operator will be HttpUploadProgressEvent:

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

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:

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:

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 to search the collection for the correct index.

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

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:

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

ng g c uploads

Copy the following to uploads.component.ts:

Add the following snippet to uploads.component.html:

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:

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:

Congratulations! šŸŽ‰

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 Twitter, and GitHub, and subscribe to my YouTube channel.

Ā