├── .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 |
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 |
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 |
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 |
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 |
6 |
7 |
8 |
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 |
--------------------------------------------------------------------------------