├── .editorconfig ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── angular.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── build-specifics │ │ ├── index.prod.ts │ │ └── index.ts │ ├── home │ │ ├── menu.component.html │ │ ├── menu.component.ts │ │ ├── page-not-found.component.ts │ │ ├── shell.component.css │ │ ├── shell.component.html │ │ ├── shell.component.ts │ │ ├── welcome.component.html │ │ └── welcome.component.ts │ ├── products │ │ ├── product-data.ts │ │ ├── product-edit │ │ │ ├── product-edit.component.html │ │ │ └── product-edit.component.ts │ │ ├── product-list │ │ │ ├── product-list.component.css │ │ │ ├── product-list.component.html │ │ │ └── product-list.component.ts │ │ ├── product-shell │ │ │ ├── product-shell.component.html │ │ │ └── product-shell.component.ts │ │ ├── product.module.ts │ │ ├── product.service.ts │ │ ├── product.ts │ │ └── state │ │ │ ├── actions │ │ │ ├── index.ts │ │ │ ├── product-api.actions.ts │ │ │ └── product-page.actions.ts │ │ │ ├── index.ts │ │ │ ├── product-state-facade.service.ts │ │ │ ├── product.effects.ts │ │ │ └── product.reducer.ts │ ├── shared │ │ ├── generic-validator.ts │ │ ├── number.validator.ts │ │ └── shared.module.ts │ └── user │ │ ├── auth-guard.service.ts │ │ ├── auth.service.ts │ │ ├── login.component.css │ │ ├── login.component.html │ │ ├── login.component.ts │ │ ├── state │ │ ├── actions │ │ │ ├── index.ts │ │ │ └── user-page.actions.ts │ │ ├── user-state-facade.service.ts │ │ └── user.reducer.ts │ │ ├── user.module.ts │ │ └── user.ts ├── assets │ ├── .gitkeep │ └── images │ │ └── logo.jpg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css └── test.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.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 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.angular/cache 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | package.json 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true, 4 | "printWidth": 100, 5 | "htmlWhitespaceSensitivity": "ignore" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Florian Spier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | Project contains copy pasted code from https://github.com/DeborahK/Angular-NgRx-GettingStarted 25 | 26 | MIT License 27 | 28 | Copyright (c) 2018 Deborah Kurata 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to deal 32 | in the Software without restriction, including without limitation the rights 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in all 38 | copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 46 | SOFTWARE. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## State Management Bundle Size Comparison Angular 2 | 3 | Checking the **app** bundle sizes for different state management solutions with [source-map-explorer](https://www.npmjs.com/package/source-map-explorer). 4 | 5 | The project is based on https://github.com/DeborahK/Angular-NgRx-GettingStarted. 6 | 7 | See the branches for the different setups. 8 | 9 | Run `npm run build:stats` to let source-map-explorer calculate the **prod** bundle size. 10 | 11 | ## Results 12 | 13 | The measured size represents the **total size of the app**, which is build with **production** configuration. 14 | 15 | ### Angular 19.1.3 16 | 17 | | Library | Version | Size (KB) | Comments | Branch | 18 | |---------------------------------------------|---------|-----------|----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 19 | | 🚦DIY Signal State Service | - | 374.34 | | [ng@19.1.3--diy-signal-state-service](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--diy-signal-state-service) | 20 | | DIY RxJS State Service | - | 376.77 | | [ng@19.1.3--diy-rxjs-state-service](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--diy-rxjs-state-service) | 21 | | Elf | 2.5.1 | 379.22 | Uses [ngneat/effects](https://github.com/ngneat/effects) for effects | [ng@19.1.3--elf@2.5.1](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--elf%402.5.1) | 22 | | 🚦NgRx Signal Store | 19.0.0 | 380.15 | | [ng@19.1.3--ngrx-signals@19.0.0](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--ngrx-signals%4019.0.0) | 23 | | MiniRx Store (Feature Store API) | 6.0.0 | 384.13 | | [ng@19.1.3--mini-rx-store@6.0.0--feature-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--mini-rx-store%406.0.0--feature-store-api) | 24 | | MiniRx Store (Component Store API) | 6.0.0 | 384.37 | | [ng@19.1.3--mini-rx-store@6.0.0--component-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--mini-rx-store%406.0.0--component-store-api) | 25 | | 🚦MiniRx Signal Store (Feature Store API) | 3.0.0 | 385.38 | | [ng@19.1.3--mini-rx-signal-store@3.0.0--feature-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--mini-rx-signal-store%403.0.0--feature-store-api) | 26 | | 🚦MiniRx Signal Store (Component Store API) | 3.0.0 | 385.64 | | [ng@19.1.3--mini-rx-signal-store@3.0.0--component-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--mini-rx-signal-store%403.0.0--component-store-api) | 27 | | 🚦MiniRx Signal Store (Redux Store API) | 3.0.0 | 385.69 | Uses [ts-action](https://github.com/cartant/ts-action) for actions | [ng@19.1.3--mini-rx-signal-store@3.0.0--redux-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--mini-rx-signal-store%403.0.0--redux-store-api) | 28 | | NgRx Component Store | 19.0.0 | 385.64 | | [ng@19.1.3--ngrx-component-store@19.0.0](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--ngrx-component-store%4019.0.0) | 29 | | MiniRx Store (Redux Store API) | 6.0.0 | 390.31 | Uses [ts-action](https://github.com/cartant/ts-action) for actions | [ng@19.1.3--mini-rx-store@6.0.0--redux-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--mini-rx-store%406.0.0--redux-store-api) | 30 | | NgRx Store | 19.0.0 | 408.33 | Uses [ngrx/effects](https://ngrx.io/guide/effects) for effects | [ng@19.1.3--ngrx-store@19.0.0 ](https://github.com/spierala/angular-state-management-comparison/tree/ng%4019.1.3--ngrx-store%4019.0.0) | 31 | 32 | ### Angular 17.0.3 33 | 34 | | Library | Version | Size (KB) | Comments | Branch | 35 | |---------------------------------------------|-------------|-----------|----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 36 | | DIY RxJS State Service | - | 377.05 | | [ng@17.0.3--diy-rxjs-state-service](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--diy-rxjs-state-service) | 37 | | 🚦DIY Signal State Service | - | 377.20 | | [ng@17.0.3--diy-signal-state-service](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--diy-signal-state-service) | 38 | | Elf | 2.4.0 | 379.48 | Uses [ngneat/effects](https://github.com/ngneat/effects) for effects | [ng@17.0.3--elf@2.4](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--elf%402.4) | 39 | | 🚦NgRx Signal Store | 17.0.1 | 382.83 | | [ng@17.0.3--ngrx-signals@17.0.1](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--ngrx-signals%4017.0.1) | 40 | | MiniRx Store (Component Store API) | 5.1.0 | 382.91 | | [ng@17.0.3--mini-rx-store@5.1--component-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--mini-rx-store%405.1--component-store-api) | 41 | | MiniRx Store (Feature Store API) | 5.1.0 | 383.53 | | [ng@17.0.3--mini-rx-store@5.1--feature-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--mini-rx-store%405.1--feature-store-api) | 42 | | 🚦MiniRx Signal Store (Component Store API) | 0.0.24 | 386.08 | | [ng@17.0.3--mini-rx-signal-store@0.0.21--component-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--mini-rx-signal-store%400.0.21--component-store-api) | 43 | | 🚦MiniRx Signal Store (Feature Store API) | 0.0.24 | 386.66 | | [ng@17.0.3--mini-rx-signal-store@0.0.21--feature-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--mini-rx-signal-store%400.0.21--feature-store-api) | 44 | | 🚦MiniRx Signal Store (Redux Store API) | 0.0.24 | 387.76 | Uses [ts-action](https://github.com/cartant/ts-action) for actions | [ng@17.0.3--mini-rx-signal-store@0.0.21--redux-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--mini-rx-signal-store%400.0.21--redux-store-api) | 45 | | NgRx Component Store | 17.0.0-rc.0 | 388.93 | | [ng@17.0.3--ngrx-component-store@17.0.0-rc.0](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--ngrx-component-store%4017.0.0-rc.0) | 46 | | MiniRx Store (Redux Store API) | 5.1.0 | 390.55 | Uses [ts-action](https://github.com/cartant/ts-action) for actions | [ng@17.0.3--mini-rx-store@5.1--redux-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--mini-rx-store%405.1--redux-store-api) | 47 | | Akita | 8.0.1 | 401.79 | | [ng@17.0.3--akita@8.0.1 ](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--akita%408.0.1) | 48 | | NgRx Store | 17.0.0-rc.0 | 408.18 | Uses [ngrx/effects](https://ngrx.io/guide/effects) for effects | [ng@17.0.3--ngrx-store@17.0.0-rc.0](https://github.com/spierala/angular-state-management-comparison/tree/ng%4017.0.3--ngrx-store%4017.0.0-rc.0) | 49 | 50 | ### Angular 16.1 51 | 52 | | Library | Version | Size (KB) | Comments | Branch | 53 | |---------------------------------------------|---------|-----------|----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 54 | | 🚦DIY Signal State Service | - | 371.98 | | [ng@16.1--diy-signal-state-service](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--diy-signal-state-service) | 55 | | DIY RxJS State Service | - | 372.08 | | [ng@16.1--diy-rxjs-state-service](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--diy-rxjs-state-service) | 56 | | Elf | 2.3.2 | 374.59 | Uses [ngneat/effects](https://github.com/ngneat/effects) for effects | [ng@16.1--elf@2.3](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--elf%402.3) | 57 | | MiniRx Store (Component Store API) | 5.1.0 | 378.08 | | [ng@16.1--mini-rx-store@5.1--component-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--mini-rx-store%405.1--component-store-api) | 58 | | MiniRx Store (Feature Store API) | 5.1.0 | 378.72 | | [ng@16.1--mini-rx-store@5.1--feature-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--mini-rx-store%405.1--feature-store-api) | 59 | | 🚦MiniRx Signal Store (Component Store API) | 0.0.5 | 380.53 | | [ng@16.1--mini-rx-signal-store@0.0.5--component-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--mini-rx-signal-store%400.0.5--component-store-api) | 60 | | 🚦MiniRx Signal Store (Feature Store API) | 0.0.5 | 380.97 | | [ng@16.1--mini-rx-signal-store@0.0.5--feature-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--mini-rx-signal-store%400.0.5--feature-store-api) | 61 | | 🚦MiniRx Signal Store (Redux Store API) | 0.0.5 | 382.54 | Uses [ts-action](https://github.com/cartant/ts-action) for actions | [ng@16.1--mini-rx-signal-store@0.0.5--redux-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--mini-rx-signal-store%400.0.5--redux-store-api) | 62 | | NgRx Component Store | 16.1.0 | 383.98 | | [ng@16.1--ngrx-component-store@16.1](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--ngrx-component-store%4016.1) | 63 | | MiniRx Store (Redux Store API) | 5.1.0 | 385.49 | Uses [ts-action](https://github.com/cartant/ts-action) for actions | [ng@16.1--mini-rx-store@5.1--redux-store-api](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--mini-rx-store%405.1--redux-store-api) | 64 | | Akita | 8.0.1 | 396.68 | | [ng@16.1--akita@8.0](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--akita%408.0) | 65 | | NgRx Store | 16.1.0 | 402.90 | Uses [ngrx/effects](https://ngrx.io/guide/effects) for effects | [ng@16.1--ngrx-store@16.1](https://github.com/spierala/angular-state-management-comparison/tree/ng%4016.1--ngrx-store%4016.1) | 66 | 67 | ## Contributing 68 | 69 | You are welcome to add your favourite state management library as well! 70 | 71 | You can follow these steps: 72 | 73 | 1. Fork and clone the repo 74 | 2. Create a branch based on master (or based on another branch with a familiar state management lib (e.g. "ng@17.0.3--diy-signal-state-service") 75 | 3. Specify the Angular version and the state management library (and version) in the branch name: e.g. "ng@17.0.3--ngrx-signals@17.0.0") 76 | 4. Now refactor to your favourite state management solution (to have equal conditions: try to follow the facade pattern for the state management code, and use something for effects) 77 | 5. Run `npm run build:stats` to check the bundle size 78 | 6. Create the PR 79 | - add the bundle size in the PR description 80 | - target of the PR is this repo and the branch which you initially used as the base for your refactor-branch (e.g. "ng@17.0.3--diy-signal-state-service") 81 | 7. I will review your PR and add your results to the README on the master branch 82 | 8. Finally, I will merge your work to another branch which I will create (e.g. "ng@17.0.3--ngrx-signals@17.0.0") and link that branch in the README 83 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ng-state-management-comparison": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:application": { 10 | "strict": true 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": { 21 | "base": "dist/ng-state-management-comparison" 22 | }, 23 | "index": "src/index.html", 24 | "polyfills": ["src/polyfills.ts"], 25 | "tsConfig": "tsconfig.app.json", 26 | "assets": ["src/favicon.ico", "src/assets"], 27 | "styles": [ 28 | "src/styles.css", 29 | "node_modules/bootstrap/dist/css/bootstrap.css" 30 | ], 31 | "scripts": [], 32 | "browser": "src/main.ts" 33 | }, 34 | "configurations": { 35 | "production": { 36 | "budgets": [ 37 | { 38 | "type": "initial", 39 | "maximumWarning": "500kb", 40 | "maximumError": "1mb" 41 | }, 42 | { 43 | "type": "anyComponentStyle", 44 | "maximumWarning": "2kb", 45 | "maximumError": "4kb" 46 | } 47 | ], 48 | "fileReplacements": [ 49 | { 50 | "replace": "src/environments/environment.ts", 51 | "with": "src/environments/environment.prod.ts" 52 | }, 53 | { 54 | "replace": "src/app/build-specifics/index.ts", 55 | "with": "src/app/build-specifics/index.prod.ts" 56 | } 57 | ], 58 | "outputHashing": "all" 59 | }, 60 | "development": { 61 | "optimization": false, 62 | "extractLicenses": false, 63 | "sourceMap": true, 64 | "namedChunks": true 65 | } 66 | }, 67 | "defaultConfiguration": "production" 68 | }, 69 | "serve": { 70 | "builder": "@angular-devkit/build-angular:dev-server", 71 | "configurations": { 72 | "production": { 73 | "buildTarget": "ng-state-management-comparison:build:production" 74 | }, 75 | "development": { 76 | "buildTarget": "ng-state-management-comparison:build:development" 77 | } 78 | }, 79 | "defaultConfiguration": "development" 80 | }, 81 | "extract-i18n": { 82 | "builder": "@angular-devkit/build-angular:extract-i18n", 83 | "options": { 84 | "buildTarget": "ng-state-management-comparison:build" 85 | } 86 | }, 87 | "test": { 88 | "builder": "@angular-devkit/build-angular:karma", 89 | "options": { 90 | "main": "src/test.ts", 91 | "polyfills": "src/polyfills.ts", 92 | "tsConfig": "tsconfig.spec.json", 93 | "karmaConfig": "karma.conf.js", 94 | "assets": ["src/favicon.ico", "src/assets"], 95 | "styles": ["src/styles.css"], 96 | "scripts": [] 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true, // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/ng-state-management-comparison'), 29 | subdir: '.', 30 | reporters: [{ type: 'html' }, { type: 'text-summary' }], 31 | }, 32 | reporters: ['progress', 'kjhtml'], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | browsers: ['Chrome'], 38 | singleRun: false, 39 | restartOnFileChange: true, 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-state-management-comparison", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "build:sourcemap": "ng build --source-map", 9 | "build:stats": "npm run build:sourcemap && source-map-explorer dist/**/*.js", 10 | "watch": "ng build --watch --configuration development", 11 | "test": "ng test", 12 | "pretty-quick": "pretty-quick" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "~19.1.3", 17 | "@angular/common": "~19.1.3", 18 | "@angular/compiler": "~19.1.3", 19 | "@angular/core": "~19.1.3", 20 | "@angular/forms": "~19.1.3", 21 | "angular-in-memory-web-api": "^0.19.0", 22 | "@angular/platform-browser": "~19.1.3", 23 | "@angular/platform-browser-dynamic": "~19.1.3", 24 | "@angular/router": "~19.1.3", 25 | "@ngneat/effects-ng": "3.1.4", 26 | "@ngneat/elf": "2.5.1", 27 | "@ngrx/component-store": "19.0.0", 28 | "@ngrx/effects": "19.0.0", 29 | "@ngrx/operators": "19.0.0", 30 | "@ngrx/signals": "19.0.0", 31 | "@ngrx/store": "19.0.0", 32 | "@ngrx/store-devtools": "19.0.0", 33 | "@mini-rx/signal-store": "3.0.0", 34 | "bootstrap": "^4.6.2", 35 | "mini-rx-store": "6.0.0", 36 | "mini-rx-store-ng": "5.0.0", 37 | "rxjs": "7.8.1", 38 | "tslib": "^2.3.0", 39 | "zone.js": "~0.15.0" 40 | }, 41 | "devDependencies": { 42 | "@angular-devkit/build-angular": "~19.1.4", 43 | "@angular/cli": "~19.1.4", 44 | "@angular/compiler-cli": "~19.1.3", 45 | "@types/jasmine": "~3.10.0", 46 | "@types/node": "^12.11.1", 47 | "jasmine-core": "~3.10.0", 48 | "karma": "~6.3.0", 49 | "karma-chrome-launcher": "~3.1.0", 50 | "karma-coverage": "~2.1.0", 51 | "karma-jasmine": "~4.0.0", 52 | "karma-jasmine-html-reporter": "~1.7.0", 53 | "typescript": "~5.5.4", 54 | "prettier": "^2.3.2", 55 | "pretty-quick": "^2.0.1", 56 | "husky": "^4.3.8", 57 | "source-map-explorer": "^2.5.2" 58 | }, 59 | "husky": { 60 | "hooks": { 61 | "pre-commit": "pretty-quick --staged" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { AuthGuard } from './user/auth-guard.service'; 5 | 6 | import { ShellComponent } from './home/shell.component'; 7 | import { WelcomeComponent } from './home/welcome.component'; 8 | import { PageNotFoundComponent } from './home/page-not-found.component'; 9 | 10 | const appRoutes: Routes = [ 11 | { 12 | path: '', 13 | component: ShellComponent, 14 | children: [ 15 | { path: 'welcome', component: WelcomeComponent }, 16 | { 17 | path: 'products', 18 | // canActivate: [AuthGuard], 19 | loadChildren: () => 20 | import('./products/product.module').then((m) => m.ProductModule), 21 | }, 22 | { path: '', redirectTo: 'welcome', pathMatch: 'full' }, 23 | ], 24 | }, 25 | { path: '**', component: PageNotFoundComponent }, 26 | ]; 27 | 28 | @NgModule({ 29 | imports: [RouterModule.forRoot(appRoutes)], 30 | exports: [RouterModule], 31 | }) 32 | export class AppRoutingModule {} 33 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spierala/angular-state-management-comparison/11e3f67f34f5e5c0241f0dd6bb62499e1ee238d1/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'pm-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'], 7 | standalone: false, 8 | }) 9 | export class AppComponent {} 10 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpClientModule, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 4 | 5 | // Imports for loading & configuring the in-memory web api 6 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; 7 | import { ProductData } from './products/product-data'; 8 | 9 | import { AppRoutingModule } from './app-routing.module'; 10 | 11 | import { AppComponent } from './app.component'; 12 | import { ShellComponent } from './home/shell.component'; 13 | import { MenuComponent } from './home/menu.component'; 14 | import { WelcomeComponent } from './home/welcome.component'; 15 | import { PageNotFoundComponent } from './home/page-not-found.component'; 16 | 17 | /* Feature Modules */ 18 | import { UserModule } from './user/user.module'; 19 | 20 | import { StoreModule } from '@ngrx/store'; 21 | import { EffectsModule } from '@ngrx/effects'; 22 | import { extModules } from './build-specifics'; 23 | 24 | @NgModule({ 25 | declarations: [ 26 | AppComponent, 27 | ShellComponent, 28 | MenuComponent, 29 | WelcomeComponent, 30 | PageNotFoundComponent, 31 | ], 32 | bootstrap: [AppComponent], 33 | imports: [ 34 | BrowserModule, 35 | HttpClientModule, 36 | HttpClientInMemoryWebApiModule.forRoot(ProductData), 37 | UserModule, 38 | AppRoutingModule, 39 | StoreModule.forRoot({}), 40 | EffectsModule.forRoot([]), 41 | extModules, 42 | ], 43 | }) 44 | export class AppModule {} 45 | -------------------------------------------------------------------------------- /src/app/build-specifics/index.prod.ts: -------------------------------------------------------------------------------- 1 | export const extModules = []; 2 | -------------------------------------------------------------------------------- /src/app/build-specifics/index.ts: -------------------------------------------------------------------------------- 1 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 2 | 3 | export const extModules = [ 4 | StoreDevtoolsModule.instrument({ 5 | maxAge: 25, 6 | }), 7 | ]; 8 | -------------------------------------------------------------------------------- /src/app/home/menu.component.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /src/app/home/menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { AuthService } from '../user/auth.service'; 5 | 6 | @Component({ 7 | selector: 'pm-menu', 8 | templateUrl: './menu.component.html', 9 | standalone: false, 10 | }) 11 | export class MenuComponent implements OnInit { 12 | pageTitle = 'Acme Product Management'; 13 | 14 | get isLoggedIn(): boolean { 15 | return this.authService.isLoggedIn(); 16 | } 17 | 18 | get userName(): string { 19 | if (this.authService.currentUser) { 20 | return this.authService.currentUser.userName; 21 | } 22 | return ''; 23 | } 24 | 25 | constructor(private router: Router, private authService: AuthService) {} 26 | 27 | ngOnInit() {} 28 | 29 | logOut(): void { 30 | this.authService.logout(); 31 | this.router.navigate(['/welcome']); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/home/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | template: ` 5 |

