Contents
Angular File Input
Uploading Files with HttpClient
Calculate Upload Progress
Angular Material Progress Bar
Custom RxJS Upload Operator
Conclusion
Since my article on downloading files with Angular was well received, I've decided to also show how to apply the same pattern for uploads.
Uploading files is again a common interaction with web apps. Whether you want your user to upload documents in the PDF format, some archives as ZIP as well as a profile image or some kind of avatar in form of PNG or JPG - you'll need to implement a file upload and chances are that you also want to display some kind of progress indication.
If you're just here for the plain upload and would rather have a simple on/off loading indication, take a look at my post on implementing this with Angular and RxJS after the first two sections.
Here's a live example of the file upload dialog and progress bar which we're going to build. You can also find the code on GitHub.
Tip: You can generate a random big file with OS utilities:
# Ubuntu
shred -n 1 -s 1M big.pdf
# Mac OS X
mkfile -n 1M big.pdf
# Windows
fsutil file createnew big.pdf 1048576
Angular File Input
First, we need to enable the user to select a file to upload. For this, we use a regular <input>
element with type="file"
:
<!-- app.component.html -->
<input type="file" #fileInput (change)="onFileInput(fileInput.files)" />
// app.component.ts
@Component({
selector: 'ng-upload-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
file: File | null = null
onFileInput(files: FileList | null): void {
if (files) {
this.file = files.item(0)
}
}
}
It'll render as a button which opens up a file selection dialog. After a file has been selected, the filename will be displayed next to this button. Note that you may additionally specify a list of accepted file types through the accept
attribute in form of filename extensions or MIME types. You can also allow the selection of multiple files by setting the multiple
attribute to true
.
I've bound the input's change event to a component method while passing the input's files
attribute that contains a FileList
with one or more selected files. I've done this by assigning a template reference variable to the input as it works well with Angular's new strict mode. You might also use the implicit $event
variable in the event binding and retrieve the FileList
from the change event.
Unfortunately, it's pretty difficult to style file inputs and Angular Material also doesn't provide a corresponding component. Therefore you might want to hide the actual input element and have it triggered by a button next to it. Here's how that could look with Angular Material and the hidden
attribute:
<mat-label>File</mat-label>
<button mat-raised-button (click)="fileInput.click()">
{{ file ? file.name : 'Select' }}
</button>
<input hidden type="file" #fileInput (change)="onFileInput(fileInput.files)" />
Again, I'm using the template reference variable to forward the click for the button to the input element. Since the file is available from the component instance once selected, we can also use its name as the button text.
Uploading Files with HttpClient
Now that we can properly select a file, it's time implement the server upload. Of course, it's a prerequisite that you have a server (implemented with the language or framework of your choice) which can accept a file upload request. That means there's an HTTP POST endpoint for sending a body with the multipart/form-data
content-type. For our example I'm using a Node.js server with Express and the express-fileupload middleware. The server code looks like this:
import * as express from 'express'
import * as fileUpload from 'express-fileupload'
const app = express()
app.use(fileUpload())
app.post('/api/upload', (req, res) => {
console.log(`Successfully uploaded ${req.files.file.name}`)
res.sendStatus(200)
})
const server = app.listen(3333, () => {
console.log(`Listening at http://localhost:3333/api`)
})
I'm also configuring a proxy through the Angular CLI so that a request to the Angular development server at http://localhost:4200/api/upload
will be proxied to the Node.js backend server at http://localhost:3333/api/upload
.
We'll implement the actual HTTP request on the client-side in an Angular service that depends on the HttpClient
. There we have a method that accepts a file, encodes it into a FormData
body and sends it to the server:
// upload.service.ts
@Injectable({ providedIn: 'root' })
export class UploadService {
constructor(private http: HttpClient) {}
upload(file: File): Observable<void> {
const data = new FormData()
data.append('file', file)
return this.http.post('/api/upload', data)
}
}
Note that the field name 'file'
passed to append()
is arbitrary. It just needs to correspond with where the server will be looking for the file in the multipart body.
At this point we can add a submit button and method to our component, call the service and trigger the upload by subscribing to the returned observable:
<!-- app.component.html -->
<button
[disabled]="!file"
type="submit"
mat-raised-button
color="primary"
(click)="onSubmit()"
>
Submit
</button>
// app.component.ts
export class AppComponent implements OnDestroy {
file: File | null = null
private subscription: Subscription | undefined
constructor(private uploads: UploadService) {}
onFileInput(files: FileList | null): void {
if (files) {
this.file = files.item(0)
}
}
onSubmit() {
if (this.file) {
this.subscription = this.uploads.upload(this.file).subscribe()
}
}
ngOnDestroy() {
this.subscription?.unsubscribe()
}
}
Join my mailing list and follow me on Twitter @n_mehlhorn for more in-depth Angular & RxJS knowledge
Calculate Upload Progress
In order to calculate the upload progress we need to pass the reportProgress
and observe
options for our HTTP request while setting them to true
and event
respectively. This way, the HttpClient
returns and RxJS observable containing an HttpEvent
for each step in the upload request. By setting reportProgress
to true
this will also include events of type HttpProgressEvent
which provide information about the number of uploaded bytes as well as the total number of bytes in the file.
// upload.service.ts
import { HttpEvent } from '@angular/common/http'
const data = new FormData()
data.append('file', file)
const upload$: Observable<HttpEvent> = this.http.post('/api/upload', data, {
reportProgress: true,
observe: 'events',
})
Then we leverage the RxJS operator scan
which can accumulate state from each value emitted by an observable. The resulting observable will always emit the latest calculated state. Our upload state should look as follows:
export interface Upload {
progress: number
state: 'PENDING' | 'IN_PROGRESS' | 'DONE'
}
It has a progress
property ranging from 0
to 100
and state
property that tells us whether the underlying request is pending, currently in progress or done. Our initial state will start out accordingly:
const initialState: Upload = { state: 'PENDING', progress: 0 }
Now we can define how intermediate states are calculated from an existing state and an incoming HttpEvent
. But first, I'll setup some user-defined type guards for distinguishing different type of events. These guards are functions which narrow the event type based on the type
property that is available in every event:
import {
HttpEvent,
HttpEventType,
HttpResponse,
HttpProgressEvent,
} from '@angular/common/http'
function isHttpResponse<T>(event: HttpEvent<T>): event is HttpResponse<T> {
return event.type === HttpEventType.Response
}
function isHttpProgressEvent(
event: HttpEvent<unknown>
): event is HttpProgressEvent {
return (
event.type === HttpEventType.DownloadProgress ||
event.type === HttpEventType.UploadProgress
)
}
We can then use these guards in if-statements to safely access additional event properties for progress events. Here's the resulting function for calculating the state:
const calculateState = (upload: Upload, event: HttpEvent<unknown>): Upload => {
if (isHttpProgressEvent(event)) {
return {
progress: event.total
? Math.round((100 * event.loaded) / event.total)
: upload.progress,
state: 'IN_PROGRESS',
}
}
if (isHttpResponse(event)) {
return {
progress: 100,
state: 'DONE',
}
}
return upload
}
If an HttpProgressEvent
is emitted, we'll calculate the current progress and set the state property to 'IN_PROGRESS'
. We do this by returning a new Upload
state from our state calculation function while incorporating information from the incoming event. On the other hand, once the HTTP request is finished, as indicated by an HttpResponse
, we can set the progress
property to 100
and mark the upload as 'DONE'
. For all other events we'll keep (thus return) the state like it is.
Finally, we can pass our initialState
and the calculateState
function to the RxJS scan
operator and apply that to the observable returned from the HttpClient
:
// upload.service.ts
@Injectable({ providedIn: 'root' })
export class UploadService {
constructor(private http: HttpClient) {}
upload(file: File): Observable<Upload> {
const data = new FormData()
data.append('file', file)
const initialState: Upload = { state: 'PENDING', progress: 0 }
const calculateState = (
upload: Upload,
event: HttpEvent<unknown>
): Upload => {
// implementation
}
return this.http
.post('/api/upload', data)
.pipe(scan(calculateState, initialState))
}
}
Eventually, we get an observable that uploads our file while intermediately informing us about the upload state and thus progress.
Angular Material Progress Bar
We can use the Observable<Upload>
returned from the service in our component to display a progress bar. Simply assign the upload states to an instance property from inside the subscription callback (or use the AsyncPipe with NgIf):
// app.component.ts
export class AppComponent implements OnDestroy {
upload: Upload | undefined
onSubmit() {
if (this.file) {
this.subscription = this.uploads
.upload(this.file)
.subscribe((upload) => (this.upload = upload))
}
}
}
Then you can use this state information in the template to show something like the Progress Bar from Angular Material:
<!-- app.component.html -->
<mat-progress-bar
*ngIf="upload"
[mode]="upload.state == 'PENDING' ? 'buffer' : 'determinate'"
[value]="upload.progress"
>
</mat-progress-bar>
Custom RxJS Upload Operator
At this point everything should work just fine. However, if you'd like to re-use the progress logic in several places you could refactor it into a custom RxJS operator like this:
export function upload(): (
source: Observable<HttpEvent<unknown>>
) => Observable<Upload> {
const initialState: Upload = { state: 'PENDING', progress: 0 }
const calculateState = (
upload: Upload,
event: HttpEvent<unknown>
): Upload => {
// implementation
}
return (source) => source.pipe(scan(reduceState, initialState))
}
The upload
operator is also available in the ngx-operators 📚 library - a collection of battle-tested RxJS operators for Angular. I'd appreciate it if you'd give it a star ⭐️ on GitHub, this helps to let people know about it.
You'd use the operator like this:
this.http
.post('/api/upload', data, {
reportProgress: true,
observe: 'events',
})
.pipe(upload())
Conclusion
Uploading files is something that's required in many projects. With the presented solution we're able to implement it in a type-safe and re-usable way that works well with the Angular HttpClient and Angular Material. If anything is unclear, don't hesitate to post a comment below or ping me on Twitter @n_mehlhorn.