![Man Riding Rocket Ship In Space Holding Papers](https://private-user-images.githubusercontent.com/2646053/259877639-b86e1530-78e0-4507-b72e-6b1a036570dd.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjYwMjI2MTcsIm5iZiI6MTcyNjAyMjMxNywicGF0aCI6Ii8yNjQ2MDUzLzI1OTg3NzYzOS1iODZlMTUzMC03OGUwLTQ1MDctYjcyZS02YjFhMDM2NTcwZGQucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDkxMSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA5MTFUMDIzODM3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9NWNhNmUzYjFjMTk1NmVlMzc0ZjZiODBkYTAwNGJhMDgzZDE3N2I0NzMzNzNmOTg4ODdkNTMxNzMxOTFjNDVlOSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.UCiCMCkSxFd_0RIrplbwv7kzk2a7Wp74ahdDWFI7ZQs)
This is is a companion repo for File Uploads with Angular and RxJS that demonstrates how to build a drag and drop file upload control. Topics in this article include the uploading files with Angular HttpClient, using Bootstrap to display progress bars, and leveraging RxJS observables with subscriptions that complete automatically.
Clone this repo to your workspace:
git clone https://github.com/bobbyg603/file-uploads-with-angular-and-rxjs
Install the dependencies and start the application:
cd file-uploads-with-angular-and-rxjs && npm i && npm start
You will also need to clone the companion Express server:
git clone https://github.com/bobbyg603/upload-server
Install the dependencies and start the server:
cd upload-server && npm i && npm start
Use the drag and drop control or click "Browse Files" to select files to upload.
This project requires a separate server for testing file uploads. Follow the instructions in the bobbyg603/upload-server repo to configure your system for testing file uploads.
βΉοΈ The upload server should only be run on your local system while you're actively testing.
We use Angular's HttpClient to make a GET to our server so that we can display a list of files. The getFilesSubject
will emit an event that triggers another GET to the server:
files$?: Observable<FilesTableEntry[]>;
constructor(private httpClient: HttpClient) {
this.files$ = this.getFilesSubject.asObservable()
.pipe(
switchMap(() => this.httpClient.get<FilesTableEntry[]>('http://localhost:8080/files'))
);
}
The files$
property is an observable that emits the list of files and is subscribed to in the template via the AsyncPipe:
<app-files [files]="files$ | async"></app-files>
For file uploads, we start with a drag and drop control in the component template:
<app-file-drop class="d-block my-4" (filesDropped)="onFilesDropped($event)"></app-file-drop>
We handle the filesDropped
event in the component code. In the event handler we transform the NgxFileDropEntry
array into an observable array of type File
. For each file in the collection we call uploadFile
and take all of the progress events until we see HttpEventType.Response
which is the indication that the file has been uploaded successfully. We take all of the events and add them to a collection containing FileUploadProgress
objects that describe the current upload progress for each of the files. Finally, the function passed to the finalize
operator gets called and we instruct the getFilesSubject
to emit so that the table of uploaded files is refreshed:
uploads$?: Observable<FileUploadProgress[]>;
onFilesDropped(files: NgxFileDropEntry[]): void {
this.uploads$ = from(files)
.pipe(
mergeMap(selectedFile => {
const id = uuid();
const fileEntry = selectedFile.fileEntry as FileSystemFileEntry;
const observableFactory = bindCallback(fileEntry.file) as any;
const file$ = observableFactory.call(fileEntry) as Observable<File>;
return file$
.pipe(
switchMap(file => this.uploadFile(file)
.pipe(
takeWhile(event => event.type !== HttpEventType.Response),
filter(isHttpProgressEvent),
map(event => {
const name = file.name;
const loaded = event.loaded ?? 0;
const total = event.total ?? 1;
const progress = Math.round(loaded / total * 100);
const failed = false;
const done = progress === 100;
return {
id,
name,
progress,
failed,
done
};
}),
)
)
);
}),
scan((acc, next) => {
acc[next.id] = next;
return {
...acc
};
}, {} as Record<string, FileUploadProgress>),
map(progress => Object.values(progress)),
finalize(() => this.getFilesSubject.next(null))
);
}
The uploads$
property contains an observable collection of FileUploadProgress
objects and is in the template to create a progress bar for each of the files being uploaded:
<app-uploads [uploads]="uploads$ | async"></app-uploads>
Want to use this component in a production scenario? Take a look the upload-client repo.
The upload client repository takes this example and adds a Navbar, a modal, a loading wheel, error handling, and more!
If you liked this example, please follow me on Medium and X, where I post programming tutorials and discuss tips and tricks that have helped make me a better programmer.
Thank you for your support β€οΈ