This is not the page you were looking for!

6 | `, 7 | standalone: false, 8 | }) 9 | export class PageNotFoundComponent {} 10 | -------------------------------------------------------------------------------- /src/app/home/shell.component.css: -------------------------------------------------------------------------------- 1 | .main-content { 2 | margin-top: 25px; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/home/shell.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | -------------------------------------------------------------------------------- /src/app/home/shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'pm-shell', 5 | templateUrl: './shell.component.html', 6 | styleUrls: ['./shell.component.css'], 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | standalone: false, 9 | }) 10 | export class ShellComponent implements OnInit { 11 | constructor() {} 12 | 13 | ngOnInit() {} 14 | } 15 | -------------------------------------------------------------------------------- /src/app/home/welcome.component.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 | {{ pageTitle }} 4 |
5 |
6 |
7 |
8 | 13 |
14 |
15 |
16 |
Developed by:
17 |
18 |

Deborah Kurata

19 | 20 |
@deborahkurata
21 | 24 |
25 |
26 |

Duncan Hunter

27 | 28 |
@dunchunter
29 | 32 |
33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /src/app/home/welcome.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './welcome.component.html', 5 | standalone: false, 6 | }) 7 | export class WelcomeComponent { 8 | public pageTitle = 'Welcome'; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/products/product-data.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryDbService } from 'angular-in-memory-web-api'; 2 | 3 | import { Product } from './product'; 4 | 5 | export class ProductData implements InMemoryDbService { 6 | createDb() { 7 | const products: Product[] = [ 8 | { 9 | id: 1, 10 | productName: 'Leaf Rake', 11 | productCode: 'GDN-0011', 12 | description: 'Leaf rake with 48-inch wooden handle', 13 | starRating: 3.2, 14 | }, 15 | { 16 | id: 2, 17 | productName: 'Garden Cart', 18 | productCode: 'GDN-0023', 19 | description: '15 gallon capacity rolling garden cart', 20 | starRating: 4.2, 21 | }, 22 | { 23 | id: 5, 24 | productName: 'Hammer', 25 | productCode: 'TBX-0048', 26 | description: 'Curved claw steel hammer', 27 | starRating: 4.8, 28 | }, 29 | { 30 | id: 8, 31 | productName: 'Saw', 32 | productCode: 'TBX-0022', 33 | description: '15-inch steel blade hand saw', 34 | starRating: 3.7, 35 | }, 36 | { 37 | id: 10, 38 | productName: 'Video Game Controller', 39 | productCode: 'GMG-0042', 40 | description: 'Standard two-button video game controller', 41 | starRating: 4.6, 42 | }, 43 | ]; 44 | return { products }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/products/product-edit/product-edit.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ pageTitle }} 4 |
5 |
6 |
7 |
8 |
9 | 10 | 11 |
12 | 21 | 22 | {{ displayMessage['productName'] }} 23 | 24 |
25 |
26 | 27 |
28 | 29 | 30 |
31 | 40 | 41 | {{ displayMessage['productCode'] }} 42 | 43 |
44 |
45 | 46 |
47 | 50 | 51 |
52 | 60 | 61 | {{ displayMessage['starRating'] }} 62 | 63 |
64 |
65 | 66 |
67 | 68 | 69 |
70 | 78 | 79 | {{ displayMessage['description'] }} 80 | 81 |
82 |
83 | 84 |
85 |
86 | 87 | 95 | 96 | 97 | 105 | 106 | 107 | 115 | 116 |
117 |
118 |
119 |
120 |
121 |
122 |
Error: {{ errorMessage }}
123 | -------------------------------------------------------------------------------- /src/app/products/product-edit/product-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Input, 5 | EventEmitter, 6 | Output, 7 | OnChanges, 8 | SimpleChanges, 9 | } from '@angular/core'; 10 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 11 | 12 | import { Product } from '../product'; 13 | import { GenericValidator } from '../../shared/generic-validator'; 14 | import { NumberValidators } from '../../shared/number.validator'; 15 | 16 | @Component({ 17 | selector: 'pm-product-edit', 18 | templateUrl: './product-edit.component.html', 19 | standalone: false, 20 | }) 21 | export class ProductEditComponent implements OnInit, OnChanges { 22 | pageTitle = 'Product Edit'; 23 | @Input() errorMessage: string; 24 | @Input() selectedProduct: Product; 25 | @Output() create = new EventEmitter(); 26 | @Output() update = new EventEmitter(); 27 | @Output() delete = new EventEmitter(); 28 | @Output() clearCurrent = new EventEmitter(); 29 | 30 | productForm: FormGroup; 31 | 32 | // Use with the generic validation message class 33 | displayMessage: { [key: string]: string } = {}; 34 | private validationMessages: { [key: string]: { [key: string]: string } }; 35 | private genericValidator: GenericValidator; 36 | 37 | constructor(private fb: FormBuilder) { 38 | // Defines all of the validation messages for the form. 39 | // These could instead be retrieved from a file or database. 40 | this.validationMessages = { 41 | productName: { 42 | required: 'Product name is required.', 43 | minlength: 'Product name must be at least three characters.', 44 | maxlength: 'Product name cannot exceed 50 characters.', 45 | }, 46 | productCode: { 47 | required: 'Product code is required.', 48 | }, 49 | starRating: { 50 | range: 'Rate the product between 1 (lowest) and 5 (highest).', 51 | }, 52 | }; 53 | 54 | // Define an instance of the validator for use with this form, 55 | // passing in this form's set of validation messages. 56 | this.genericValidator = new GenericValidator(this.validationMessages); 57 | } 58 | 59 | ngOnInit(): void { 60 | // Define the form group 61 | this.productForm = this.fb.group({ 62 | productName: [ 63 | '', 64 | [Validators.required, Validators.minLength(3), Validators.maxLength(50)], 65 | ], 66 | productCode: ['', Validators.required], 67 | starRating: ['', NumberValidators.range(1, 5)], 68 | description: '', 69 | }); 70 | 71 | // Watch for value changes for validation 72 | this.productForm.valueChanges.subscribe( 73 | () => (this.displayMessage = this.genericValidator.processMessages(this.productForm)) 74 | ); 75 | } 76 | 77 | ngOnChanges(changes: SimpleChanges): void { 78 | // patch form with value from the store 79 | if (changes['selectedProduct']) { 80 | const product = changes['selectedProduct'].currentValue as Product; 81 | this.displayProduct(product); 82 | } 83 | } 84 | 85 | // Also validate on blur 86 | // Helpful if the user tabs through required fields 87 | blur(): void { 88 | this.displayMessage = this.genericValidator.processMessages(this.productForm); 89 | } 90 | 91 | displayProduct(product: Product | null): void { 92 | if (product && this.productForm) { 93 | // Reset the form back to pristine 94 | this.productForm.reset(); 95 | 96 | // Display the appropriate page title 97 | if (product.id === 0) { 98 | this.pageTitle = 'Add Product'; 99 | } else { 100 | this.pageTitle = `Edit Product: ${product.productName}`; 101 | } 102 | 103 | // Update the data on the form 104 | this.productForm.patchValue({ 105 | productName: product.productName, 106 | productCode: product.productCode, 107 | starRating: product.starRating, 108 | description: product.description, 109 | }); 110 | } 111 | } 112 | 113 | cancelEdit(): void { 114 | // Redisplay the currently selected product 115 | // replacing any edits made 116 | this.displayProduct(this.selectedProduct); 117 | } 118 | 119 | deleteProduct(): void { 120 | if (this.selectedProduct && this.selectedProduct.id) { 121 | if (confirm(`Really delete the product: ${this.selectedProduct.productName}?`)) { 122 | this.delete.emit(this.selectedProduct); 123 | } 124 | } else { 125 | // No need to delete, it was never saved 126 | this.clearCurrent.emit(); 127 | } 128 | } 129 | 130 | saveProduct(): void { 131 | if (this.productForm.valid) { 132 | if (this.productForm.dirty) { 133 | // Copy over all of the original product properties 134 | // Then copy over the values from the form 135 | // This ensures values not on the form, such as the Id, are retained 136 | const product = { ...this.selectedProduct, ...this.productForm.value }; 137 | 138 | if (product.id === 0) { 139 | this.create.emit(product); 140 | } else { 141 | this.update.emit(product); 142 | } 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/app/products/product-list/product-list.component.css: -------------------------------------------------------------------------------- 1 | .card-body { 2 | padding: 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/products/product-list/product-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ pageTitle }} 4 |
5 | 6 |
7 |
8 | 17 |
18 |
19 | 20 | 38 |
39 |
Error: {{ errorMessage }}
40 | -------------------------------------------------------------------------------- /src/app/products/product-list/product-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; 2 | import { Product } from '../product'; 3 | 4 | @Component({ 5 | selector: 'pm-product-list', 6 | templateUrl: './product-list.component.html', 7 | styleUrls: ['./product-list.component.css'], 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | standalone: false, 10 | }) 11 | export class ProductListComponent { 12 | pageTitle = 'Products'; 13 | 14 | @Input() errorMessage: string; 15 | @Input() products: Product[]; 16 | @Input() displayCode: boolean; 17 | @Input() selectedProduct: Product | undefined | null; 18 | @Output() displayCodeChanged = new EventEmitter(); 19 | @Output() initializeNewProduct = new EventEmitter(); 20 | @Output() productWasSelected = new EventEmitter(); 21 | 22 | checkChanged(): void { 23 | this.displayCodeChanged.emit(); 24 | } 25 | 26 | newProduct(): void { 27 | this.initializeNewProduct.emit(); 28 | } 29 | 30 | productSelected(product: Product): void { 31 | this.productWasSelected.emit(product); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/products/product-shell/product-shell.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 12 |
13 |
14 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/app/products/product-shell/product-shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Product } from '../product'; 3 | import { ProductStateFacadeService } from '../state/product-state-facade.service'; 4 | 5 | @Component({ 6 | templateUrl: './product-shell.component.html', 7 | standalone: false, 8 | }) 9 | export class ProductShellComponent implements OnInit { 10 | constructor(public productState: ProductStateFacadeService) {} 11 | 12 | ngOnInit(): void { 13 | this.productState.loadProducts(); 14 | } 15 | 16 | checkChanged(): void { 17 | this.productState.toggleProductCode(); 18 | } 19 | 20 | newProduct(): void { 21 | this.productState.initializeCurrentProduct(); 22 | } 23 | 24 | productSelected(product: Product): void { 25 | this.productState.setCurrentProduct(product); 26 | } 27 | 28 | deleteProduct(product: Product): void { 29 | this.productState.deleteProduct(product); 30 | } 31 | 32 | clearProduct(): void { 33 | this.productState.clearCurrentProduct(); 34 | } 35 | 36 | saveProduct(product: Product): void { 37 | this.productState.createProduct(product); 38 | } 39 | 40 | updateProduct(product: Product): void { 41 | this.productState.updateProduct(product); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/products/product.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { SharedModule } from '../shared/shared.module'; 5 | 6 | import { ProductShellComponent } from './product-shell/product-shell.component'; 7 | import { ProductListComponent } from './product-list/product-list.component'; 8 | import { ProductEditComponent } from './product-edit/product-edit.component'; 9 | 10 | import { StoreModule } from '@ngrx/store'; 11 | import { productReducer } from './state/product.reducer'; 12 | import { EffectsModule } from '@ngrx/effects'; 13 | import { ProductEffects } from './state/product.effects'; 14 | 15 | const productRoutes: Routes = [{ path: '', component: ProductShellComponent }]; 16 | 17 | @NgModule({ 18 | imports: [ 19 | SharedModule, 20 | RouterModule.forChild(productRoutes), 21 | StoreModule.forFeature('products', productReducer), 22 | EffectsModule.forFeature([ProductEffects]), 23 | ], 24 | declarations: [ProductShellComponent, ProductListComponent, ProductEditComponent], 25 | }) 26 | export class ProductModule {} 27 | -------------------------------------------------------------------------------- /src/app/products/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | 4 | import { Observable, throwError } from 'rxjs'; 5 | import { catchError, tap, map } from 'rxjs/operators'; 6 | 7 | import { Product } from './product'; 8 | 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class ProductService { 13 | private productsUrl = 'api/products'; 14 | 15 | constructor(private http: HttpClient) {} 16 | 17 | getProducts(): Observable { 18 | return this.http.get(this.productsUrl).pipe( 19 | tap((data) => console.log(JSON.stringify(data))), 20 | catchError(this.handleError) 21 | ); 22 | } 23 | 24 | createProduct(product: Product): Observable { 25 | const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); 26 | // Product Id must be null for the Web API to assign an Id 27 | const newProduct = { ...product, id: null }; 28 | return this.http.post(this.productsUrl, newProduct, { headers }).pipe( 29 | tap((data) => console.log('createProduct: ' + JSON.stringify(data))), 30 | catchError(this.handleError) 31 | ); 32 | } 33 | 34 | deleteProduct(id: number): Observable<{}> { 35 | const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); 36 | const url = `${this.productsUrl}/${id}`; 37 | return this.http.delete(url, { headers }).pipe( 38 | tap((data) => console.log('deleteProduct: ' + id)), 39 | catchError(this.handleError) 40 | ); 41 | } 42 | 43 | updateProduct(product: Product): Observable { 44 | const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); 45 | const url = `${this.productsUrl}/${product.id}`; 46 | return this.http.put(url, product, { headers }).pipe( 47 | tap(() => console.log('updateProduct: ' + product.id)), 48 | // Return the product on an update 49 | map(() => product), 50 | catchError(this.handleError) 51 | ); 52 | } 53 | 54 | private handleError(err: any) { 55 | // in a real world app, we may send the server to some remote logging infrastructure 56 | // instead of just logging it to the console 57 | let errorMessage: string; 58 | if (err.error instanceof ErrorEvent) { 59 | // A client-side or network error occurred. Handle it accordingly. 60 | errorMessage = `An error occurred: ${err.error.message}`; 61 | } else { 62 | // The backend returned an unsuccessful response code. 63 | // The response body may contain clues as to what went wrong, 64 | errorMessage = `Backend returned code ${err.status}: ${err.body.error}`; 65 | } 66 | console.error(err); 67 | return throwError(errorMessage); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/products/product.ts: -------------------------------------------------------------------------------- 1 | /* Defines the product entity */ 2 | export interface Product { 3 | id: number | null; 4 | productName: string; 5 | productCode: string; 6 | description: string; 7 | starRating: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/products/state/actions/index.ts: -------------------------------------------------------------------------------- 1 | import * as ProductPageActions from './product-page.actions'; 2 | import * as ProductApiActions from './product-api.actions'; 3 | 4 | export { ProductPageActions, ProductApiActions }; 5 | -------------------------------------------------------------------------------- /src/app/products/state/actions/product-api.actions.ts: -------------------------------------------------------------------------------- 1 | import { Product } from '../../product'; 2 | 3 | import { createAction, props } from '@ngrx/store'; 4 | 5 | export const loadProductsSuccess = createAction( 6 | '[Product API] Load Success', 7 | props<{ products: Product[] }>() 8 | ); 9 | 10 | export const loadProductsFailure = createAction( 11 | '[Product API] Load Fail', 12 | props<{ error: string }>() 13 | ); 14 | 15 | export const updateProductSuccess = createAction( 16 | '[Product API] Update Product Success', 17 | props<{ product: Product }>() 18 | ); 19 | 20 | export const updateProductFailure = createAction( 21 | '[Product API] Update Product Fail', 22 | props<{ error: string }>() 23 | ); 24 | 25 | export const createProductSuccess = createAction( 26 | '[Product API] Create Product Success', 27 | props<{ product: Product }>() 28 | ); 29 | 30 | export const createProductFailure = createAction( 31 | '[Product API] Create Product Fail', 32 | props<{ error: string }>() 33 | ); 34 | 35 | export const deleteProductSuccess = createAction( 36 | '[Product API] Delete Product Success', 37 | props<{ productId: number }>() 38 | ); 39 | 40 | export const deleteProductFailure = createAction( 41 | '[Product API] Delete Product Fail', 42 | props<{ error: string }>() 43 | ); 44 | -------------------------------------------------------------------------------- /src/app/products/state/actions/product-page.actions.ts: -------------------------------------------------------------------------------- 1 | import { Product } from '../../product'; 2 | 3 | import { createAction, props } from '@ngrx/store'; 4 | 5 | export const toggleProductCode = createAction('[Product Page] Toggle Product Code'); 6 | 7 | export const setCurrentProduct = createAction( 8 | '[Product Page] Set Current Product', 9 | props<{ currentProductId: number }>() 10 | ); 11 | 12 | export const clearCurrentProduct = createAction('[Product Page] Clear Current Product'); 13 | 14 | export const initializeCurrentProduct = createAction('[Product Page] Initialize Current Product'); 15 | 16 | export const loadProducts = createAction('[Product Page] Load'); 17 | 18 | export const updateProduct = createAction( 19 | '[Product Page] Update Product', 20 | props<{ product: Product }>() 21 | ); 22 | 23 | export const createProduct = createAction( 24 | '[Product Page] Create Product', 25 | props<{ product: Product }>() 26 | ); 27 | 28 | export const deleteProduct = createAction( 29 | '[Product Page] Delete Product', 30 | props<{ productId: number }>() 31 | ); 32 | -------------------------------------------------------------------------------- /src/app/products/state/index.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | import { ProductState } from './product.reducer'; 3 | 4 | // Selector functions 5 | const getProductFeatureState = createFeatureSelector('products'); 6 | 7 | export const getShowProductCode = createSelector( 8 | getProductFeatureState, 9 | (state) => state.showProductCode 10 | ); 11 | 12 | export const getCurrentProductId = createSelector( 13 | getProductFeatureState, 14 | (state) => state.currentProductId 15 | ); 16 | 17 | export const getCurrentProduct = createSelector( 18 | getProductFeatureState, 19 | getCurrentProductId, 20 | (state, currentProductId) => { 21 | if (currentProductId === 0) { 22 | return { 23 | id: 0, 24 | productName: '', 25 | productCode: 'New', 26 | description: '', 27 | starRating: 0, 28 | }; 29 | } else { 30 | return currentProductId ? state.products.find((p) => p.id === currentProductId) : null; 31 | } 32 | } 33 | ); 34 | 35 | export const getProducts = createSelector(getProductFeatureState, (state) => state.products); 36 | 37 | export const getError = createSelector(getProductFeatureState, (state) => state.error); 38 | -------------------------------------------------------------------------------- /src/app/products/state/product-state-facade.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs'; 4 | import { Product } from '../product'; 5 | import { getCurrentProduct, getError, getProducts, getShowProductCode } from './index'; 6 | import { ProductPageActions } from './actions'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class ProductStateFacadeService { 12 | displayCode$: Observable = this.store.select(getShowProductCode); 13 | selectedProduct$: Observable = this.store.select(getCurrentProduct); 14 | products$: Observable = this.store.select(getProducts); 15 | errorMessage$: Observable = this.store.select(getError); 16 | 17 | constructor(private store: Store) {} 18 | 19 | loadProducts(): void { 20 | this.store.dispatch(ProductPageActions.loadProducts()); 21 | } 22 | 23 | toggleProductCode(): void { 24 | this.store.dispatch(ProductPageActions.toggleProductCode()); 25 | } 26 | 27 | initializeCurrentProduct(): void { 28 | this.store.dispatch(ProductPageActions.initializeCurrentProduct()); 29 | } 30 | 31 | setCurrentProduct(product: Product): void { 32 | this.store.dispatch( 33 | ProductPageActions.setCurrentProduct({ currentProductId: product.id! }) 34 | ); 35 | } 36 | 37 | deleteProduct(product: Product): void { 38 | this.store.dispatch(ProductPageActions.deleteProduct({ productId: product.id! })); 39 | } 40 | 41 | clearCurrentProduct(): void { 42 | this.store.dispatch(ProductPageActions.clearCurrentProduct()); 43 | } 44 | 45 | createProduct(product: Product): void { 46 | this.store.dispatch(ProductPageActions.createProduct({ product })); 47 | } 48 | 49 | updateProduct(product: Product): void { 50 | this.store.dispatch(ProductPageActions.updateProduct({ product })); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/products/state/product.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { mergeMap, map, catchError, concatMap } from 'rxjs/operators'; 4 | import { of } from 'rxjs'; 5 | import { ProductService } from '../product.service'; 6 | 7 | import { Actions, createEffect, ofType } from '@ngrx/effects'; 8 | import { ProductPageActions, ProductApiActions } from './actions'; 9 | 10 | @Injectable() 11 | export class ProductEffects { 12 | constructor(private actions$: Actions, private productService: ProductService) {} 13 | 14 | loadProducts$ = createEffect(() => { 15 | return this.actions$.pipe( 16 | ofType(ProductPageActions.loadProducts), 17 | mergeMap(() => 18 | this.productService.getProducts().pipe( 19 | map((products) => ProductApiActions.loadProductsSuccess({ products })), 20 | catchError((error) => of(ProductApiActions.loadProductsFailure({ error }))) 21 | ) 22 | ) 23 | ); 24 | }); 25 | 26 | updateProduct$ = createEffect(() => { 27 | return this.actions$.pipe( 28 | ofType(ProductPageActions.updateProduct), 29 | concatMap((action) => 30 | this.productService.updateProduct(action.product).pipe( 31 | map((product) => ProductApiActions.updateProductSuccess({ product })), 32 | catchError((error) => of(ProductApiActions.updateProductFailure({ error }))) 33 | ) 34 | ) 35 | ); 36 | }); 37 | 38 | createProduct$ = createEffect(() => { 39 | return this.actions$.pipe( 40 | ofType(ProductPageActions.createProduct), 41 | concatMap((action) => 42 | this.productService.createProduct(action.product).pipe( 43 | map((product) => ProductApiActions.createProductSuccess({ product })), 44 | catchError((error) => of(ProductApiActions.createProductFailure({ error }))) 45 | ) 46 | ) 47 | ); 48 | }); 49 | 50 | deleteProduct$ = createEffect(() => { 51 | return this.actions$.pipe( 52 | ofType(ProductPageActions.deleteProduct), 53 | mergeMap((action) => 54 | this.productService.deleteProduct(action.productId).pipe( 55 | map(() => 56 | ProductApiActions.deleteProductSuccess({ 57 | productId: action.productId, 58 | }) 59 | ), 60 | catchError((error) => of(ProductApiActions.deleteProductFailure({ error }))) 61 | ) 62 | ) 63 | ); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /src/app/products/state/product.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Product } from '../product'; 2 | 3 | import { createReducer, on } from '@ngrx/store'; 4 | import { ProductApiActions, ProductPageActions } from './actions'; 5 | 6 | // State for this feature (Product) 7 | export interface ProductState { 8 | showProductCode: boolean; 9 | currentProductId: number | null; 10 | products: Product[]; 11 | error: string; 12 | } 13 | 14 | const initialState: ProductState = { 15 | showProductCode: true, 16 | currentProductId: null, 17 | products: [], 18 | error: '', 19 | }; 20 | 21 | export const productReducer = createReducer( 22 | initialState, 23 | on(ProductPageActions.toggleProductCode, (state): ProductState => { 24 | return { 25 | ...state, 26 | showProductCode: !state.showProductCode, 27 | }; 28 | }), 29 | on(ProductPageActions.setCurrentProduct, (state, action): ProductState => { 30 | return { 31 | ...state, 32 | currentProductId: action.currentProductId, 33 | }; 34 | }), 35 | on(ProductPageActions.clearCurrentProduct, (state): ProductState => { 36 | return { 37 | ...state, 38 | currentProductId: null, 39 | }; 40 | }), 41 | on(ProductPageActions.initializeCurrentProduct, (state): ProductState => { 42 | return { 43 | ...state, 44 | currentProductId: 0, 45 | }; 46 | }), 47 | on(ProductApiActions.loadProductsSuccess, (state, action): ProductState => { 48 | return { 49 | ...state, 50 | products: action.products, 51 | error: '', 52 | }; 53 | }), 54 | on(ProductApiActions.loadProductsFailure, (state, action): ProductState => { 55 | return { 56 | ...state, 57 | products: [], 58 | error: action.error, 59 | }; 60 | }), 61 | on(ProductApiActions.updateProductSuccess, (state, action): ProductState => { 62 | const updatedProducts = state.products.map((item) => 63 | action.product.id === item.id ? action.product : item 64 | ); 65 | return { 66 | ...state, 67 | products: updatedProducts, 68 | currentProductId: action.product.id, 69 | error: '', 70 | }; 71 | }), 72 | on(ProductApiActions.updateProductFailure, (state, action): ProductState => { 73 | return { 74 | ...state, 75 | error: action.error, 76 | }; 77 | }), 78 | // After a create, the currentProduct is the new product. 79 | on(ProductApiActions.createProductSuccess, (state, action): ProductState => { 80 | return { 81 | ...state, 82 | products: [...state.products, action.product], 83 | currentProductId: action.product.id, 84 | error: '', 85 | }; 86 | }), 87 | on(ProductApiActions.createProductFailure, (state, action): ProductState => { 88 | return { 89 | ...state, 90 | error: action.error, 91 | }; 92 | }), 93 | // After a delete, the currentProduct is null. 94 | on(ProductApiActions.deleteProductSuccess, (state, action): ProductState => { 95 | return { 96 | ...state, 97 | products: state.products.filter((product) => product.id !== action.productId), 98 | currentProductId: null, 99 | error: '', 100 | }; 101 | }), 102 | on(ProductApiActions.deleteProductFailure, (state, action): ProductState => { 103 | return { 104 | ...state, 105 | error: action.error, 106 | }; 107 | }) 108 | ); 109 | -------------------------------------------------------------------------------- /src/app/shared/generic-validator.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup } from '@angular/forms'; 2 | 3 | // Generic validator for Reactive forms 4 | // Implemented as a class, not a service, so it can retain state for multiple forms. 5 | export class GenericValidator { 6 | // Provide the set of valid validation messages 7 | // Stucture: 8 | // controlName1: { 9 | // validationRuleName1: 'Validation Message.', 10 | // validationRuleName2: 'Validation Message.' 11 | // }, 12 | // controlName2: { 13 | // validationRuleName1: 'Validation Message.', 14 | // validationRuleName2: 'Validation Message.' 15 | // } 16 | constructor(private validationMessages: { [key: string]: { [key: string]: string } }) {} 17 | 18 | // Processes each control within a FormGroup 19 | // And returns a set of validation messages to display 20 | // Structure 21 | // controlName1: 'Validation Message.', 22 | // controlName2: 'Validation Message.' 23 | processMessages(container: FormGroup): { [key: string]: string } { 24 | const messages: Record = {}; 25 | for (const controlKey in container.controls) { 26 | if (container.controls.hasOwnProperty(controlKey)) { 27 | const c = container.controls[controlKey]; 28 | // If it is a FormGroup, process its child controls. 29 | if (c instanceof FormGroup) { 30 | const childMessages = this.processMessages(c); 31 | Object.assign(messages, childMessages); 32 | } else { 33 | // Only validate if there are validation messages for the control 34 | if (this.validationMessages[controlKey]) { 35 | messages[controlKey] = ''; 36 | if ((c.dirty || c.touched) && c.errors) { 37 | Object.keys(c.errors).map((messageKey) => { 38 | if (this.validationMessages[controlKey][messageKey]) { 39 | messages[controlKey] += 40 | this.validationMessages[controlKey][messageKey] + ' '; 41 | } 42 | }); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | return messages; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/shared/number.validator.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, ValidatorFn } from '@angular/forms'; 2 | 3 | export class NumberValidators { 4 | static range(min: number, max: number): ValidatorFn { 5 | return (c: AbstractControl): { [key: string]: boolean } | null => { 6 | if (c.value && (isNaN(c.value) || c.value < min || c.value > max)) { 7 | return { range: true }; 8 | } 9 | return null; 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | 5 | @NgModule({ 6 | imports: [CommonModule], 7 | exports: [CommonModule, FormsModule, ReactiveFormsModule], 8 | }) 9 | export class SharedModule {} 10 | -------------------------------------------------------------------------------- /src/app/user/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; 3 | 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class AuthGuard { 10 | constructor(private authService: AuthService, private router: Router) {} 11 | 12 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { 13 | return this.checkLoggedIn(state.url); 14 | } 15 | 16 | checkLoggedIn(url: string): boolean { 17 | if (this.authService.isLoggedIn()) { 18 | return true; 19 | } 20 | 21 | // Retain the attempted URL for redirection 22 | this.authService.redirectUrl = url; 23 | this.router.navigate(['/login']); 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/user/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { User } from './user'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class AuthService { 9 | currentUser: User | null; 10 | redirectUrl: string; 11 | 12 | constructor() {} 13 | 14 | isLoggedIn(): boolean { 15 | return !!this.currentUser; 16 | } 17 | 18 | login(userName: string, password: string): void { 19 | // Code here would log into a back end service 20 | // and return user information 21 | // This is just hard-coded here. 22 | this.currentUser = { 23 | id: 2, 24 | userName, 25 | isAdmin: false, 26 | }; 27 | } 28 | 29 | logout(): void { 30 | this.currentUser = null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/user/login.component.css: -------------------------------------------------------------------------------- 1 | .main-content { 2 | margin-top: 25px; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/user/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ pageTitle }} 5 |
6 | 7 |
8 |
15 |
16 |
17 | 18 |
19 | 36 |
43 | User name is required. 44 |
45 |
46 |
47 | 48 |
49 | 50 |
51 | 66 |
73 | Password is required. 74 |
75 |
76 |
77 | 78 |
79 |
80 | 81 | 89 | 90 | 91 | Cancel 92 | 93 |
94 |
95 |
96 |
97 |
98 | 99 | 114 |
115 |
116 | -------------------------------------------------------------------------------- /src/app/user/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { AuthService } from './auth.service'; 5 | import { UserStateFacadeService } from './state/user-state-facade.service'; 6 | 7 | @Component({ 8 | templateUrl: './login.component.html', 9 | styleUrls: ['./login.component.css'], 10 | standalone: false, 11 | }) 12 | export class LoginComponent implements OnInit { 13 | pageTitle = 'Log In'; 14 | 15 | constructor( 16 | private authService: AuthService, 17 | public userStateFacade: UserStateFacadeService, 18 | private router: Router 19 | ) {} 20 | 21 | ngOnInit(): void {} 22 | 23 | cancel(): void { 24 | this.router.navigate(['welcome']); 25 | } 26 | 27 | checkChanged(): void { 28 | this.userStateFacade.maskUserName(); 29 | } 30 | 31 | login(loginForm: NgForm): void { 32 | if (loginForm && loginForm.valid) { 33 | const userName = loginForm.form.value.userName; 34 | const password = loginForm.form.value.password; 35 | this.authService.login(userName, password); 36 | 37 | if (this.authService.redirectUrl) { 38 | this.router.navigateByUrl(this.authService.redirectUrl); 39 | } else { 40 | this.router.navigate(['/products']); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/user/state/actions/index.ts: -------------------------------------------------------------------------------- 1 | import * as UserPageActions from './user-page.actions'; 2 | 3 | export { UserPageActions }; 4 | -------------------------------------------------------------------------------- /src/app/user/state/actions/user-page.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@ngrx/store'; 2 | 3 | export const maskUserName = createAction('[User Page] Mask User Name'); 4 | -------------------------------------------------------------------------------- /src/app/user/state/user-state-facade.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Store } from '@ngrx/store'; 4 | import { UserPageActions } from './actions'; 5 | import { getMaskUserName } from './user.reducer'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class UserStateFacadeService { 11 | maskUserName$: Observable = this.store.select(getMaskUserName); 12 | 13 | constructor(private store: Store) {} 14 | 15 | maskUserName(): void { 16 | this.store.dispatch(UserPageActions.maskUserName()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/user/state/user.reducer.ts: -------------------------------------------------------------------------------- 1 | // Homework 2 | import { User } from '../user'; 3 | 4 | import { createReducer, on, createFeatureSelector, createSelector } from '@ngrx/store'; 5 | import { UserPageActions } from './actions'; 6 | 7 | // State for this feature (User) 8 | export interface UserState { 9 | maskUserName: boolean; 10 | currentUser: User | null; 11 | } 12 | 13 | const initialState: UserState = { 14 | maskUserName: true, 15 | currentUser: null, 16 | }; 17 | 18 | // Selector functions 19 | const getUserFeatureState = createFeatureSelector('users'); 20 | 21 | export const getMaskUserName = createSelector(getUserFeatureState, (state) => state.maskUserName); 22 | 23 | export const getCurrentUser = createSelector(getUserFeatureState, (state) => state.currentUser); 24 | 25 | export const userReducer = createReducer( 26 | initialState, 27 | on(UserPageActions.maskUserName, (state): UserState => { 28 | return { 29 | ...state, 30 | maskUserName: !state.maskUserName, 31 | }; 32 | }) 33 | ); 34 | -------------------------------------------------------------------------------- /src/app/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { SharedModule } from '../shared/shared.module'; 5 | 6 | import { LoginComponent } from './login.component'; 7 | 8 | import { StoreModule } from '@ngrx/store'; 9 | import { userReducer } from './state/user.reducer'; 10 | 11 | const userRoutes: Routes = [{ path: 'login', component: LoginComponent }]; 12 | 13 | @NgModule({ 14 | imports: [ 15 | SharedModule, 16 | RouterModule.forChild(userRoutes), 17 | StoreModule.forFeature('users', userReducer), 18 | ], 19 | declarations: [LoginComponent], 20 | }) 21 | export class UserModule {} 22 | -------------------------------------------------------------------------------- /src/app/user/user.ts: -------------------------------------------------------------------------------- 1 | /* Defines the user entity */ 2 | export interface User { 3 | id: number; 4 | userName: string; 5 | isAdmin: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spierala/angular-state-management-comparison/11e3f67f34f5e5c0241f0dd6bb62499e1ee238d1/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spierala/angular-state-management-comparison/11e3f67f34f5e5c0241f0dd6bb62499e1ee238d1/src/assets/images/logo.jpg -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spierala/angular-state-management-comparison/11e3f67f34f5e5c0241f0dd6bb62499e1ee238d1/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Acme Product Management 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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'; 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'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | li { 3 | font-size: large; 4 | } 5 | 6 | div.panel-heading { 7 | font-size: x-large; 8 | } 9 | 10 | body { 11 | margin-top: 50px; 12 | } 13 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 12 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "sourceMap": true, 15 | "declaration": false, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "es2020", 21 | "lib": ["es2020", "dom"], 22 | "strictPropertyInitialization": false, 23 | "useDefineForClassFields": false 24 | }, 25 | "angularCompilerOptions": { 26 | "enableI18nLegacyMessageIdFormat": false, 27 | "strictInjectionParameters": true, 28 | "strictInputAccessModifiers": true, 29 | "strictTemplates": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "files": ["src/test.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | --------------------------------------------------------------------------------