├── .gitignore
├── ManagingFileUploadsWithNgRx.pdf
├── README.md
├── angular.json
├── apps
├── .gitkeep
├── api
│ ├── jest.config.js
│ ├── src
│ │ ├── app
│ │ │ ├── .gitkeep
│ │ │ ├── app.controller.ts
│ │ │ └── app.module.ts
│ │ ├── assets
│ │ │ └── .gitkeep
│ │ ├── environments
│ │ │ ├── environment.prod.ts
│ │ │ └── environment.ts
│ │ └── main.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.spec.json
│ └── tslint.json
├── app-e2e
│ ├── cypress.json
│ ├── src
│ │ ├── fixtures
│ │ │ └── example.json
│ │ ├── integration
│ │ │ └── app.spec.ts
│ │ ├── plugins
│ │ │ └── index.js
│ │ └── support
│ │ │ ├── app.po.ts
│ │ │ ├── commands.ts
│ │ │ └── index.ts
│ ├── tsconfig.e2e.json
│ ├── tsconfig.json
│ └── tslint.json
└── app
│ ├── browserslist
│ ├── jest.config.js
│ ├── proxy.conf.json
│ ├── src
│ ├── app
│ │ ├── app.component.css
│ │ ├── app.component.html
│ │ ├── app.component.spec.ts
│ │ ├── app.component.ts
│ │ └── app.module.ts
│ ├── assets
│ │ ├── .gitkeep
│ │ └── ngrx-badge.svg
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.css
│ └── test-setup.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.spec.json
│ └── tslint.json
├── jest.config.js
├── libs
├── .gitkeep
└── file-upload
│ ├── README.md
│ ├── jest.config.js
│ ├── src
│ ├── index.ts
│ ├── lib
│ │ ├── file-upload.module.spec.ts
│ │ ├── file-upload.module.ts
│ │ ├── file-upload
│ │ │ ├── file-upload.component.css
│ │ │ ├── file-upload.component.html
│ │ │ ├── file-upload.component.spec.ts
│ │ │ └── file-upload.component.ts
│ │ ├── models
│ │ │ ├── file-upload-model.ts
│ │ │ ├── file-upload-status.ts
│ │ │ ├── file-view-model.ts
│ │ │ └── index.ts
│ │ ├── pipes
│ │ │ ├── file-size.pipe.ts
│ │ │ └── index.ts
│ │ ├── services
│ │ │ ├── file-upload.service.spec.ts
│ │ │ ├── file-upload.service.ts
│ │ │ └── index.ts
│ │ └── state
│ │ │ ├── file-upload-api.actions.ts
│ │ │ ├── file-upload-ui.actions.ts
│ │ │ ├── file-upload.effects.ts
│ │ │ ├── file-upload.reducer.ts
│ │ │ ├── file-upload.selectors.ts
│ │ │ └── index.ts
│ └── test-setup.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ ├── tsconfig.spec.json
│ └── tslint.json
├── nx.json
├── package-lock.json
├── package.json
├── tools
├── schematics
│ └── .gitkeep
└── tsconfig.tools.json
├── tsconfig.json
└── tslint.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 | !.vscode/snippets.code-snippets
27 |
28 | # misc
29 | /.sass-cache
30 | /connect.lock
31 | /coverage
32 | /libpeerconnection.log
33 | npm-debug.log
34 | yarn-error.log
35 | testem.log
36 | /typings
37 |
38 | # System Files
39 | .DS_Store
40 | Thumbs.db
41 | node_modules
42 |
--------------------------------------------------------------------------------
/ManagingFileUploadsWithNgRx.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesleygrimes/managing-file-uploads-with-ngrx/9e344617546fab19e3b007205cf2f33c7607ab22/ManagingFileUploadsWithNgRx.pdf
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RealWorldApp
2 |
3 | Run the app: `npm run serve-with-api`
4 |
5 | Download the presentation: [ManagingFileUploadsWithNgRx.pdf](./ManagingFileUploadsWithNgRx.pdf)
6 |
7 | ## Further help
8 |
9 | Visit the [Nx Documentation](https://nx.dev/angular) to learn more.
10 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "projects": {
4 | "app": {
5 | "projectType": "application",
6 | "schematics": {},
7 | "root": "apps/app",
8 | "sourceRoot": "apps/app/src",
9 | "prefix": "app",
10 | "architect": {
11 | "build": {
12 | "builder": "@angular-devkit/build-angular:browser",
13 | "options": {
14 | "outputPath": "dist/apps/app",
15 | "index": "apps/app/src/index.html",
16 | "main": "apps/app/src/main.ts",
17 | "polyfills": "apps/app/src/polyfills.ts",
18 | "tsConfig": "apps/app/tsconfig.app.json",
19 | "aot": false,
20 | "assets": ["apps/app/src/favicon.ico", "apps/app/src/assets"],
21 | "styles": ["apps/app/src/styles.css"],
22 | "scripts": []
23 | },
24 | "configurations": {
25 | "production": {
26 | "fileReplacements": [
27 | {
28 | "replace": "apps/app/src/environments/environment.ts",
29 | "with": "apps/app/src/environments/environment.prod.ts"
30 | }
31 | ],
32 | "optimization": true,
33 | "outputHashing": "all",
34 | "sourceMap": false,
35 | "extractCss": true,
36 | "namedChunks": false,
37 | "aot": true,
38 | "extractLicenses": true,
39 | "vendorChunk": false,
40 | "buildOptimizer": true,
41 | "budgets": [
42 | {
43 | "type": "initial",
44 | "maximumWarning": "2mb",
45 | "maximumError": "5mb"
46 | },
47 | {
48 | "type": "anyComponentStyle",
49 | "maximumWarning": "6kb",
50 | "maximumError": "10kb"
51 | }
52 | ]
53 | }
54 | }
55 | },
56 | "serve": {
57 | "builder": "@angular-devkit/build-angular:dev-server",
58 | "options": {
59 | "browserTarget": "app:build",
60 | "proxyConfig": "apps/app/proxy.conf.json"
61 | },
62 | "configurations": {
63 | "production": {
64 | "browserTarget": "app:build:production"
65 | }
66 | }
67 | },
68 | "serve-with-api": {
69 | "builder": "@angular-devkit/architect:allOf",
70 | "options": {
71 | "targets": [
72 | {
73 | "target": "app:serve"
74 | },
75 | {
76 | "target": "api:serve"
77 | }
78 | ]
79 | }
80 | },
81 | "extract-i18n": {
82 | "builder": "@angular-devkit/build-angular:extract-i18n",
83 | "options": {
84 | "browserTarget": "app:build"
85 | }
86 | },
87 | "lint": {
88 | "builder": "@angular-devkit/build-angular:tslint",
89 | "options": {
90 | "tsConfig": [
91 | "apps/app/tsconfig.app.json",
92 | "apps/app/tsconfig.spec.json"
93 | ],
94 | "exclude": ["**/node_modules/**", "!apps/app/**"]
95 | }
96 | },
97 | "test": {
98 | "builder": "@nrwl/jest:jest",
99 | "options": {
100 | "jestConfig": "apps/app/jest.config.js",
101 | "tsConfig": "apps/app/tsconfig.spec.json",
102 | "setupFile": "apps/app/src/test-setup.ts"
103 | }
104 | }
105 | }
106 | },
107 | "app-e2e": {
108 | "root": "apps/app-e2e",
109 | "sourceRoot": "apps/app-e2e/src",
110 | "projectType": "application",
111 | "architect": {
112 | "e2e": {
113 | "builder": "@nrwl/cypress:cypress",
114 | "options": {
115 | "cypressConfig": "apps/app-e2e/cypress.json",
116 | "tsConfig": "apps/app-e2e/tsconfig.e2e.json",
117 | "devServerTarget": "app:serve"
118 | },
119 | "configurations": {
120 | "production": {
121 | "devServerTarget": "app:serve:production"
122 | }
123 | }
124 | },
125 | "lint": {
126 | "builder": "@angular-devkit/build-angular:tslint",
127 | "options": {
128 | "tsConfig": ["apps/app-e2e/tsconfig.e2e.json"],
129 | "exclude": ["**/node_modules/**", "!apps/app-e2e/**"]
130 | }
131 | }
132 | }
133 | },
134 | "api": {
135 | "root": "apps/api",
136 | "sourceRoot": "apps/api/src",
137 | "projectType": "application",
138 | "prefix": "api",
139 | "schematics": {},
140 | "architect": {
141 | "build": {
142 | "builder": "@nrwl/node:build",
143 | "options": {
144 | "outputPath": "dist/apps/api",
145 | "main": "apps/api/src/main.ts",
146 | "tsConfig": "apps/api/tsconfig.app.json",
147 | "assets": ["apps/api/src/assets"]
148 | },
149 | "configurations": {
150 | "production": {
151 | "optimization": true,
152 | "extractLicenses": true,
153 | "inspect": false,
154 | "fileReplacements": [
155 | {
156 | "replace": "apps/api/src/environments/environment.ts",
157 | "with": "apps/api/src/environments/environment.prod.ts"
158 | }
159 | ]
160 | }
161 | }
162 | },
163 | "serve": {
164 | "builder": "@nrwl/node:execute",
165 | "options": {
166 | "buildTarget": "api:build"
167 | }
168 | },
169 | "lint": {
170 | "builder": "@angular-devkit/build-angular:tslint",
171 | "options": {
172 | "tsConfig": [
173 | "apps/api/tsconfig.app.json",
174 | "apps/api/tsconfig.spec.json"
175 | ],
176 | "exclude": ["**/node_modules/**", "!apps/api/**"]
177 | }
178 | },
179 | "test": {
180 | "builder": "@nrwl/jest:jest",
181 | "options": {
182 | "jestConfig": "apps/api/jest.config.js",
183 | "tsConfig": "apps/api/tsconfig.spec.json"
184 | }
185 | }
186 | }
187 | },
188 | "file-upload": {
189 | "projectType": "library",
190 | "root": "libs/file-upload",
191 | "sourceRoot": "libs/file-upload/src",
192 | "prefix": "app",
193 | "architect": {
194 | "lint": {
195 | "builder": "@angular-devkit/build-angular:tslint",
196 | "options": {
197 | "tsConfig": [
198 | "libs/file-upload/tsconfig.lib.json",
199 | "libs/file-upload/tsconfig.spec.json"
200 | ],
201 | "exclude": ["**/node_modules/**", "!libs/file-upload/**"]
202 | }
203 | },
204 | "test": {
205 | "builder": "@nrwl/jest:jest",
206 | "options": {
207 | "jestConfig": "libs/file-upload/jest.config.js",
208 | "tsConfig": "libs/file-upload/tsconfig.spec.json",
209 | "setupFile": "libs/file-upload/src/test-setup.ts"
210 | }
211 | }
212 | },
213 | "schematics": {}
214 | }
215 | },
216 | "cli": {
217 | "defaultCollection": "@nrwl/angular"
218 | },
219 | "schematics": {
220 | "@nrwl/angular:application": {
221 | "unitTestRunner": "jest",
222 | "e2eTestRunner": "cypress"
223 | },
224 | "@nrwl/angular:library": {
225 | "unitTestRunner": "jest"
226 | }
227 | },
228 | "defaultProject": "app"
229 | }
230 |
--------------------------------------------------------------------------------
/apps/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/api/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'api',
3 | preset: '../../jest.config.js',
4 | coverageDirectory: '../../coverage/apps/api'
5 | };
6 |
--------------------------------------------------------------------------------
/apps/api/src/app/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesleygrimes/managing-file-uploads-with-ngrx/9e344617546fab19e3b007205cf2f33c7607ab22/apps/api/src/app/.gitkeep
--------------------------------------------------------------------------------
/apps/api/src/app/app.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BadRequestException,
3 | Controller,
4 | HttpCode,
5 | Post,
6 | UploadedFile,
7 | UseInterceptors
8 | } from '@nestjs/common';
9 | import { FileInterceptor } from '@nestjs/platform-express';
10 | import { writeFileSync } from 'fs';
11 |
12 | @Controller()
13 | export class AppController {
14 | private UPLOAD_PATH = './tmp';
15 | constructor() {}
16 |
17 | @Post('upload')
18 | @HttpCode(200)
19 | @UseInterceptors(FileInterceptor('file'))
20 | async uploadFile(@UploadedFile()
21 | file: {
22 | originalname: string;
23 | buffer: Buffer;
24 | }) {
25 | try {
26 | writeFileSync(`${this.UPLOAD_PATH}/${file.originalname}`, file.buffer);
27 | } catch (error) {
28 | throw new BadRequestException(`Failed to upload file. ${error}`);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/apps/api/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | @Module({
4 | controllers: [AppController]
5 | })
6 | export class AppModule {}
7 |
--------------------------------------------------------------------------------
/apps/api/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesleygrimes/managing-file-uploads-with-ngrx/9e344617546fab19e3b007205cf2f33c7607ab22/apps/api/src/assets/.gitkeep
--------------------------------------------------------------------------------
/apps/api/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/apps/api/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: false
3 | };
4 |
--------------------------------------------------------------------------------
/apps/api/src/main.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This is not a production server yet!
3 | * This is only a minimal backend to get started.
4 | **/
5 |
6 | import { NestFactory } from '@nestjs/core';
7 | import * as bodyParser from 'body-parser';
8 | import { AppModule } from './app/app.module';
9 |
10 | async function bootstrap() {
11 | const app = await NestFactory.create(AppModule);
12 | const globalPrefix = 'api';
13 | app.setGlobalPrefix(globalPrefix);
14 | app.use(bodyParser.json({ limit: '50mb' }));
15 | app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
16 | const port = process.env.port || 3333;
17 | await app.listen(port, () => {
18 | console.log('Listening at http://localhost:' + port + '/' + globalPrefix);
19 | });
20 | }
21 |
22 | bootstrap();
23 |
--------------------------------------------------------------------------------
/apps/api/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": ["node"]
6 | },
7 | "exclude": ["**/*.spec.ts"],
8 | "include": ["**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/apps/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "types": ["node", "jest"]
5 | },
6 | "include": ["**/*.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/apps/api/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["**/*.spec.ts", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/apps/api/tslint.json:
--------------------------------------------------------------------------------
1 | { "extends": "../../tslint.json", "rules": [] }
2 |
--------------------------------------------------------------------------------
/apps/app-e2e/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileServerFolder": ".",
3 | "fixturesFolder": "./src/fixtures",
4 | "integrationFolder": "./src/integration",
5 | "pluginsFile": "./src/plugins/index",
6 | "supportFile": false,
7 | "video": true,
8 | "videosFolder": "../../dist/cypress/apps/app-e2e/videos",
9 | "screenshotsFolder": "../../dist/cypress/apps/app-e2e/screenshots",
10 | "chromeWebSecurity": false
11 | }
12 |
--------------------------------------------------------------------------------
/apps/app-e2e/src/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io"
4 | }
5 |
--------------------------------------------------------------------------------
/apps/app-e2e/src/integration/app.spec.ts:
--------------------------------------------------------------------------------
1 | import { getGreeting } from '../support/app.po';
2 |
3 | describe('app', () => {
4 | beforeEach(() => cy.visit('/'));
5 |
6 | it('should display welcome message', () => {
7 | getGreeting().contains('Welcome to app!');
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/apps/app-e2e/src/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor');
15 |
16 | module.exports = (on, config) => {
17 | // `on` is used to hook into various events Cypress emits
18 | // `config` is the resolved Cypress config
19 |
20 | // Preprocess Typescript
21 | on('file:preprocessor', preprocessTypescript(config));
22 | };
23 |
--------------------------------------------------------------------------------
/apps/app-e2e/src/support/app.po.ts:
--------------------------------------------------------------------------------
1 | export const getGreeting = () => cy.get('h1');
2 |
--------------------------------------------------------------------------------
/apps/app-e2e/src/support/commands.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/apps/app-e2e/src/support/index.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
--------------------------------------------------------------------------------
/apps/app-e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "sourceMap": false,
5 | "outDir": "../../dist/out-tsc"
6 | },
7 | "include": ["src/**/*.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/app-e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "types": ["cypress", "node"]
5 | },
6 | "include": ["**/*.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/apps/app-e2e/tslint.json:
--------------------------------------------------------------------------------
1 | { "extends": "../../tslint.json", "rules": [] }
2 |
--------------------------------------------------------------------------------
/apps/app/browserslist:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | > 0.5%
9 | last 2 versions
10 | Firefox ESR
11 | not dead
12 | not IE 9-11 # For IE 9-11 support, remove 'not'.
--------------------------------------------------------------------------------
/apps/app/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'app',
3 | preset: '../../jest.config.js',
4 | coverageDirectory: '../../coverage/apps/app',
5 | snapshotSerializers: [
6 | 'jest-preset-angular/AngularSnapshotSerializer.js',
7 | 'jest-preset-angular/HTMLCommentSerializer.js'
8 | ]
9 | };
10 |
--------------------------------------------------------------------------------
/apps/app/proxy.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api": {
3 | "target": "http://localhost:3333",
4 | "secure": false
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/apps/app/src/app/app.component.css:
--------------------------------------------------------------------------------
1 | .container-fluid {
2 | margin-top: 15px;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/app/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/apps/app/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { TestBed, async } from '@angular/core/testing';
3 | import { HttpClientModule } from '@angular/common/http';
4 | import { AppComponent } from './app.component';
5 |
6 | describe('AppComponent', () => {
7 | beforeEach(async(() => {
8 | TestBed.configureTestingModule({
9 | declarations: [AppComponent],
10 | imports: [HttpClientModule]
11 | }).compileComponents();
12 | }));
13 |
14 | it('should create the app', () => {
15 | const fixture = TestBed.createComponent(AppComponent);
16 | const app = fixture.debugElement.componentInstance;
17 | expect(app).toBeTruthy();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/apps/app/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.css']
7 | })
8 | export class AppComponent {}
9 |
--------------------------------------------------------------------------------
/apps/app/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { HttpClientModule } from '@angular/common/http';
2 | import { NgModule } from '@angular/core';
3 | import { BrowserModule } from '@angular/platform-browser';
4 | import { FileUploadModule } from '@app/file-upload';
5 | import { EffectsModule } from '@ngrx/effects';
6 | import { StoreModule } from '@ngrx/store';
7 | import { StoreDevtoolsModule } from '@ngrx/store-devtools';
8 | import { AppComponent } from './app.component';
9 |
10 | @NgModule({
11 | declarations: [AppComponent],
12 | imports: [
13 | BrowserModule,
14 | HttpClientModule,
15 | StoreModule.forRoot({}),
16 | EffectsModule.forRoot([]),
17 | StoreDevtoolsModule.instrument(),
18 | FileUploadModule
19 | ],
20 | bootstrap: [AppComponent]
21 | })
22 | export class AppModule {}
23 |
--------------------------------------------------------------------------------
/apps/app/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesleygrimes/managing-file-uploads-with-ngrx/9e344617546fab19e3b007205cf2f33c7607ab22/apps/app/src/assets/.gitkeep
--------------------------------------------------------------------------------
/apps/app/src/assets/ngrx-badge.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/app/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/apps/app/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/apps/app/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesleygrimes/managing-file-uploads-with-ngrx/9e344617546fab19e3b007205cf2f33c7607ab22/apps/app/src/favicon.ico
--------------------------------------------------------------------------------
/apps/app/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Managing File Uploads w/ NgRx
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/apps/app/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic()
12 | .bootstrapModule(AppModule)
13 | .catch(err => console.error(err));
14 |
--------------------------------------------------------------------------------
/apps/app/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
22 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
23 |
24 | /**
25 | * Web Animations `@angular/platform-browser/animations`
26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
28 | */
29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
30 |
31 | /**
32 | * By default, zone.js will patch all possible macroTask and DomEvents
33 | * user can disable parts of macroTask/DomEvents patch by setting following flags
34 | * because those flags need to be set before `zone.js` being loaded, and webpack
35 | * will put import in the top of bundle, so user need to create a separate file
36 | * in this directory (for example: zone-flags.ts), and put the following flags
37 | * into that file, and then add the following code before importing zone.js.
38 | * import './zone-flags.ts';
39 | *
40 | * The flags allowed in zone-flags.ts are listed here.
41 | *
42 | * The following flags will work for all browsers.
43 | *
44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
47 | *
48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
50 | *
51 | * (window as any).__Zone_enable_cross_context_check = true;
52 | *
53 | */
54 |
55 | /***************************************************************************************************
56 | * Zone JS is required by default for Angular itself.
57 | */
58 | import 'zone.js/dist/zone'; // Included with Angular CLI.
59 |
60 | /***************************************************************************************************
61 | * APPLICATION IMPORTS
62 | */
63 |
--------------------------------------------------------------------------------
/apps/app/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 | @import '~bootstrap/dist/css/bootstrap.min.css';
3 |
--------------------------------------------------------------------------------
/apps/app/src/test-setup.ts:
--------------------------------------------------------------------------------
1 | import 'jest-preset-angular';
2 |
--------------------------------------------------------------------------------
/apps/app/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": []
6 | },
7 | "files": ["src/main.ts", "src/polyfills.ts"],
8 | "include": ["**/*.ts"],
9 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "types": ["node", "jest"]
5 | },
6 | "include": ["**/*.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/apps/app/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "files": ["src/test-setup.ts"],
9 | "include": ["**/*.spec.ts", "**/*.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/app/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json",
3 | "rules": {
4 | "directive-selector": [true, "attribute", "realWorldApp", "camelCase"],
5 | "component-selector": [true, "element", "app", "kebab-case"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
3 | transform: {
4 | '^.+\\.(ts|js|html)$': 'ts-jest'
5 | },
6 | resolver: '@nrwl/jest/plugins/resolver',
7 | moduleFileExtensions: ['ts', 'js', 'html'],
8 | coverageReporters: ['html'],
9 | passWithNoTests: true
10 | };
11 |
--------------------------------------------------------------------------------
/libs/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesleygrimes/managing-file-uploads-with-ngrx/9e344617546fab19e3b007205cf2f33c7607ab22/libs/.gitkeep
--------------------------------------------------------------------------------
/libs/file-upload/README.md:
--------------------------------------------------------------------------------
1 | # file-upload
2 |
3 | This library was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test file-upload` to execute the unit tests.
8 |
--------------------------------------------------------------------------------
/libs/file-upload/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'file-upload',
3 | preset: '../../jest.config.js',
4 | coverageDirectory: '../../coverage/libs/file-upload',
5 | snapshotSerializers: [
6 | 'jest-preset-angular/AngularSnapshotSerializer.js',
7 | 'jest-preset-angular/HTMLCommentSerializer.js'
8 | ]
9 | };
10 |
--------------------------------------------------------------------------------
/libs/file-upload/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/file-upload.module';
2 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/file-upload.module.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, TestBed } from '@angular/core/testing';
2 | import { FileUploadModule } from './file-upload.module';
3 |
4 | describe('FileUploadModule', () => {
5 | beforeEach(async(() => {
6 | TestBed.configureTestingModule({
7 | imports: [FileUploadModule]
8 | }).compileComponents();
9 | }));
10 |
11 | it('should create', () => {
12 | expect(FileUploadModule).toBeDefined();
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/file-upload.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
4 | import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap';
5 | import { EffectsModule } from '@ngrx/effects';
6 | import { StoreModule } from '@ngrx/store';
7 | import { FileUploadComponent } from './file-upload/file-upload.component';
8 | import { FileSizePipe } from './pipes';
9 | import { FileUploadEffects, fileUploadFeatureKey, reducer } from './state';
10 |
11 | @NgModule({
12 | imports: [
13 | CommonModule,
14 | FontAwesomeModule,
15 | NgbProgressbarModule,
16 | StoreModule.forFeature(fileUploadFeatureKey, reducer),
17 | EffectsModule.forFeature([FileUploadEffects])
18 | ],
19 | declarations: [FileUploadComponent, FileSizePipe],
20 | exports: [FileUploadComponent]
21 | })
22 | export class FileUploadModule {}
23 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/file-upload/file-upload.component.css:
--------------------------------------------------------------------------------
1 | /* Container */
2 | .file-upload {
3 | margin-top: 10px;
4 | }
5 |
6 | /* Buttons */
7 | .upload-files,
8 | .clear-files,
9 | .cancel-upload {
10 | margin-left: 10px;
11 | }
12 |
13 | .upload-files > label,
14 | .browse-files > label,
15 | .clear-files > label,
16 | .cancel-upload > label {
17 | margin-bottom: 0;
18 | }
19 |
20 | /* File Status */
21 | .red {
22 | color: red;
23 | }
24 |
25 | .black {
26 | color: black;
27 | }
28 |
29 | .green {
30 | color: green;
31 | }
32 |
33 | .file-upload-error {
34 | margin-left: 5px;
35 | color: red;
36 | }
37 |
38 | /* Other */
39 | .file-input {
40 | display: none;
41 | }
42 |
43 | /* File Upload Table */
44 | table.file-list {
45 | color: #333;
46 | margin-top: 0;
47 | margin-bottom: 20px;
48 | width: 100%;
49 | border-collapse: collapse;
50 | border-spacing: 0;
51 | }
52 |
53 | td,
54 | th {
55 | border: 1px solid transparent;
56 | height: 30px;
57 | transition: all 0.3s;
58 | }
59 |
60 | tr:nth-child(even) td {
61 | background: #f1f1f1;
62 | border-bottom: 1px solid #333;
63 | }
64 |
65 | tr:nth-child(odd) td {
66 | background: #fefefe;
67 | border-bottom: 1px solid #333;
68 | }
69 |
70 | .file-name {
71 | width: 40%;
72 | }
73 |
74 | .file-size {
75 | width: 100px;
76 | }
77 |
78 | .file-action {
79 | width: 100px;
80 | text-align: center;
81 | }
82 |
83 | .file-action-buttons {
84 | display: grid;
85 | grid-template-columns: 1fr 1fr;
86 | }
87 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/file-upload/file-upload.component.html:
--------------------------------------------------------------------------------
1 |
2 |
34 |
35 |
36 |
0; else noFilesStaged" class="table file-list">
37 |
38 |
39 | File Name |
40 | File Size |
41 | Upload Status |
42 | |
43 |
44 |
45 |
46 |
47 | {{ file.fileName }} |
48 | {{ file.formattedFileSize }} |
49 |
50 |
51 | {{ file.progress }}%
62 | {{
63 | file.errorMessage
64 | }}
65 |
66 | |
67 |
68 |
69 |
74 |
75 | |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | No files chosen |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/file-upload/file-upload.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { FileUploadComponent } from './file-upload.component';
4 |
5 | describe('FileUploadComponent', () => {
6 | let component: FileUploadComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ FileUploadComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(FileUploadComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/file-upload/file-upload.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { faUndo } from '@fortawesome/free-solid-svg-icons';
3 | import { Store } from '@ngrx/store';
4 | import { FileUploadSelectors, FileUploadUIActions } from '../state';
5 |
6 | @Component({
7 | selector: 'app-file-upload',
8 | templateUrl: './file-upload.component.html',
9 | styleUrls: ['./file-upload.component.css']
10 | })
11 | export class FileUploadComponent {
12 | filesInQueue$ = this.store.select(FileUploadSelectors.selectFileUploadQueue);
13 |
14 | faUndo = faUndo;
15 |
16 | constructor(private store: Store<{}>) {}
17 |
18 | addFiles(event) {
19 | const files: File[] = event.target.files ? [...event.target.files] : [];
20 |
21 | files.forEach(file =>
22 | this.store.dispatch(FileUploadUIActions.added({ file }))
23 | );
24 |
25 | event.target.value = '';
26 | }
27 |
28 | requestRetry(id: string) {
29 | this.store.dispatch(FileUploadUIActions.retryRequested({ id }));
30 | }
31 |
32 | requestCancel() {
33 | this.store.dispatch(FileUploadUIActions.cancelRequested());
34 | }
35 |
36 | requestProcess() {
37 | this.store.dispatch(FileUploadUIActions.processRequested());
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/models/file-upload-model.ts:
--------------------------------------------------------------------------------
1 | import { FileUploadStatus } from './file-upload-status';
2 | export interface FileUploadModel {
3 | id: string;
4 | fileName: string;
5 | fileSize: number;
6 | rawFile: File;
7 | progress: number;
8 | status: FileUploadStatus;
9 | error: string;
10 | }
11 |
12 | export interface FileUploadState {
13 | ids: string[];
14 | entities: { [id: string]: FileUploadModel };
15 | }
16 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/models/file-upload-status.ts:
--------------------------------------------------------------------------------
1 | export enum FileUploadStatus {
2 | Ready = 'Ready',
3 | Requested = 'Requested',
4 | Started = 'Started',
5 | InProgress = 'InProgress',
6 | Completed = 'Completed',
7 | Failed = 'Failed',
8 | Canceled = 'Canceled'
9 | }
10 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/models/file-view-model.ts:
--------------------------------------------------------------------------------
1 | import { IconDefinition } from '@fortawesome/free-solid-svg-icons';
2 |
3 | export interface FileViewModel {
4 | id: string;
5 | fileName: string;
6 | formattedFileSize: string;
7 | canRetry: boolean;
8 | canDelete: boolean;
9 | statusIcon: IconDefinition;
10 | statusColorClass: 'red' | 'green' | 'black';
11 | showProgress: boolean;
12 | progress: number;
13 | errorMessage: string;
14 | }
15 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './file-upload-model';
2 | export * from './file-upload-status';
3 | export * from './file-view-model';
4 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/pipes/file-size.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'fileSize'
5 | })
6 | export class FileSizePipe implements PipeTransform {
7 | transform(value: any, args?: any): any {
8 | if (value && !isNaN(value)) {
9 | return `${value / 1024}`;
10 | }
11 | return value;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/pipes/index.ts:
--------------------------------------------------------------------------------
1 | export * from './file-size.pipe';
2 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/services/file-upload.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { FileUploadService } from './file-upload.service';
4 |
5 | describe('FileUploadService', () => {
6 | beforeEach(() => TestBed.configureTestingModule({}));
7 |
8 | it('should be created', () => {
9 | const service: FileUploadService = TestBed.get(FileUploadService);
10 | expect(service).toBeTruthy();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/services/file-upload.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient, HttpRequest } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 |
4 | @Injectable({
5 | providedIn: 'root'
6 | })
7 | export class FileUploadService {
8 | private _apiBaseUrl = '/api';
9 | constructor(private http: HttpClient) {}
10 |
11 | uploadFile(file: File) {
12 | const formData = new FormData();
13 | formData.append('file', file);
14 |
15 | const httpOptions = {
16 | reportProgress: true
17 | };
18 |
19 | const req = new HttpRequest(
20 | 'POST',
21 | `${this._apiBaseUrl}/upload`,
22 | formData,
23 | httpOptions
24 | );
25 |
26 | return this.http.request(req);
27 | }
28 |
29 | uploadFileError(file: File) {
30 | const formData = new FormData();
31 | formData.append('file', file);
32 |
33 | const httpOptions = {
34 | reportProgress: true
35 | };
36 |
37 | const req = new HttpRequest('POST', ``, formData, httpOptions);
38 |
39 | return this.http.request(req);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './file-upload.service';
2 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/state/file-upload-api.actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction, props } from '@ngrx/store';
2 | import { FileUploadModel } from '../models';
3 |
4 | export const uploadRequested = createAction(
5 | '[Upload Effect] Upload Requested',
6 | props<{ fileToUpload: FileUploadModel }>()
7 | );
8 | export const uploadFailed = createAction(
9 | '[Upload API] Upload Failed',
10 | props<{ id: string; error: string }>()
11 | );
12 | export const uploadStarted = createAction(
13 | '[Upload API] Upload Started',
14 | props<{ id: string }>()
15 | );
16 | export const uploadProgressed = createAction(
17 | '[Upload API] Upload Progressed',
18 | props<{ id: string; progress: number }>()
19 | );
20 | export const uploadCompleted = createAction(
21 | '[Upload API] Upload Completed',
22 | props<{ id: string }>()
23 | );
24 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/state/file-upload-ui.actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction, props } from '@ngrx/store';
2 |
3 | export const added = createAction(
4 | '[Upload Form] Added',
5 | props<{ file: File }>()
6 | );
7 |
8 | export const removed = createAction(
9 | '[Upload Form] Removed',
10 | props<{ id: string }>()
11 | );
12 |
13 | export const processRequested = createAction('[Upload Form] Process Requested');
14 |
15 | export const cancelRequested = createAction('[Upload Form] Cancel Requested');
16 |
17 | export const retryRequested = createAction(
18 | '[Upload Form] Retry Requested',
19 | props<{ id: string }>()
20 | );
21 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/state/file-upload.effects.ts:
--------------------------------------------------------------------------------
1 | import { HttpEvent, HttpEventType } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 | import { Actions, createEffect, ofType } from '@ngrx/effects';
4 | import { Store } from '@ngrx/store';
5 | import { of } from 'rxjs';
6 | import {
7 | catchError,
8 | map,
9 | mergeMap,
10 | switchMap,
11 | takeUntil,
12 | withLatestFrom
13 | } from 'rxjs/operators';
14 | import { FileUploadService } from '../services';
15 | import * as FileUploadAPIActions from './file-upload-api.actions';
16 | import * as FileUploadUIActions from './file-upload-ui.actions';
17 | import * as FileUploadSelectors from './file-upload.selectors';
18 |
19 | @Injectable()
20 | export class FileUploadEffects {
21 | constructor(
22 | private fileUploadService: FileUploadService,
23 | private actions$: Actions,
24 | private store: Store<{}>
25 | ) {}
26 |
27 | processQueueEffect$ = createEffect(() =>
28 | this.actions$.pipe(
29 | ofType(
30 | FileUploadUIActions.processRequested,
31 | FileUploadUIActions.retryRequested
32 | ),
33 | withLatestFrom(
34 | this.store.select(FileUploadSelectors.selectFilesReadyForUpload)
35 | ),
36 | switchMap(([_, filesToUpload]) =>
37 | filesToUpload.map(fileToUpload =>
38 | FileUploadAPIActions.uploadRequested({ fileToUpload })
39 | )
40 | )
41 | )
42 | );
43 |
44 | uploadEffect$ = createEffect(() =>
45 | this.actions$.pipe(
46 | ofType(FileUploadAPIActions.uploadRequested),
47 | mergeMap(({ fileToUpload }) =>
48 | this.fileUploadService.uploadFile(fileToUpload.rawFile).pipe(
49 | takeUntil(
50 | this.actions$.pipe(ofType(FileUploadUIActions.cancelRequested))
51 | ),
52 | map(event => this.getActionFromHttpEvent(fileToUpload.id, event)),
53 | catchError(error =>
54 | of(
55 | FileUploadAPIActions.uploadFailed({
56 | error: error.message,
57 | id: fileToUpload.id
58 | })
59 | )
60 | )
61 | )
62 | )
63 | )
64 | );
65 |
66 | private getActionFromHttpEvent(id: string, event: HttpEvent) {
67 | switch (event.type) {
68 | case HttpEventType.Sent: {
69 | return FileUploadAPIActions.uploadStarted({ id });
70 | }
71 | case HttpEventType.DownloadProgress:
72 | case HttpEventType.UploadProgress: {
73 | return FileUploadAPIActions.uploadProgressed({
74 | id,
75 | progress: Math.round((100 * event.loaded) / event.total)
76 | });
77 | }
78 | case HttpEventType.ResponseHeader:
79 | case HttpEventType.Response: {
80 | if (event.status === 200) {
81 | return FileUploadAPIActions.uploadCompleted({ id });
82 | } else {
83 | return FileUploadAPIActions.uploadFailed({
84 | id,
85 | error: event.statusText
86 | });
87 | }
88 | }
89 |
90 | default: {
91 | return FileUploadAPIActions.uploadFailed({
92 | id,
93 | error: `Unknown Event: ${JSON.stringify(event)}`
94 | });
95 | }
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/state/file-upload.reducer.ts:
--------------------------------------------------------------------------------
1 | import { createEntityAdapter, EntityState } from '@ngrx/entity';
2 | import { Action, createReducer, on } from '@ngrx/store';
3 | import * as uuid from 'uuid';
4 | import { FileUploadModel, FileUploadStatus } from '../models';
5 | import * as FileUploadAPIActions from './file-upload-api.actions';
6 | import * as FileUploadUIActions from './file-upload-ui.actions';
7 |
8 | export interface FileUploadState extends EntityState {}
9 |
10 | export const fileUploadFeatureKey = 'fileUpload';
11 |
12 | export const featureAdapter = createEntityAdapter({
13 | selectId: (model: FileUploadModel) => model.id
14 | });
15 |
16 | export const initialState: FileUploadState = featureAdapter.getInitialState();
17 |
18 | const fileUploadReducer = createReducer(
19 | initialState,
20 | on(FileUploadUIActions.added, (state, { file }) =>
21 | featureAdapter.addOne(
22 | {
23 | id: uuid.v4(),
24 | fileName: file.name,
25 | fileSize: file.size,
26 | rawFile: file,
27 | error: null,
28 | progress: null,
29 | status: FileUploadStatus.Ready
30 | },
31 | state
32 | )
33 | ),
34 | on(FileUploadUIActions.removed, (state, { id }) =>
35 | featureAdapter.removeOne(id, state)
36 | ),
37 | on(FileUploadUIActions.retryRequested, (state, { id }) =>
38 | featureAdapter.updateOne(
39 | {
40 | id,
41 | changes: {
42 | status: FileUploadStatus.Ready,
43 | progress: 0,
44 | error: null
45 | }
46 | },
47 | state
48 | )
49 | ),
50 | on(FileUploadAPIActions.uploadRequested, (state, { fileToUpload }) =>
51 | featureAdapter.updateOne(
52 | { id: fileToUpload.id, changes: { status: FileUploadStatus.Requested } },
53 | state
54 | )
55 | ),
56 | on(FileUploadAPIActions.uploadStarted, (state, { id }) =>
57 | featureAdapter.updateOne(
58 | { id: id, changes: { status: FileUploadStatus.Started, progress: 0 } },
59 | state
60 | )
61 | ),
62 | on(FileUploadAPIActions.uploadProgressed, (state, { id, progress }) =>
63 | featureAdapter.updateOne(
64 | {
65 | id: id,
66 | changes: { status: FileUploadStatus.InProgress, progress: progress }
67 | },
68 | state
69 | )
70 | ),
71 | on(FileUploadAPIActions.uploadCompleted, (state, { id }) =>
72 | featureAdapter.updateOne(
73 | {
74 | id: id,
75 | changes: { status: FileUploadStatus.Completed, progress: 100 }
76 | },
77 | state
78 | )
79 | ),
80 | on(FileUploadAPIActions.uploadFailed, (state, { id, error }) =>
81 | featureAdapter.updateOne(
82 | {
83 | id: id,
84 | changes: {
85 | status: FileUploadStatus.Failed,
86 | progress: null,
87 | error
88 | }
89 | },
90 | state
91 | )
92 | ),
93 | on(FileUploadUIActions.cancelRequested, _ => ({
94 | ...initialState
95 | }))
96 | );
97 |
98 | export function reducer(state: FileUploadState | undefined, action: Action) {
99 | return fileUploadReducer(state, action);
100 | }
101 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/state/file-upload.selectors.ts:
--------------------------------------------------------------------------------
1 | import {
2 | faCheck,
3 | faExclamationCircle
4 | } from '@fortawesome/free-solid-svg-icons';
5 | import { createFeatureSelector, createSelector } from '@ngrx/store';
6 | import { FileUploadModel, FileUploadStatus, FileViewModel } from '../models';
7 | import {
8 | featureAdapter,
9 | fileUploadFeatureKey,
10 | FileUploadState
11 | } from './file-upload.reducer';
12 |
13 | const selectFileUploadState = createFeatureSelector(
14 | fileUploadFeatureKey
15 | );
16 |
17 | const selectAllFileUploads: (
18 | state: object
19 | ) => FileUploadModel[] = featureAdapter.getSelectors(selectFileUploadState)
20 | .selectAll;
21 |
22 | const getFileViewModelIcon = (fileStatus: FileUploadStatus) => {
23 | switch (fileStatus) {
24 | case FileUploadStatus.Completed:
25 | return faCheck;
26 | case FileUploadStatus.Failed:
27 | return faExclamationCircle;
28 | default:
29 | return null;
30 | }
31 | };
32 |
33 | const getFileViewModelColorClass = (fileStatus: FileUploadStatus) => {
34 | switch (fileStatus) {
35 | case FileUploadStatus.Completed:
36 | return 'green';
37 | case FileUploadStatus.Failed:
38 | return 'red';
39 | default:
40 | return 'black';
41 | }
42 | };
43 |
44 | const getFormattedFileSize = (fileSize: number) => {
45 | if (fileSize && !isNaN(fileSize)) {
46 | const fileSizeInKB = Math.round(fileSize / 1024);
47 | return `${fileSizeInKB} KB`;
48 | }
49 | return `${fileSize}`;
50 | };
51 |
52 | const getFileViewModel = (file: FileUploadModel): FileViewModel => ({
53 | id: file.id,
54 | fileName: file.fileName,
55 | formattedFileSize: getFormattedFileSize(file.fileSize),
56 | canRetry: file.status === FileUploadStatus.Failed,
57 | canDelete: file.status !== FileUploadStatus.Completed,
58 | statusIcon: getFileViewModelIcon(file.status),
59 | statusColorClass: getFileViewModelColorClass(file.status),
60 | showProgress:
61 | file.status === FileUploadStatus.InProgress &&
62 | file.progress &&
63 | file.progress >= 0,
64 | progress: file.progress,
65 | errorMessage: file.status === FileUploadStatus.Failed && file.error
66 | });
67 |
68 | export const selectFilesReadyForUpload = createSelector(
69 | selectAllFileUploads,
70 | (allUploads: FileUploadModel[]) =>
71 | allUploads && allUploads.filter(f => f.status === FileUploadStatus.Ready)
72 | );
73 |
74 | export const selectFileUploadQueue = createSelector(
75 | selectAllFileUploads,
76 | files => files && files.map(file => getFileViewModel(file))
77 | );
78 |
--------------------------------------------------------------------------------
/libs/file-upload/src/lib/state/index.ts:
--------------------------------------------------------------------------------
1 | import * as FileUploadUIActions from './file-upload-ui.actions';
2 | import * as FileUploadSelectors from './file-upload.selectors';
3 | export { FileUploadEffects } from './file-upload.effects';
4 | export { fileUploadFeatureKey, reducer } from './file-upload.reducer';
5 | export { FileUploadSelectors, FileUploadUIActions };
6 |
--------------------------------------------------------------------------------
/libs/file-upload/src/test-setup.ts:
--------------------------------------------------------------------------------
1 | import 'jest-preset-angular';
2 |
--------------------------------------------------------------------------------
/libs/file-upload/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "types": ["node", "jest"]
5 | },
6 | "include": ["**/*.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/libs/file-upload/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "target": "es2015",
6 | "declaration": true,
7 | "inlineSources": true,
8 | "types": [],
9 | "lib": ["dom", "es2018"]
10 | },
11 | "angularCompilerOptions": {
12 | "annotateForClosureCompiler": true,
13 | "skipTemplateCodegen": true,
14 | "strictMetadataEmit": true,
15 | "fullTemplateTypeCheck": true,
16 | "strictInjectionParameters": true,
17 | "enableResourceInlining": true
18 | },
19 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"]
20 | }
21 |
--------------------------------------------------------------------------------
/libs/file-upload/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "files": ["src/test-setup.ts"],
9 | "include": ["**/*.spec.ts", "**/*.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/libs/file-upload/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json",
3 | "rules": {
4 | "directive-selector": [true, "attribute", "realWorldApp", "camelCase"],
5 | "component-selector": [true, "element", "app", "kebab-case"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "npmScope": "app",
3 | "implicitDependencies": {
4 | "angular.json": "*",
5 | "package.json": "*",
6 | "tsconfig.json": "*",
7 | "tslint.json": "*",
8 | "nx.json": "*"
9 | },
10 | "projects": {
11 | "app-e2e": {
12 | "tags": []
13 | },
14 | "app": {
15 | "tags": []
16 | },
17 | "api": {
18 | "tags": []
19 | },
20 | "file-upload": {
21 | "tags": []
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "ng": "ng",
7 | "nx": "nx",
8 | "start": "ng serve",
9 | "build": "ng build",
10 | "test": "ng test",
11 | "lint": "nx workspace-lint && ng lint",
12 | "e2e": "ng e2e",
13 | "serve-with-api": "ng run app:serve-with-api",
14 | "affected:apps": "nx affected:apps",
15 | "affected:libs": "nx affected:libs",
16 | "affected:build": "nx affected:build",
17 | "affected:e2e": "nx affected:e2e",
18 | "affected:test": "nx affected:test",
19 | "affected:lint": "nx affected:lint",
20 | "affected:dep-graph": "nx affected:dep-graph",
21 | "affected": "nx affected",
22 | "format": "nx format:write",
23 | "format:write": "nx format:write",
24 | "format:check": "nx format:check",
25 | "update": "ng update @nrwl/workspace",
26 | "update:check": "ng update",
27 | "workspace-schematic": "nx workspace-schematic",
28 | "dep-graph": "nx dep-graph",
29 | "help": "nx help"
30 | },
31 | "private": true,
32 | "dependencies": {
33 | "@angular/animations": "^8.2.0",
34 | "@angular/common": "^8.2.0",
35 | "@angular/compiler": "^8.2.0",
36 | "@angular/core": "^8.2.0",
37 | "@angular/forms": "^8.2.0",
38 | "@angular/platform-browser": "^8.2.0",
39 | "@angular/platform-browser-dynamic": "^8.2.0",
40 | "@angular/router": "^8.2.0",
41 | "@fortawesome/angular-fontawesome": "^0.5.0",
42 | "@fortawesome/fontawesome-svg-core": "^1.2.22",
43 | "@fortawesome/free-solid-svg-icons": "^5.10.2",
44 | "@nestjs/common": "^6.2.4",
45 | "@nestjs/core": "^6.2.4",
46 | "@nestjs/platform-express": "^6.7.2",
47 | "@ng-bootstrap/ng-bootstrap": "^5.1.1",
48 | "@ngrx/effects": "^8.3.0",
49 | "@ngrx/entity": "^8.3.0",
50 | "@ngrx/store": "^8.3.0",
51 | "@ngrx/store-devtools": "^8.3.0",
52 | "@nrwl/angular": "8.5.0",
53 | "bootstrap": "^4.3.1",
54 | "core-js": "^2.5.4",
55 | "file-saver": "^2.0.2",
56 | "reflect-metadata": "^0.1.12",
57 | "rxjs": "~6.4.0",
58 | "serialize-error": "^5.0.0",
59 | "uuid": "^3.3.3",
60 | "zone.js": "^0.9.1"
61 | },
62 | "devDependencies": {
63 | "@angular-devkit/build-angular": "^0.803.3",
64 | "@angular/cli": "8.3.3",
65 | "@angular/compiler-cli": "^8.2.0",
66 | "@angular/language-service": "^8.2.0",
67 | "@nestjs/schematics": "^6.3.0",
68 | "@nestjs/testing": "^6.2.4",
69 | "@nrwl/cypress": "8.5.0",
70 | "@nrwl/jest": "8.5.0",
71 | "@nrwl/nest": "8.5.0",
72 | "@nrwl/node": "8.5.0",
73 | "@nrwl/workspace": "8.5.0",
74 | "@types/file-saver": "^2.0.1",
75 | "@types/jest": "24.0.9",
76 | "@types/node": "~8.9.4",
77 | "codelyzer": "~5.0.1",
78 | "cypress": "3.4.1",
79 | "dotenv": "6.2.0",
80 | "eslint": "6.1.0",
81 | "jest": "24.1.0",
82 | "jest-preset-angular": "7.0.0",
83 | "prettier": "1.16.4",
84 | "ts-jest": "24.0.0",
85 | "ts-node": "~7.0.0",
86 | "tslint": "~5.11.0",
87 | "typescript": "~3.4.5"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tools/schematics/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesleygrimes/managing-file-uploads-with-ngrx/9e344617546fab19e3b007205cf2f33c7607ab22/tools/schematics/.gitkeep
--------------------------------------------------------------------------------
/tools/tsconfig.tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../dist/out-tsc/tools",
5 | "rootDir": ".",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": ["node"]
9 | },
10 | "include": ["**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "importHelpers": true,
11 | "target": "es2015",
12 | "module": "esnext",
13 | "typeRoots": ["node_modules/@types"],
14 | "lib": ["es2017", "dom"],
15 | "skipLibCheck": true,
16 | "skipDefaultLibCheck": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@app/file-upload": ["libs/file-upload/src/index.ts"]
20 | }
21 | },
22 | "exclude": ["node_modules", "tmp"]
23 | }
24 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/@nrwl/workspace/src/tslint",
4 | "node_modules/codelyzer"
5 | ],
6 | "rules": {
7 | "arrow-return-shorthand": true,
8 | "callable-types": true,
9 | "class-name": true,
10 | "deprecation": {
11 | "severity": "warn"
12 | },
13 | "forin": true,
14 | "import-blacklist": [true, "rxjs/Rx"],
15 | "interface-over-type-literal": true,
16 | "member-access": false,
17 | "member-ordering": [
18 | true,
19 | {
20 | "order": [
21 | "static-field",
22 | "instance-field",
23 | "static-method",
24 | "instance-method"
25 | ]
26 | }
27 | ],
28 | "no-arg": true,
29 | "no-bitwise": true,
30 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
31 | "no-construct": true,
32 | "no-debugger": true,
33 | "no-duplicate-super": true,
34 | "no-empty": false,
35 | "no-empty-interface": true,
36 | "no-eval": true,
37 | "no-inferrable-types": [true, "ignore-params"],
38 | "no-misused-new": true,
39 | "no-non-null-assertion": true,
40 | "no-shadowed-variable": true,
41 | "no-string-literal": false,
42 | "no-string-throw": true,
43 | "no-switch-case-fall-through": true,
44 | "no-unnecessary-initializer": true,
45 | "no-unused-expression": true,
46 | "no-var-keyword": true,
47 | "object-literal-sort-keys": false,
48 | "prefer-const": true,
49 | "radix": true,
50 | "triple-equals": [true, "allow-null-check"],
51 | "unified-signatures": true,
52 | "variable-name": false,
53 | "nx-enforce-module-boundaries": [
54 | true,
55 | {
56 | "allow": [],
57 | "depConstraints": [
58 | {
59 | "sourceTag": "*",
60 | "onlyDependOnLibsWithTags": ["*"]
61 | }
62 | ]
63 | }
64 | ],
65 | "directive-selector": [true, "attribute", "app", "camelCase"],
66 | "component-selector": [true, "element", "app", "kebab-case"],
67 | "no-conflicting-lifecycle": true,
68 | "no-host-metadata-property": true,
69 | "no-input-rename": true,
70 | "no-inputs-metadata-property": true,
71 | "no-output-native": true,
72 | "no-output-on-prefix": true,
73 | "no-output-rename": true,
74 | "no-outputs-metadata-property": true,
75 | "template-banana-in-box": true,
76 | "template-no-negated-async": true,
77 | "use-lifecycle-interface": true,
78 | "use-pipe-transform-interface": true
79 | }
80 | }
81 |
--------------------------------------------------------------------------------