{"id":25591397,"url":"https://github.com/bobbyg603/file-uploads-with-angular-and-rxjs","last_synced_at":"2025-07-02T14:08:08.217Z","repository":{"id":70064282,"uuid":"490426601","full_name":"bobbyg603/file-uploads-with-angular-and-rxjs","owner":"bobbyg603","description":"📂🚀☁️ Powerful, elegant file uploads for Angular applications with RxJS","archived":false,"fork":false,"pushed_at":"2023-08-11T01:07:18.000Z","size":533,"stargazers_count":6,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-12T23:14:33.084Z","etag":null,"topics":["angular","article","bindcallback","bobbyg603","mergemap","observable","observables","rxjs","scan","switchmap","tutorial"],"latest_commit_sha":null,"homepage":"https://betterprogramming.pub/file-uploads-with-angular-and-rxjs-34262b3450ae","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bobbyg603.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-05-09T19:59:11.000Z","updated_at":"2024-05-01T02:07:46.000Z","dependencies_parsed_at":"2025-02-21T09:51:24.642Z","dependency_job_id":"dd32384b-d60d-4f29-8c60-43f2f2c2c967","html_url":"https://github.com/bobbyg603/file-uploads-with-angular-and-rxjs","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bobbyg603%2Ffile-uploads-with-angular-and-rxjs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bobbyg603%2Ffile-uploads-with-angular-and-rxjs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bobbyg603%2Ffile-uploads-with-angular-and-rxjs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bobbyg603%2Ffile-uploads-with-angular-and-rxjs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bobbyg603","download_url":"https://codeload.github.com/bobbyg603/file-uploads-with-angular-and-rxjs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248643007,"owners_count":21138355,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","article","bindcallback","bobbyg603","mergemap","observable","observables","rxjs","scan","switchmap","tutorial"],"created_at":"2025-02-21T09:51:16.908Z","updated_at":"2025-04-12T23:14:38.319Z","avatar_url":"https://github.com/bobbyg603.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## 📂🚀☁️ File Uploads with Angular and RxJS\n\n\u003cimg alt=\"Man Riding Rocket Ship In Space Holding Papers\" src=\"https://github.com/bobbyg603/file-uploads-with-angular-and-rxjs/assets/2646053/b86e1530-78e0-4507-b72e-6b1a036570dd\" width=\"540px\" height=\"auto\"\u003e\n\n[![medium profile link](https://img.shields.io/badge/Medium-12100E?style=for-the-badge\u0026logo=medium\u0026logoColor=white)](https://medium.com/better-programming/file-uploads-with-angular-and-rxjs-34262b3450ae)\n[![twitter profile link](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge\u0026logo=twitter\u0026logoColor=white)](https://twitter.com/bobbyg603/status/1524465334522195968)\n[![StackBlitz](https://img.shields.io/badge/StackBlitz-Edit-blue?style=for-the-badge\u0026logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAABECAYAAAD+1gcLAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH5AINBw4X0bTGRQAABSxJREFUaN7VmVtsFFUYx//fmQW79bbd2QKpaIIaDcGoifFBEgMGqTTRRA01SgxE5Rbi7QG6S3lgo9J2twpeotxEQlCigLdoQwJ4ARN9QB9MRCNRDBdRzE7LJbTSmTl/H4BYStmd2Z3tDOdt5lzml/9833fO9x0gYi2xgom6Tt5aapyKEnRDlrVGPzfGT+G3SwZ87HLGT8f5uYD7jmSl99IAX80RfTY3A5wMqDVepoQPnqVKHtMbAN4PyJeFtPwafXBSknG9UoDHAIDQq7xODRU8mdc5Aeaeffy7O2F8GnnwZM5dKsCic88CrMU8sSMNbubdZwTIDnjlOoZa52eNYQc3c84sEK+d/1a6ji2UA5EFN3POw4C8fcYy/m+a3p1y2MGTOXsqIJsAxAZ1Hei53tgeSfBkBycK1McALrswJGIVHhE3cuD1ed4uorsAXD5Ed7/hqvXlrFtV8LpO3qKpdwJIDLn/AB/+s0SORgp8VJ43KK23AzAvNsagWlXu+lKV6LGc14itvyEwrsiwX6wWNQEijITiY9pYD1vvKAENAG+VC40hQlNlNt3Bq22lt4EYX2Jor6PVe5V8KzDFG7KsFXE/A3GHB/vcdHyx9IQPnuXI/ji3CuRuT+N1+U4ZHPhmGqk43yXY5C0ccE9hsfwQLjgp5n69hmCz9ylYGcRPrgg8ldfLIXjSx5RjNX3GB6GCm3m3ncDz/v4QNnjJ4KsGbubdVhAZ35YFtTaoKOY7jps5dwGIZf73aH7dnZa9QYH72vLNDmcmRNaX86eEnGvT2BoIdA0o3pV2HgRkS9C7bXnRDGlPypmd9r2AvB8FaAFetDJGvqTiyU7eJWeOp1cgfOo3rRbj6ZJRJdHB20TrrkhAAxutXvVsSedMtfEmGno3gNHhM8snVp80IytO0The18HraOgdkYCm7KyLy6MDoYdUfNQyjnZjeheAm8NXmt/FlDH16CI5dUHaN/DhypeZUqK/AkomAsMQ8fCjq41GKy0nim75ydd51UjX3QZgQgQccV/MUfcVSzYM4Mw1hnPa7QJkYgSgD2qqe6xWOVL8kLWaI3ptbgFkUgSgjwpUY09GDpY8ZJnH9UsExhPYH8CuVgtgTJlzC5pqipXxdpUSaF3FzLkdANJleOIJETWlkJbvh78glOVIM64PARjlc2afiGoqtMiuUMoTqRp3ehnQtpDNfqEDBdeC+T6nuELOLGRiXVVPJC5u2xwP6L0+1qOQ8wqZWNmpXECK6wV+RBCipRLoQBRvyLL2dFwfBlDnTWos7W4xXgi3IATg31p3hldoEG8EAR0IuEC8OuUGK62eCyoYVARutvNOL9VZQD6yxqmnKqmHB6u46PkejHp7XVxmlHOzVhXnTKxgwujXhzH0bdo56m9jymgcKhEITXFl61lFoYV7BMa0akCjkjqJEHOKdP/U7xhNJ1vlZLXOv2Upnmq3JxfJlH4XRzWebBWrmgf38hRXav5F4vSfjqGmHl8if1W/NuSzjWljvW3oQxh0Ly9AQRtqUvdC+Xk4UiXfpmLH9JzB0CBOQKtpwwXtHzxLJcTsQW97FdQDQVxIVc3GUzVuEyEDb4z7NTndysju4c6qfSlOOc8pXQof78nEtoVRDvDsnMlXeK04+o+ztRgSnNOdjq1DSM2z4uLoeecKSCQWhgntXfEsY2ZcHwDQAMESq8VoC7ty5EnxZK37EIAGAV6NArT3c3def2Hm3HdASlSYSipe384bAR6x+tTsIBOBqoMTzlirVz2BrOgoWcF/mizikfkwKiQAAAAASUVORK5CYII=)]([https://stackblitz.com/edit/github-ktbxdp-ftgv1g](https://stackblitz.com/github/bobbyg603/file-uploads-with-angular-and-rxjs))\n\nThis is is a companion repo for [File Uploads with Angular and RxJS](https://betterprogramming.pub/file-uploads-with-angular-and-rxjs-34262b3450ae) 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.\n\n## ☕️ TL;DR\n\nClone this repo to your workspace:\n\n```sh\ngit clone https://github.com/bobbyg603/file-uploads-with-angular-and-rxjs\n```\n\nInstall the dependencies and start the application:\n\n```sh\ncd file-uploads-with-angular-and-rxjs \u0026\u0026 npm i \u0026\u0026 npm start\n```\n\nYou will also need to clone the companion Express server:\n\n```sh\ngit clone https://github.com/bobbyg603/upload-server\n```\n\nInstall the dependencies and start the server:\n\n```sh\ncd upload-server \u0026\u0026 npm i \u0026\u0026 npm start\n```\n\nUse the drag and drop control or click \"Browse Files\" to select files to upload.\n\n\u003cimg alt=\"File Uploads With Angular and RxJS\" src=\"https://user-images.githubusercontent.com/2646053/167491922-66cc1460-1a59-4444-ab5c-7fd4d0dce2c9.gif\" width=\"540px\" height=\"auto\"\u003e\n\n## 🕵️ Inspecting the Code\n\nThis project requires a separate server for testing file uploads. Follow the instructions in the [bobbyg603/upload-server](https://github.com/bobbyg603/upload-server) repo to configure your system for testing file uploads.\n\n\u003e ℹ️ The upload server should only be run on your local system while you're actively testing.\n\nWe 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:\n\n[app.component.ts](https://github.com/bobbyg603/file-uploads-with-angular-and-rxjs/blob/c327f1c833d0de4d8b09d5a2a5012b8670b2c2d3/src/app/app.component.ts#L21-L26)\n```ts\nfiles$?: Observable\u003cFilesTableEntry[]\u003e;\n\nconstructor(private httpClient: HttpClient) {\n  this.files$ = this.getFilesSubject.asObservable()\n    .pipe(\n      switchMap(() =\u003e this.httpClient.get\u003cFilesTableEntry[]\u003e('http://localhost:8080/files'))\n    );\n}\n```\n\nThe `files$` property is an observable that emits the list of files and is subscribed to in the template via the AsyncPipe:\n\n[app.component.html](https://github.com/bobbyg603/file-uploads-with-angular-and-rxjs/blob/c327f1c833d0de4d8b09d5a2a5012b8670b2c2d3/src/app/app.component.html#L3)\n```html\n\u003capp-files [files]=\"files$ | async\"\u003e\u003c/app-files\u003e\n```\n\nFor file uploads, we start with a drag and drop control in the component template:\n\n[app.component.html](https://github.com/bobbyg603/file-uploads-with-angular-and-rxjs/blob/c327f1c833d0de4d8b09d5a2a5012b8670b2c2d3/src/app/app.component.html#L1)\n```html\n\u003capp-file-drop class=\"d-block my-4\" (filesDropped)=\"onFilesDropped($event)\"\u003e\u003c/app-file-drop\u003e\n```\n\nWe 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:\n\n[app.component.ts](https://github.com/bobbyg603/file-uploads-with-angular-and-rxjs/blob/c327f1c833d0de4d8b09d5a2a5012b8670b2c2d3/src/app/app.component.ts#L28-L70)\n```ts\nuploads$?: Observable\u003cFileUploadProgress[]\u003e;\n\nonFilesDropped(files: NgxFileDropEntry[]): void {\n  this.uploads$ = from(files)\n    .pipe(\n      mergeMap(selectedFile =\u003e {\n        const id = uuid();\n        const fileEntry = selectedFile.fileEntry as FileSystemFileEntry;\n        const observableFactory = bindCallback(fileEntry.file) as any;\n        const file$ = observableFactory.call(fileEntry) as Observable\u003cFile\u003e;\n        return file$\n          .pipe(\n            switchMap(file =\u003e this.uploadFile(file)\n              .pipe(\n                takeWhile(event =\u003e event.type !== HttpEventType.Response),\n                filter(isHttpProgressEvent),\n                map(event =\u003e {\n                  const name = file.name;\n                  const loaded = event.loaded ?? 0;\n                  const total = event.total ?? 1;\n                  const progress = Math.round(loaded / total * 100);\n                  const failed = false;\n                  const done = progress === 100;\n                  return {\n                    id,\n                    name,\n                    progress,\n                    failed,\n                    done\n                  };\n                }),\n              )\n            )\n          );\n      }),\n      scan((acc, next) =\u003e {\n        acc[next.id] = next;\n        return {\n          ...acc\n        };\n      }, {} as Record\u003cstring, FileUploadProgress\u003e),\n      map(progress =\u003e Object.values(progress)),\n      finalize(() =\u003e this.getFilesSubject.next(null))\n    );\n}\n```\n\nThe `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:\n\n[app.component.html](https://github.com/bobbyg603/file-uploads-with-angular-and-rxjs/blob/c327f1c833d0de4d8b09d5a2a5012b8670b2c2d3/src/app/app.component.html#L2)\n```html\n\u003capp-uploads [uploads]=\"uploads$ | async\"\u003e\u003c/app-uploads\u003e\n```\n\n## 🧑‍🎓 Further Exploration\n\nWant to use this component in a production scenario? Take a look the [upload-client](https://github.com/bobbyg603/upload-client) repo.\n\n\u003cimg alt=\"Further Exploration Preview\" src=\"https://user-images.githubusercontent.com/2646053/167490042-56670fb6-2779-4b67-8bfd-825a7a4e2a83.gif\" width=\"540px\" height=\"auto\"\u003e\n\nThe upload client repository takes this example and adds a Navbar, a modal, a loading wheel, error handling, and more!\n\n## 🧑‍💻 Next Steps\n\nIf you liked this example, please follow me on [Medium](https://bobbyg603.medium.com/) and [X](https://twitter.com/bobbyg603), where I post programming tutorials and discuss tips and tricks that have helped make me a better programmer.\n\n**Thank you for your support ❤️**\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbobbyg603%2Ffile-uploads-with-angular-and-rxjs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbobbyg603%2Ffile-uploads-with-angular-and-rxjs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbobbyg603%2Ffile-uploads-with-angular-and-rxjs/lists"}