Skip to content

bobbyg603/file-uploads-with-angular-and-rxjs

Repository files navigation

πŸ“‚πŸš€β˜οΈ File Uploads with Angular and RxJS

Man Riding Rocket Ship In Space Holding Papers

medium profile link twitter profile link StackBlitz

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.

β˜•οΈ TL;DR

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.

File Uploads With Angular and RxJS

πŸ•΅οΈ Inspecting the Code

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:

app.component.ts

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.component.html

<app-files [files]="files$ | async"></app-files>

For file uploads, we start with a drag and drop control in the component template:

app.component.html

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

app.component.ts

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.component.html

<app-uploads [uploads]="uploads$ | async"></app-uploads>

πŸ§‘β€πŸŽ“ Further Exploration

Want to use this component in a production scenario? Take a look the upload-client repo.

Further Exploration Preview

The upload client repository takes this example and adds a Navbar, a modal, a loading wheel, error handling, and more!

πŸ§‘β€πŸ’» Next Steps

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 ❀️