File Uploads With Angular and RxJS
Add a powerful, elegant file upload control to your Angular application
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:
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:
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:
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:
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:
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:
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:
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.