├── .commitlintrc.json
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── ci.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .prettierignore
├── .yarn
├── plugins
│ └── @yarnpkg
│ │ └── plugin-after-install.cjs
└── releases
│ └── yarn-3.2.1.cjs
├── .yarnrc.yml
├── LICENSE
├── README.md
├── apps
├── demos-e2e
│ ├── .eslintrc.json
│ ├── cypress.config.ts
│ ├── project.json
│ ├── src
│ │ ├── e2e
│ │ │ └── ssr.cy.ts
│ │ └── plugins
│ │ │ └── index.js
│ ├── tsconfig.e2e.json
│ └── tsconfig.json
└── demos
│ ├── .browserslistrc
│ ├── .eslintrc.json
│ ├── jest.config.ts
│ ├── project.json
│ ├── src
│ ├── app
│ │ ├── app.component.html
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ ├── app.server.module.ts
│ │ ├── counter.state.ts
│ │ └── counter
│ │ │ ├── counter.component.html
│ │ │ ├── counter.component.ts
│ │ │ └── counter.facade.ts
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── index.html
│ ├── main.server.ts
│ ├── main.ts
│ ├── polyfills.ts
│ ├── server.ts
│ └── test-setup.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.editor.json
│ ├── tsconfig.json
│ ├── tsconfig.server.json
│ └── tsconfig.spec.json
├── decorate-angular-cli.js
├── jest.config.ts
├── jest.preset.js
├── libs
└── dispatch-decorator
│ ├── .eslintrc.json
│ ├── jest.config.ts
│ ├── ng-package.json
│ ├── package.json
│ ├── project.json
│ ├── src
│ ├── index.ts
│ ├── lib
│ │ ├── decorators
│ │ │ └── dispatch.ts
│ │ ├── dispatch.module.ts
│ │ ├── internals
│ │ │ ├── action-completer.ts
│ │ │ ├── decorator-injector-adapter.ts
│ │ │ ├── internals.ts
│ │ │ ├── static-injector.ts
│ │ │ └── unwrap.ts
│ │ └── tests
│ │ │ ├── dispatch.spec.ts
│ │ │ ├── fresh-platform.ts
│ │ │ ├── parallel-modules.spec.ts
│ │ │ └── skip-console-logging.ts
│ └── test-setup.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ └── tsconfig.spec.json
├── nx.json
├── package.json
├── tsconfig.base.json
└── yarn.lock
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 |
10 | [*.md]
11 | max_line_length = off
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": ["**/*"],
4 | "plugins": ["@nrwl/nx"],
5 | "overrides": [
6 | {
7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
8 | "rules": {
9 | "@nrwl/nx/enforce-module-boundaries": [
10 | "error",
11 | {
12 | "enforceBuildableLibDependency": true,
13 | "allow": [],
14 | "depConstraints": [
15 | {
16 | "sourceTag": "*",
17 | "onlyDependOnLibsWithTags": ["*"]
18 | }
19 | ]
20 | }
21 | ]
22 | }
23 | },
24 | {
25 | "files": ["*.ts", "*.tsx"],
26 | "extends": ["plugin:@nrwl/nx/typescript"],
27 | "parserOptions": {
28 | "project": "./tsconfig.*?.json"
29 | },
30 | "rules": {}
31 | },
32 | {
33 | "files": ["*.js", "*.jsx"],
34 | "extends": ["plugin:@nrwl/nx/javascript"],
35 | "rules": {}
36 | }
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | ## I'm submitting a...
8 |
9 |
10 | [ ] Regression (a behavior that used to work and stopped working in a new release)
11 | [ ] Bug report
12 | [ ] Performance issue
13 | [ ] Feature request
14 | [ ] Documentation issue or request
15 | [ ] Support request => https://github.com/ngxs/store/blob/master/CONTRIBUTING.md
16 | [ ] Other... Please describe:
17 |
18 |
19 | ## Current behavior
20 |
21 |
22 |
23 | ## Expected behavior
24 |
25 |
26 |
27 | ## Minimal reproduction of the problem with instructions
28 |
29 |
38 |
39 | ## What is the motivation / use case for changing the behavior?
40 |
41 |
42 |
43 | ## Environment
44 |
45 |
46 | Libs:
47 | - @angular/core version: X.Y.Z
48 | - @ngxs/store version: X.Y.Z
49 |
50 |
51 | Browser:
52 | - [ ] Chrome (desktop) version XX
53 | - [ ] Chrome (Android) version XX
54 | - [ ] Chrome (iOS) version XX
55 | - [ ] Firefox version XX
56 | - [ ] Safari (desktop) version XX
57 | - [ ] Safari (iOS) version XX
58 | - [ ] IE version XX
59 | - [ ] Edge version XX
60 |
61 | For Tooling issues:
62 | - Node version: XX
63 | - Platform:
64 |
65 | Others:
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## PR Checklist
2 | Please check if your PR fulfills the following requirements:
3 |
4 | - [ ] The commit message follows our guidelines: https://github.com/ngxs/store/blob/master/CONTRIBUTING.md#commit
5 | - [ ] Tests for the changes have been added (for bug fixes / features)
6 | - [ ] Docs have been added / updated (for bug fixes / features)
7 |
8 | ## PR Type
9 | What kind of change does this PR introduce?
10 |
11 |
12 | ```
13 | [ ] Bugfix
14 | [ ] Feature
15 | [ ] Code style update (formatting, local variables)
16 | [ ] Refactoring (no functional changes, no api changes)
17 | [ ] Build related changes
18 | [ ] CI related changes
19 | [ ] Documentation content changes
20 | [ ] Other... Please describe:
21 | ```
22 |
23 | ## What is the current behavior?
24 |
25 |
26 | Issue Number: N/A
27 |
28 |
29 | ## What is the new behavior?
30 |
31 |
32 | ## Does this PR introduce a breaking change?
33 | ```
34 | [ ] Yes
35 | [ ] No
36 | ```
37 |
38 |
39 |
40 |
41 | ## Other information
42 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: true
14 |
15 | steps:
16 | - uses: actions/checkout@v3
17 | with:
18 | fetch-depth: 0
19 |
20 | - run: git fetch --no-tags --prune --depth 2 origin master
21 |
22 | - uses: actions/cache@v3
23 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
24 | with:
25 | path: ~/.cache # Default cache directory for both Yarn and Cypress
26 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
27 | restore-keys: |
28 | ${{ runner.os }}-yarn-
29 |
30 | - uses: actions/setup-node@v3
31 | with:
32 | node-version: 16.13.0
33 | registry-url: 'https://registry.npmjs.org'
34 |
35 | - name: Install dependencies
36 | run: yarn --immutable
37 |
38 | - run: yarn nx affected:lint --parallel --base=origin/master
39 | - run: yarn nx affected:test --parallel --base=origin/master
40 | - run: yarn nx affected:build --base=origin/master
41 | - run: yarn nx affected:e2e --base=origin/master
42 | env:
43 | ELECTRON_EXTRA_LAUNCH_ARGS: '--disable-gpu'
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.angular/cache
2 | dist
3 | node_modules
4 | .idea
5 | .vscode
6 | coverage
7 | yarn-error.log
8 | .cache
9 |
10 | migrations.json
11 |
12 | .pnp.*
13 | .yarn/*
14 | !.yarn/patches
15 | !.yarn/plugins
16 | !.yarn/releases
17 | !.yarn/sdks
18 | !.yarn/versions
19 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn lint-staged
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
3 | .gitignore
4 | .prettierignore
5 | .gitkeep
6 | .npmignore
7 | .editorconfig
8 | *.template
9 | .yarn
10 | yarn.lock
11 | yarn-error.log
12 | LICENSE
13 | browserslist
14 | *.ico
15 | dist
16 | .cache
17 | *.snap
18 | assets
19 | .husky
20 |
--------------------------------------------------------------------------------
/.yarn/plugins/@yarnpkg/plugin-after-install.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | //prettier-ignore
3 | module.exports = {
4 | name: "@yarnpkg/plugin-after-install",
5 | factory: function (require) {
6 | var plugin=(()=>{var g=Object.create,r=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var k=Object.getPrototypeOf,y=Object.prototype.hasOwnProperty;var I=t=>r(t,"__esModule",{value:!0});var i=t=>{if(typeof require!="undefined")return require(t);throw new Error('Dynamic require of "'+t+'" is not supported')};var h=(t,o)=>{for(var e in o)r(t,e,{get:o[e],enumerable:!0})},w=(t,o,e)=>{if(o&&typeof o=="object"||typeof o=="function")for(let n of C(o))!y.call(t,n)&&n!=="default"&&r(t,n,{get:()=>o[n],enumerable:!(e=x(o,n))||e.enumerable});return t},a=t=>w(I(r(t!=null?g(k(t)):{},"default",t&&t.__esModule&&"default"in t?{get:()=>t.default,enumerable:!0}:{value:t,enumerable:!0})),t);var j={};h(j,{default:()=>b});var c=a(i("@yarnpkg/core")),m={afterInstall:{description:"Hook that will always run after install",type:c.SettingsType.STRING,default:""}};var u=a(i("clipanion")),d=a(i("@yarnpkg/core"));var p=a(i("@yarnpkg/shell")),l=async(t,o)=>{var f;let e=t.get("afterInstall"),n=!!((f=t.projectCwd)==null?void 0:f.endsWith(`dlx-${process.pid}`));return e&&!n?(o&&console.log("Running `afterInstall` hook..."),(0,p.execute)(e,[],{cwd:t.projectCwd||void 0})):0};var s=class extends u.Command{async execute(){let o=await d.Configuration.find(this.context.cwd,this.context.plugins);return l(o,!1)}};s.paths=[["after-install"]];var P={configuration:m,commands:[s],hooks:{afterAllInstalled:async t=>{if(await l(t.configuration,!0))throw new Error("The `afterInstall` hook failed, see output above.")}}},b=P;return j;})();
7 | return plugin;
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | afterInstall: yarn ngcc && node ./decorate-angular-cli.js && yarn husky install
2 |
3 | nodeLinker: node-modules
4 |
5 | npmRegistryServer: "https://registry.npmjs.org"
6 |
7 | plugins:
8 | - path: .yarn/plugins/@yarnpkg/plugin-after-install.cjs
9 | spec: "https://raw.githubusercontent.com/mhassan1/yarn-plugin-after-install/v0.3.1/bundles/@yarnpkg/plugin-after-install.js"
10 |
11 | yarnPath: .yarn/releases/yarn-3.2.1.cjs
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 NGXS
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ---
6 |
7 | > The distribution for separation of concern between the state management and the view
8 |
9 | [](https://www.npmjs.com/package/@ngxs-labs/dispatch-decorator)
10 | [](https://github.com/ngxs-labs/dispatch-decorator/blob/master/LICENSE)
11 |
12 | This package simplifies the dispatching process. It would be best if you didn't care about the `Store` service injection, as we provide a more declarative way to dispatch events out of the box.
13 |
14 | ## Compatibility with Angular Versions
15 |
16 |
17 |
18 |
19 | @ngxs-labs/dispatch-decorator
20 | Angular
21 |
22 |
23 |
24 |
25 |
26 | 4.x
27 |
28 |
29 | >= 13 < 15
30 |
31 |
32 |
33 |
34 | 5.x
35 |
36 |
37 | >= 15
38 |
39 |
40 |
41 |
42 |
43 | ## 📦 Install
44 |
45 | To install the `@ngxs-labs/dispatch-decorator`, run the following command:
46 |
47 | ```sh
48 | $ npm install @ngxs-labs/dispatch-decorator
49 | # Or if you're using yarn
50 | $ yarn add @ngxs-labs/dispatch-decorator
51 | # Or if you're using pnpm
52 | $ pnpm install @ngxs-labs/dispatch-decorator
53 | ```
54 |
55 | ## 🔨 Usage
56 |
57 | Import the module into your root application module:
58 |
59 | ```ts
60 | import { NgModule } from '@angular/core';
61 | import { NgxsModule } from '@ngxs/store';
62 | import { NgxsDispatchPluginModule } from '@ngxs-labs/dispatch-decorator';
63 |
64 | @NgModule({
65 | imports: [NgxsModule.forRoot(states), NgxsDispatchPluginModule.forRoot()]
66 | })
67 | export class AppModule {}
68 | ```
69 |
70 | ### Dispatch Decorator
71 |
72 | `@Dispatch()` can be used to decorate methods and properties of your classes. Firstly let's create our state for demonstrating purposes:
73 |
74 | ```ts
75 | import { State, Action, StateContext } from '@ngxs/store';
76 |
77 | export class Increment {
78 | static readonly type = '[Counter] Increment';
79 | }
80 |
81 | export class Decrement {
82 | static readonly type = '[Counter] Decrement';
83 | }
84 |
85 | @State({
86 | name: 'counter',
87 | defaults: 0
88 | })
89 | export class CounterState {
90 | @Action(Increment)
91 | increment(ctx: StateContext) {
92 | ctx.setState(ctx.getState() + 1);
93 | }
94 |
95 | @Action(Decrement)
96 | decrement(ctx: StateContext) {
97 | ctx.setState(ctx.getState() - 1);
98 | }
99 | }
100 | ```
101 |
102 | We are ready to try the plugin after registering our state in the `NgxsModule`, given the following component:
103 |
104 | ```ts
105 | import { Component } from '@angular/core';
106 | import { Select } from '@ngxs/store';
107 | import { Dispatch } from '@ngxs-labs/dispatch-decorator';
108 |
109 | import { Observable } from 'rxjs';
110 |
111 | import { CounterState, Increment, Decrement } from './counter.state';
112 |
113 | @Component({
114 | selector: 'app-root',
115 | template: `
116 |
117 | {{ counter }}
118 |
119 |
120 | Increment
121 | Decrement
122 | `
123 | })
124 | export class AppComponent {
125 | @Select(CounterState) counter$: Observable;
126 |
127 | @Dispatch() increment = () => new Increment();
128 |
129 | @Dispatch() decrement = () => new Decrement();
130 | }
131 | ```
132 |
133 | You may mention that we don't have to inject the `Store` class to dispatch actions. The `@Dispatch` decorator takes care of delivering actions internally. It unwraps the result of function calls and calls `store.dispatch(...)`.
134 |
135 | Dispatch function can be both synchronous and asynchronous, meaning that the `@Dispatch` decorator can unwrap `Promise` and `Observable`. Dispatch functions are called outside of the Angular zone, which means asynchronous tasks won't notify Angular about change detection forced to be run:
136 |
137 | ```ts
138 | export class AppComponent {
139 | // `ApiService` is defined somewhere
140 | constructor(private api: ApiService) {}
141 |
142 | @Dispatch()
143 | async setAppSchema() {
144 | const version = await this.api.getApiVersion();
145 | const schema = await this.api.getSchemaForVersion(version);
146 | return new SetAppSchema(schema);
147 | }
148 |
149 | // OR using lambda
150 |
151 | @Dispatch() setAppSchema = () =>
152 | this.api.getApiVersion().pipe(
153 | mergeMap(version => this.api.getSchemaForVersion(version)),
154 | map(schema => new SetAppSchema(schema))
155 | );
156 | }
157 | ```
158 |
159 | Note it doesn't if an arrow function or a regular class method is used.
160 |
161 | ### Dispatching Multiple Actions
162 |
163 | `@Dispatch` function can return arrays of actions:
164 |
165 | ```ts
166 | export class AppComponent {
167 | @Dispatch() setLanguageAndNavigateHome = (language: string) => [
168 | new SetLanguage(language),
169 | new Navigate('/')
170 | ];
171 | }
172 | ```
173 |
174 | ### Canceling
175 |
176 | `@Dispatch` functions can cancel currently running actions if they're called again in the middle of running actions. This is useful for canceling previous requests like in a typeahead. Given the following example:
177 |
178 | ```ts
179 | @Component({ ... })
180 | export class NovelsComponent {
181 | @Dispatch() searchNovels = (query: string) =>
182 | this.novelsService.getNovels(query).pipe(map(novels => new SetNovels(novels)));
183 |
184 | constructor(private novelsService: NovelsService) {}
185 | }
186 | ```
187 |
188 | We have to provide the `cancelUncompleted` option if we'd want to cancel previously uncompleted `getNovels` action:
189 |
190 | ```ts
191 | @Component({ ... })
192 | export class NovelsComponent {
193 | @Dispatch({ cancelUncompleted: true }) searchNovels = (query: string) =>
194 | this.novelsService.getNovels(query).pipe(map(novels => new SetNovels(novels)));
195 |
196 | constructor(private novelsService: NovelsService) {}
197 | }
198 | ```
199 |
--------------------------------------------------------------------------------
/apps/demos-e2e/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "rules": {},
5 | "overrides": [
6 | {
7 | "files": ["src/plugins/index.js"],
8 | "rules": {
9 | "@typescript-eslint/no-var-requires": "off",
10 | "no-undef": "off"
11 | }
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/apps/demos-e2e/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress';
2 |
3 | export default defineConfig({
4 | video: false,
5 | chromeWebSecurity: false,
6 | fileServerFolder: '.',
7 | screenshotOnRunFailure: false,
8 | e2e: {
9 | supportFile: false,
10 | fixturesFolder: false,
11 | specPattern: './src/e2e/**/*.cy.ts'
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/apps/demos-e2e/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demos-e2e",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "projectType": "application",
5 | "sourceRoot": "apps/demos-e2e/src",
6 | "targets": {
7 | "e2e": {
8 | "executor": "@nrwl/cypress:cypress",
9 | "options": {
10 | "cypressConfig": "apps/demos-e2e/cypress.config.ts",
11 | "tsConfig": "apps/demos-e2e/tsconfig.e2e.json",
12 | "devServerTarget": "demos:serve-ssr:production"
13 | }
14 | },
15 | "lint": {
16 | "executor": "@nrwl/linter:eslint",
17 | "options": {
18 | "lintFilePatterns": ["apps/demos-e2e/**/*.{js,ts}"]
19 | }
20 | }
21 | },
22 | "implicitDependencies": ["demos"]
23 | }
24 |
--------------------------------------------------------------------------------
/apps/demos-e2e/src/e2e/ssr.cy.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe('Server side rendering', () => {
4 | const indexUrl = '/';
5 |
6 | it('should make concurrent requests and app should render correctly for each request', async () => {
7 | const promises: Promise[] = Array.from({ length: 50 }).map(() =>
8 | fetch('/').then(res => res.text())
9 | );
10 |
11 | const bodies = await Promise.all(promises);
12 |
13 | bodies.forEach(body => {
14 | expect(body).to.contain('Toggle counter component');
15 | });
16 | });
17 |
18 | it('successfully render index page', () => {
19 | // Arrange & act & assert
20 | cy.request(indexUrl).its('body').should('contain', 'Counter is 0');
21 | });
22 |
23 | it('should increment and decrement after button clicks', () => {
24 | // Arrange & act & assert
25 | cy.visit(indexUrl)
26 | .get('button.increment')
27 | // Increment 5 times
28 | .click()
29 | .click()
30 | .click()
31 | .click()
32 | .click()
33 | .get('button.decrement')
34 | // Decrement 3 times
35 | .click()
36 | .click()
37 | .click()
38 | .get('h1.counter')
39 | .should('contain', 'Counter is 2');
40 | });
41 |
42 | it('should increment but cancel previously uncompleted async job', () => {
43 | // Arrange & act & assert
44 | // eslint-disable-next-line cypress/no-unnecessary-waiting
45 | cy.visit(indexUrl)
46 | .get('button.increment-async')
47 | // Try to increment 3 times
48 | .click()
49 | .click()
50 | .click()
51 | .wait(600)
52 | .get('h1.counter')
53 | .should('contain', 'Counter is 1');
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/apps/demos-e2e/src/plugins/index.js:
--------------------------------------------------------------------------------
1 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor');
2 |
3 | module.exports = (on, config) => {
4 | on('file:preprocessor', preprocessTypescript(config));
5 | };
6 |
--------------------------------------------------------------------------------
/apps/demos-e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "sourceMap": false,
6 | "skipLibCheck": true,
7 | "types": ["cypress", "node"]
8 | },
9 | "include": ["src/**/*.ts", "src/**/*.js"]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/demos-e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.e2e.json"
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/demos/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | last 1 Chrome version
9 |
--------------------------------------------------------------------------------
/apps/demos/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts"],
7 | "extends": [
8 | "plugin:@nrwl/nx/angular",
9 | "plugin:@angular-eslint/template/process-inline-templates"
10 | ],
11 | "rules": {
12 | "@angular-eslint/directive-selector": [
13 | "error",
14 | {
15 | "type": "attribute",
16 | "prefix": "app",
17 | "style": "camelCase"
18 | }
19 | ],
20 | "@angular-eslint/component-selector": [
21 | "error",
22 | {
23 | "type": "element",
24 | "prefix": "app",
25 | "style": "kebab-case"
26 | }
27 | ]
28 | }
29 | },
30 | {
31 | "files": ["*.html"],
32 | "extends": ["plugin:@nrwl/nx/angular-template"],
33 | "rules": {}
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/apps/demos/jest.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export default {
3 | displayName: 'demos',
4 | preset: '../../jest.preset.js',
5 | setupFilesAfterEnv: ['/src/test-setup.ts'],
6 | transform: {
7 | '^.+\\.(ts|mjs|js|html)$': [
8 | 'jest-preset-angular',
9 | {
10 | isolatedModules: true,
11 | tsconfig: '/tsconfig.spec.json',
12 | stringifyContentPathRegex: '\\.(html|svg)$'
13 | }
14 | ]
15 | },
16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
17 | snapshotSerializers: [
18 | 'jest-preset-angular/build/serializers/no-ng-attributes',
19 | 'jest-preset-angular/build/serializers/ng-snapshot',
20 | 'jest-preset-angular/build/serializers/html-comment'
21 | ]
22 | };
23 |
--------------------------------------------------------------------------------
/apps/demos/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demos",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "projectType": "application",
5 | "sourceRoot": "apps/demos/src",
6 | "prefix": "app",
7 | "targets": {
8 | "build": {
9 | "executor": "@angular-devkit/build-angular:browser",
10 | "options": {
11 | "outputPath": "dist/apps/demos/browser",
12 | "index": "apps/demos/src/index.html",
13 | "main": "apps/demos/src/main.ts",
14 | "polyfills": "apps/demos/src/polyfills.ts",
15 | "tsConfig": "apps/demos/tsconfig.app.json",
16 | "styles": [],
17 | "scripts": [],
18 | "vendorChunk": true,
19 | "extractLicenses": false,
20 | "buildOptimizer": false,
21 | "sourceMap": true,
22 | "optimization": false,
23 | "namedChunks": true,
24 | "assets": ["apps/demos/src/favicon.ico", "apps/demos/src/assets"]
25 | },
26 | "configurations": {
27 | "production": {
28 | "fileReplacements": [
29 | {
30 | "replace": "apps/demos/src/environments/environment.ts",
31 | "with": "apps/demos/src/environments/environment.prod.ts"
32 | }
33 | ],
34 | "optimization": true,
35 | "outputHashing": "all",
36 | "sourceMap": false,
37 | "namedChunks": false,
38 | "extractLicenses": true,
39 | "vendorChunk": false,
40 | "buildOptimizer": true,
41 | "budgets": [
42 | {
43 | "type": "initial",
44 | "maximumWarning": "2mb",
45 | "maximumError": "5mb"
46 | },
47 | {
48 | "type": "anyComponentStyle",
49 | "maximumWarning": "6kb",
50 | "maximumError": "10kb"
51 | }
52 | ]
53 | }
54 | }
55 | },
56 | "serve": {
57 | "executor": "@angular-devkit/build-angular:dev-server",
58 | "options": {
59 | "browserTarget": "demos:build"
60 | },
61 | "configurations": {
62 | "production": {
63 | "browserTarget": "demos:build:production"
64 | }
65 | }
66 | },
67 | "lint": {
68 | "executor": "@nrwl/linter:eslint",
69 | "options": {
70 | "lintFilePatterns": ["apps/demos/src/**/*.ts", "apps/demos/src/**/*.html"]
71 | },
72 | "outputs": ["{options.outputFile}"]
73 | },
74 | "test": {
75 | "executor": "@nrwl/jest:jest",
76 | "outputs": ["{workspaceRoot}/coverage/apps/demos"],
77 | "options": {
78 | "jestConfig": "apps/demos/jest.config.ts",
79 | "passWithNoTests": true
80 | }
81 | },
82 | "server": {
83 | "executor": "@angular-devkit/build-angular:server",
84 | "options": {
85 | "outputPath": "dist/apps/demos/server",
86 | "main": "apps/demos/src/server.ts",
87 | "tsConfig": "apps/demos/tsconfig.server.json",
88 | "sourceMap": true,
89 | "optimization": false
90 | },
91 | "configurations": {
92 | "production": {
93 | "sourceMap": false,
94 | "optimization": true,
95 | "outputHashing": "media",
96 | "fileReplacements": [
97 | {
98 | "replace": "apps/demos/src/environments/environment.ts",
99 | "with": "apps/demos/src/environments/environment.prod.ts"
100 | }
101 | ]
102 | }
103 | }
104 | },
105 | "serve-ssr": {
106 | "executor": "@nguniversal/builders:ssr-dev-server",
107 | "options": {
108 | "browserTarget": "demos:build",
109 | "serverTarget": "demos:server"
110 | },
111 | "configurations": {
112 | "production": {
113 | "browserTarget": "demos:build:production",
114 | "serverTarget": "demos:server:production"
115 | }
116 | }
117 | }
118 | },
119 | "type": ["app"]
120 | }
121 |
--------------------------------------------------------------------------------
/apps/demos/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 | Toggle counter component
3 |
--------------------------------------------------------------------------------
/apps/demos/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectionStrategy } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | changeDetection: ChangeDetectionStrategy.OnPush
7 | })
8 | export class AppComponent {
9 | counterComponentShown = true;
10 | }
11 |
--------------------------------------------------------------------------------
/apps/demos/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { HttpClientModule } from '@angular/common/http';
4 | import { NgxsModule } from '@ngxs/store';
5 | import { NgxsDispatchPluginModule } from '@ngxs-labs/dispatch-decorator';
6 |
7 | import { CounterState } from './counter.state';
8 |
9 | import { AppComponent } from './app.component';
10 | import { CounterComponent } from './counter/counter.component';
11 |
12 | import { environment } from '../environments/environment';
13 |
14 | @NgModule({
15 | imports: [
16 | BrowserModule.withServerTransition({ appId: 'dispatch-decorator' }),
17 | HttpClientModule,
18 | NgxsModule.forRoot([CounterState], { developmentMode: !environment.production }),
19 | NgxsDispatchPluginModule.forRoot()
20 | ],
21 | declarations: [AppComponent, CounterComponent],
22 | bootstrap: [AppComponent]
23 | })
24 | export class AppModule {}
25 |
--------------------------------------------------------------------------------
/apps/demos/src/app/app.server.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { ServerModule } from '@angular/platform-server';
3 |
4 | import { AppModule } from './app.module';
5 | import { AppComponent } from './app.component';
6 |
7 | @NgModule({
8 | imports: [AppModule, ServerModule],
9 | bootstrap: [AppComponent]
10 | })
11 | export class AppServerModule {}
12 |
--------------------------------------------------------------------------------
/apps/demos/src/app/counter.state.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { State, Action, StateContext } from '@ngxs/store';
3 |
4 | export interface CounterStateModel {
5 | counter: number;
6 | }
7 |
8 | export class Increment {
9 | static readonly type = '[Counter] Increment';
10 | }
11 |
12 | export class Decrement {
13 | static readonly type = '[Counter] Decrement';
14 | }
15 |
16 | @State({
17 | name: 'counter',
18 | defaults: {
19 | counter: 0
20 | }
21 | })
22 | @Injectable()
23 | export class CounterState {
24 | @Action(Increment)
25 | increment(ctx: StateContext) {
26 | const counter = ctx.getState().counter + 1;
27 | ctx.setState({ counter });
28 | }
29 |
30 | @Action(Decrement)
31 | decrement(ctx: StateContext) {
32 | const counter = ctx.getState().counter - 1;
33 | ctx.setState({ counter });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/apps/demos/src/app/counter/counter.component.html:
--------------------------------------------------------------------------------
1 |
2 | Counter state is {{ state | json }}
3 | Counter is {{ state.counter }}
4 |
5 |
6 | Increment via facade
7 | Decrement via facade
8 | Increment async via facade
9 |
--------------------------------------------------------------------------------
/apps/demos/src/app/counter/counter.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectionStrategy } from '@angular/core';
2 | import { Select } from '@ngxs/store';
3 |
4 | import { Observable } from 'rxjs';
5 |
6 | import { CounterFacade } from './counter.facade';
7 | import { CounterState, CounterStateModel } from '../counter.state';
8 |
9 | @Component({
10 | selector: 'app-counter',
11 | templateUrl: './counter.component.html',
12 | changeDetection: ChangeDetectionStrategy.OnPush
13 | })
14 | export class CounterComponent {
15 | @Select(CounterState) counter$!: Observable;
16 |
17 | constructor(private counterFacade: CounterFacade) {}
18 |
19 | increment(): void {
20 | this.counterFacade.increment();
21 | }
22 |
23 | decrement(): void {
24 | this.counterFacade.decrement();
25 | }
26 |
27 | incrementAsync(): void {
28 | this.counterFacade.incrementAsync();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/apps/demos/src/app/counter/counter.facade.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Dispatch } from '@ngxs-labs/dispatch-decorator';
3 |
4 | import { timer } from 'rxjs';
5 | import { mapTo } from 'rxjs/operators';
6 |
7 | import { Increment, Decrement } from '../counter.state';
8 |
9 | @Injectable({ providedIn: 'root' })
10 | export class CounterFacade {
11 | @Dispatch() increment = () => new Increment();
12 |
13 | @Dispatch() decrement = () => new Decrement();
14 |
15 | @Dispatch({ cancelUncompleted: true }) incrementAsync = () =>
16 | timer(500).pipe(mapTo(new Increment()));
17 | }
18 |
--------------------------------------------------------------------------------
/apps/demos/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/apps/demos/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: false
3 | };
4 |
--------------------------------------------------------------------------------
/apps/demos/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Angular Universal @ngxs-labs/package integration
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/apps/demos/src/main.server.ts:
--------------------------------------------------------------------------------
1 | import '@angular/platform-server/init';
2 |
3 | import { enableProdMode } from '@angular/core';
4 |
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | export { AppServerModule } from './app/app.server.module';
12 | export { renderModule, renderModuleFactory } from '@angular/platform-server';
13 |
--------------------------------------------------------------------------------
/apps/demos/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 | document.addEventListener('DOMContentLoaded', () => {
12 | platformBrowserDynamic().bootstrapModule(AppModule);
13 | });
14 |
--------------------------------------------------------------------------------
/apps/demos/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js';
2 |
--------------------------------------------------------------------------------
/apps/demos/src/server.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js/node';
2 |
3 | import { join } from 'path';
4 | import * as express from 'express';
5 |
6 | import { APP_BASE_HREF } from '@angular/common';
7 | import { ngExpressEngine } from '@nguniversal/express-engine';
8 |
9 | import { AppServerModule } from './main.server';
10 |
11 | export function app(): express.Express {
12 | const server = express();
13 | const distFolder = join(process.cwd(), 'dist/apps/demos/browser');
14 |
15 | server.engine(
16 | 'html',
17 | ngExpressEngine({
18 | bootstrap: AppServerModule
19 | })
20 | );
21 |
22 | server.set('view engine', 'html');
23 | server.set('views', distFolder);
24 |
25 | server.get(
26 | '*.*',
27 | express.static(distFolder, {
28 | maxAge: '1y'
29 | })
30 | );
31 |
32 | server.get('*', (req, res) => {
33 | res.render('index', { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
34 | });
35 |
36 | return server;
37 | }
38 |
39 | function run(): void {
40 | const port = process.env.PORT || 4200;
41 |
42 | const server = app();
43 | server.listen(port, () => {
44 | console.log(`Node Express server listening on http://localhost:${port}`);
45 | });
46 | }
47 |
48 | // Webpack will replace 'require' with '__webpack_require__'
49 | // '__non_webpack_require__' is a proxy to Node 'require'
50 | // The below code is to ensure that the server is run only when not requiring the bundle.
51 | declare const __non_webpack_require__: NodeRequire;
52 | const mainModule = __non_webpack_require__.main;
53 | const moduleFilename = (mainModule && mainModule.filename) || '';
54 | if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
55 | run();
56 | }
57 |
58 | export * from './main.server';
59 |
--------------------------------------------------------------------------------
/apps/demos/src/test-setup.ts:
--------------------------------------------------------------------------------
1 | import 'jest-preset-angular/setup-jest';
2 |
--------------------------------------------------------------------------------
/apps/demos/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "types": ["node"],
5 | "target": "es2022",
6 | "useDefineForClassFields": false
7 | },
8 | "files": ["src/main.ts", "src/polyfills.ts"],
9 | "include": ["src/**/*.d.ts"],
10 | "exclude": ["**/*.test.ts", "**/*.spec.ts", "jest.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/apps/demos/tsconfig.editor.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["**/*.ts"],
4 | "compilerOptions": {
5 | "types": ["jest", "node"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/apps/demos/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.app.json"
8 | },
9 | {
10 | "path": "./tsconfig.spec.json"
11 | },
12 | {
13 | "path": "./tsconfig.editor.json"
14 | },
15 | {
16 | "path": "./tsconfig.server.json"
17 | }
18 | ],
19 | "compilerOptions": {
20 | "forceConsistentCasingInFileNames": true,
21 | "strict": true,
22 | "noImplicitOverride": true,
23 | "noPropertyAccessFromIndexSignature": false,
24 | "noImplicitReturns": true,
25 | "noFallthroughCasesInSwitch": true,
26 | "target": "es2020"
27 | },
28 | "angularCompilerOptions": {
29 | "strictInjectionParameters": true,
30 | "strictInputAccessModifiers": true,
31 | "strictTemplates": true
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/apps/demos/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.app.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "types": ["node"]
6 | },
7 | "files": ["src/main.server.ts", "src/server.ts"],
8 | "angularCompilerOptions": {
9 | "entryModule": "./src/app/app.server.module#AppServerModule"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/demos/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "types": ["jest", "node"]
6 | },
7 | "files": ["src/test-setup.ts"],
8 | "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/decorate-angular-cli.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching
3 | * and faster execution of tasks.
4 | *
5 | * It does this by:
6 | *
7 | * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command.
8 | * - Symlinking the ng to nx command, so all commands run through the Nx CLI
9 | * - Updating the package.json postinstall script to give you control over this script
10 | *
11 | * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it.
12 | * Every command you run should work the same when using the Nx CLI, except faster.
13 | *
14 | * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case,
15 | * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked.
16 | * The Nx CLI simply does some optimizations before invoking the Angular CLI.
17 | *
18 | * To opt out of this patch:
19 | * - Replace occurrences of nx with ng in your package.json
20 | * - Remove the script from your postinstall script in your package.json
21 | * - Delete and reinstall your node_modules
22 | */
23 |
24 | const fs = require('fs');
25 | const os = require('os');
26 | const cp = require('child_process');
27 | const isWindows = os.platform() === 'win32';
28 | let output;
29 | try {
30 | output = require('@nrwl/workspace').output;
31 | } catch (e) {
32 | console.warn(
33 | 'Angular CLI could not be decorated to enable computation caching. Please ensure @nrwl/workspace is installed.'
34 | );
35 | process.exit(0);
36 | }
37 |
38 | /**
39 | * Paths to files being patched
40 | */
41 | const angularCLIInitPath = 'node_modules/@angular/cli/lib/cli/index.js';
42 |
43 | /**
44 | * Patch index.js to warn you if you invoke the undecorated Angular CLI.
45 | */
46 | function patchAngularCLI(initPath) {
47 | const angularCLIInit = fs.readFileSync(initPath, 'utf-8').toString();
48 |
49 | if (!angularCLIInit.includes('NX_CLI_SET')) {
50 | fs.writeFileSync(
51 | initPath,
52 | `
53 | if (!process.env['NX_CLI_SET']) {
54 | const { output } = require('@nrwl/workspace');
55 | output.warn({ title: 'The Angular CLI was invoked instead of the Nx CLI. Use "npx ng [command]" or "nx [command]" instead.' });
56 | }
57 | ${angularCLIInit}
58 | `
59 | );
60 | }
61 | }
62 |
63 | /**
64 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still
65 | * invoke the Nx CLI and get the benefits of computation caching.
66 | */
67 | function symlinkNgCLItoNxCLI() {
68 | try {
69 | const ngPath = './node_modules/.bin/ng';
70 | const nxPath = './node_modules/.bin/nx';
71 | if (isWindows) {
72 | /**
73 | * This is the most reliable way to create symlink-like behavior on Windows.
74 | * Such that it works in all shells and works with npx.
75 | */
76 | ['', '.cmd', '.ps1'].forEach(ext => {
77 | if (fs.existsSync(nxPath + ext))
78 | fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext));
79 | });
80 | } else {
81 | // If unix-based, symlink
82 | cp.execSync(`ln -sf ./nx ${ngPath}`);
83 | }
84 | } catch (e) {
85 | output.error({
86 | title: 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + e.message
87 | });
88 | throw e;
89 | }
90 | }
91 |
92 | try {
93 | symlinkNgCLItoNxCLI();
94 | patchAngularCLI(angularCLIInitPath);
95 | output.log({
96 | title: 'Angular CLI has been decorated to enable computation caching.'
97 | });
98 | } catch (e) {
99 | output.error({
100 | title: 'Decoration of the Angular CLI did not complete successfully'
101 | });
102 | }
103 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | const { getJestProjects } = require('@nrwl/jest');
2 |
3 | export default {
4 | projects: getJestProjects()
5 | };
6 |
--------------------------------------------------------------------------------
/jest.preset.js:
--------------------------------------------------------------------------------
1 | const nxPreset = require('@nrwl/jest/preset').default;
2 |
3 | module.exports = {
4 | ...nxPreset,
5 | testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
6 | transform: {
7 | '^.+\\.(ts|js|html)$': 'ts-jest'
8 | },
9 | resolver: '@nrwl/jest/plugins/resolver',
10 | moduleFileExtensions: ['ts', 'js', 'html'],
11 | coverageReporters: ['html', 'lcov'],
12 | /* TODO: Update to latest Jest snapshotFormat
13 | * By default Nx has kept the older style of Jest Snapshot formats
14 | * to prevent breaking of any existing tests with snapshots.
15 | * It's recommend you update to the latest format.
16 | * You can do this by removing snapshotFormat property
17 | * and running tests with --update-snapshot flag.
18 | * Example: "nx affected --targets=test --update-snapshot"
19 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
20 | */
21 | snapshotFormat: { escapeString: true, printBasicPrototype: true }
22 | };
23 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts"],
7 | "extends": [
8 | "plugin:@nrwl/nx/angular",
9 | "plugin:@angular-eslint/template/process-inline-templates"
10 | ],
11 | "rules": {
12 | "@angular-eslint/no-output-native": ["off"],
13 | "@typescript-eslint/no-non-null-assertion": ["off"],
14 | "@nrwl/nx/enforce-module-boundaries": [
15 | "error",
16 | {
17 | "allow": []
18 | }
19 | ]
20 | }
21 | },
22 | {
23 | "files": ["*.html"],
24 | "extends": ["plugin:@nrwl/nx/angular-template"],
25 | "rules": {}
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/jest.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export default {
3 | displayName: 'dispatch-decorator',
4 | preset: '../../jest.preset.js',
5 | setupFilesAfterEnv: ['/src/test-setup.ts'],
6 | transform: {
7 | '^.+\\.(ts|mjs|js|html)$': [
8 | 'jest-preset-angular',
9 | {
10 | isolatedModules: true,
11 | tsconfig: '/tsconfig.spec.json',
12 | stringifyContentPathRegex: '\\.(html|svg)$'
13 | }
14 | ]
15 | },
16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
17 | snapshotSerializers: [
18 | 'jest-preset-angular/build/serializers/no-ng-attributes',
19 | 'jest-preset-angular/build/serializers/ng-snapshot',
20 | 'jest-preset-angular/build/serializers/html-comment'
21 | ]
22 | };
23 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/libs/dispatch-decorator",
4 | "lib": {
5 | "entryFile": "src/index.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ngxs-labs/dispatch-decorator",
3 | "version": "5.0.0",
4 | "repository": {
5 | "type": "git",
6 | "url": "git+https://github.com/ngxs-labs/dispatch-decorator.git"
7 | },
8 | "license": "MIT",
9 | "homepage": "https://github.com/ngxs-labs/dispatch-decorator#readme",
10 | "bugs": {
11 | "url": "https://github.com/ngxs-labs/dispatch-decorator/issues"
12 | },
13 | "keywords": [
14 | "ngxs",
15 | "redux",
16 | "store"
17 | ],
18 | "sideEffects": false,
19 | "peerDependencies": {
20 | "@angular/core": ">=15.0.0",
21 | "@ngxs/store": ">=3.7.2",
22 | "rxjs": ">=6.1.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dispatch-decorator",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "projectType": "library",
5 | "sourceRoot": "libs/dispatch-decorator/src",
6 | "targets": {
7 | "build": {
8 | "executor": "@nrwl/angular:package",
9 | "options": {
10 | "tsConfig": "libs/dispatch-decorator/tsconfig.lib.json",
11 | "project": "libs/dispatch-decorator/ng-package.json"
12 | }
13 | },
14 | "lint": {
15 | "executor": "@nrwl/linter:eslint",
16 | "options": {
17 | "lintFilePatterns": [
18 | "libs/dispatch-decorator/src/**/*.ts",
19 | "libs/dispatch-decorator/src/**/*.html"
20 | ]
21 | },
22 | "outputs": ["{options.outputFile}"]
23 | },
24 | "test": {
25 | "executor": "@nrwl/jest:jest",
26 | "options": {
27 | "jestConfig": "libs/dispatch-decorator/jest.config.ts",
28 | "passWithNoTests": true
29 | }
30 | }
31 | },
32 | "type": ["lib"]
33 | }
34 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Dispatch } from './lib/decorators/dispatch';
2 | export { NgxsDispatchPluginModule } from './lib/dispatch.module';
3 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/lib/decorators/dispatch.ts:
--------------------------------------------------------------------------------
1 | import { NgZone } from '@angular/core';
2 | import { Store } from '@ngxs/store';
3 |
4 | import { unwrapAndDispatch } from '../internals/unwrap';
5 | import { DispatchOptions } from '../internals/internals';
6 | import { getNgZone, getStore } from '../internals/static-injector';
7 | import { createActionCompleter } from '../internals/action-completer';
8 | import { ensureLocalInjectorCaptured, localInject } from '../internals/decorator-injector-adapter';
9 |
10 | const defaultOptions: DispatchOptions = { cancelUncompleted: false };
11 |
12 | export function Dispatch(options = defaultOptions): PropertyDecorator {
13 | return (
14 | // eslint-disable-next-line @typescript-eslint/ban-types
15 | target: Object,
16 | propertyKey: string | symbol,
17 | // eslint-disable-next-line @typescript-eslint/ban-types
18 | descriptor?: TypedPropertyDescriptor
19 | ) => {
20 | // eslint-disable-next-line @typescript-eslint/ban-types
21 | let originalValue: Function;
22 |
23 | const actionCompleter = createActionCompleter(options.cancelUncompleted!);
24 |
25 | function wrapped(this: ThisType) {
26 | // Every time the function is invoked we have to generate event
27 | // to cancel previously uncompleted asynchronous job
28 | if (actionCompleter !== null) {
29 | actionCompleter.cancelPreviousAction();
30 | }
31 |
32 | const store = localInject(this, Store) || getStore();
33 | const ngZone = localInject(this, NgZone) || getNgZone();
34 | // eslint-disable-next-line prefer-rest-params
35 | const wrapped = originalValue.apply(this, arguments);
36 |
37 | return ngZone.runOutsideAngular(() => unwrapAndDispatch(store, wrapped, actionCompleter));
38 | }
39 |
40 | if (typeof descriptor?.value === 'function') {
41 | originalValue = descriptor.value!;
42 | descriptor.value = wrapped;
43 | } else {
44 | Object.defineProperty(target, propertyKey, {
45 | set: value => (originalValue = value),
46 | get: () => wrapped
47 | });
48 | }
49 |
50 | ensureLocalInjectorCaptured(target);
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/lib/dispatch.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, ModuleWithProviders, NgModuleRef } from '@angular/core';
2 |
3 | import { setInjector } from './internals/static-injector';
4 |
5 | @NgModule()
6 | export class NgxsDispatchPluginModule {
7 | constructor(ngModuleRef: NgModuleRef) {
8 | setInjector(ngModuleRef.injector);
9 | ngModuleRef.onDestroy(() => {
10 | setInjector(null);
11 | });
12 | }
13 |
14 | static forRoot(): ModuleWithProviders {
15 | return {
16 | ngModule: NgxsDispatchPluginModule
17 | };
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/lib/internals/action-completer.ts:
--------------------------------------------------------------------------------
1 | import { Subject } from 'rxjs';
2 |
3 | export class ActionCompleter {
4 | cancelUncompleted$ = new Subject();
5 |
6 | cancelPreviousAction(): void {
7 | this.cancelUncompleted$.next();
8 | }
9 | }
10 |
11 | export function createActionCompleter(cancelUncompleted: boolean): ActionCompleter | null {
12 | return cancelUncompleted ? new ActionCompleter() : null;
13 | }
14 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/lib/internals/decorator-injector-adapter.ts:
--------------------------------------------------------------------------------
1 | import {
2 | InjectionToken,
3 | Injector,
4 | INJECTOR,
5 | Type,
6 | ɵɵdirectiveInject,
7 | ɵglobal
8 | } from '@angular/core';
9 |
10 | // Will be provided through Terser global definitions by Angular CLI
11 | // during the production build. This is how Angular does tree-shaking internally.
12 | declare const ngDevMode: boolean;
13 |
14 | // Angular doesn't export `NG_FACTORY_DEF`.
15 | const NG_FACTORY_DEF = 'ɵfac';
16 |
17 | // A `Symbol` which is used to save the `Injector` onto the class instance.
18 | const InjectorInstance: unique symbol = Symbol('InjectorInstance');
19 |
20 | // A `Symbol` which is used to determine if factory has been decorated previously or not.
21 | const FactoryHasBeenDecorated: unique symbol = Symbol('FactoryHasBeenDecorated');
22 |
23 | // eslint-disable-next-line @typescript-eslint/ban-types
24 | export function ensureLocalInjectorCaptured(target: Object): void {
25 | if (FactoryHasBeenDecorated in target.constructor.prototype) {
26 | return;
27 | }
28 |
29 | const constructor: ConstructorWithDefinitionAndFactory = target.constructor;
30 | // Means we're in AOT mode.
31 | if (typeof constructor[NG_FACTORY_DEF] === 'function') {
32 | decorateFactory(constructor);
33 | } else if (ngDevMode) {
34 | // We're running in JIT mode and that means we're not able to get the compiled definition
35 | // on the class inside the property decorator during the current message loop tick. We have
36 | // to wait for the next message loop tick. Note that this is safe since this Promise will be
37 | // resolved even before the `APP_INITIALIZER` is resolved.
38 | // The below code also will be executed only in development mode, since it's never recommended
39 | // to use the JIT compiler in production mode (by setting "aot: false").
40 | decorateFactoryLater(constructor);
41 | }
42 |
43 | target.constructor.prototype[FactoryHasBeenDecorated] = true;
44 | }
45 |
46 | export function localInject(
47 | instance: PrivateInstance,
48 | token: InjectionToken | Type
49 | ): T | null {
50 | const injector: Injector | undefined = instance[InjectorInstance];
51 | return injector ? injector.get(token) : null;
52 | }
53 |
54 | function decorateFactory(constructor: ConstructorWithDefinitionAndFactory): void {
55 | const factory = constructor[NG_FACTORY_DEF];
56 |
57 | if (typeof factory !== 'function') {
58 | return;
59 | }
60 |
61 | // Let's try to get any definition.
62 | // Caretaker note: this will be compatible only with Angular 9+, since Angular 9 is the first
63 | // Ivy-stable version. Previously definition properties were named differently (e.g. `ngComponentDef`).
64 | const def = constructor.ɵprov || constructor.ɵpipe || constructor.ɵcmp || constructor.ɵdir;
65 |
66 | const decoratedFactory = () => {
67 | const instance = factory();
68 | // Caretaker note: `inject()` won't work here.
69 | // We can use the `directiveInject` only during the component
70 | // construction, since Angular captures the currently active injector.
71 | // We're not able to use this function inside the getter (when the `selectorId` property is
72 | // requested for the first time), since the currently active injector will be null.
73 | instance[InjectorInstance] = ɵɵdirectiveInject(
74 | // We're using `INJECTOR` token except of the `Injector` class since the compiler
75 | // throws: `Cannot assign an abstract constructor type to a non-abstract constructor type.`.
76 | // Caretaker note: that this is the same way of getting the injector.
77 | INJECTOR
78 | );
79 | return instance;
80 | };
81 |
82 | if (def) {
83 | def.factory = decoratedFactory;
84 | }
85 |
86 | Object.defineProperty(constructor, NG_FACTORY_DEF, {
87 | get: () => decoratedFactory
88 | });
89 | }
90 |
91 | function decorateFactoryLater(constructor: ConstructorWithDefinitionAndFactory): void {
92 | // This function actually will be tree-shaken away when building for production since it's guarded with `ngDevMode`.
93 | // We're having the `try-catch` here because of the `SyncTestZoneSpec`, which throws
94 | // an error when micro or macrotask is used within a synchronous test. E.g. `Cannot call
95 | // Promise.then from within a sync test`.
96 | try {
97 | Promise.resolve().then(() => {
98 | decorateFactory(constructor);
99 | });
100 | } catch {
101 | // This is kind of a "hack", but we try to be backwards-compatible,
102 | // tho this `catch` block will only be executed when tests are run with Jasmine or Jest.
103 | ɵglobal.process &&
104 | ɵglobal.process.nextTick &&
105 | ɵglobal.process.nextTick(() => {
106 | decorateFactory(constructor);
107 | });
108 | }
109 | }
110 |
111 | // We could've used `ɵɵFactoryDef` but we try to be backwards-compatible,
112 | // since it's not exported in older Angular versions.
113 | type Factory = () => PrivateInstance;
114 |
115 | // We could've used `ɵɵInjectableDef`, `ɵɵPipeDef`, etc. We try to be backwards-compatible
116 | // since they're not exported in older Angular versions.
117 | interface Definition {
118 | factory: Factory | null;
119 | }
120 |
121 | interface ConstructorWithDefinitionAndFactory extends Function {
122 | // Provider definition for the `@Injectable()` class.
123 | ɵprov?: Definition;
124 | // Pipe definition for the `@Pipe()` class.
125 | ɵpipe?: Definition;
126 | // Component definition for the `@Component()` class.
127 | ɵcmp?: Definition;
128 | // Directive definition for the `@Directive()` class.
129 | ɵdir?: Definition;
130 | [NG_FACTORY_DEF]?: Factory;
131 | }
132 |
133 | interface PrivateInstance {
134 | [InjectorInstance]?: Injector;
135 | }
136 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/lib/internals/internals.ts:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs';
2 |
3 | export type Action = new (payload?: T) => any;
4 |
5 | export type DispatchFactory = (actionOrActions: ActionOrActions) => void;
6 |
7 | export type ActionOrActions = Action | Action[];
8 |
9 | /**
10 | * This can be a plain action/actions or Promisified/streamifed action/actions
11 | * ```typescript
12 | * @Dispatch() increment = () => new Increment();
13 | * // OR
14 | * @Dispatch() increment = () => Promise.resolve(new Increment());
15 | * ```
16 | */
17 | export type Wrapped = ActionOrActions | Observable | Promise;
18 |
19 | export interface DispatchOptions {
20 | cancelUncompleted?: boolean;
21 | }
22 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/lib/internals/static-injector.ts:
--------------------------------------------------------------------------------
1 | import { Injector, NgZone } from '@angular/core';
2 | import { Store } from '@ngxs/store';
3 |
4 | class NgxsDispatchPluginModuleNotImported extends Error {
5 | override message = 'NgxsDispatchPluginModule is not imported';
6 | }
7 |
8 | let _injector: Injector | null = null;
9 |
10 | export function setInjector(injector: Injector | null): void {
11 | _injector = injector;
12 | }
13 |
14 | export function getStore(): never | Store {
15 | if (_injector === null) {
16 | throw new NgxsDispatchPluginModuleNotImported();
17 | } else {
18 | return _injector.get(Store);
19 | }
20 | }
21 |
22 | export function getNgZone(): never | NgZone {
23 | if (_injector === null) {
24 | throw new NgxsDispatchPluginModuleNotImported();
25 | } else {
26 | return _injector.get(NgZone);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/lib/internals/unwrap.ts:
--------------------------------------------------------------------------------
1 | import { ɵisPromise } from '@angular/core';
2 | import { Store } from '@ngxs/store';
3 | import { isObservable, Observable } from 'rxjs';
4 | import { takeUntil } from 'rxjs/operators';
5 |
6 | import { ActionCompleter } from './action-completer';
7 | import { Wrapped, ActionOrActions } from './internals';
8 |
9 | function unwrapObservable(
10 | store: Store,
11 | wrapped: Observable,
12 | actionCompleter: ActionCompleter | null
13 | ): Observable {
14 | if (actionCompleter !== null) {
15 | wrapped = wrapped.pipe(takeUntil(actionCompleter.cancelUncompleted$));
16 | }
17 |
18 | wrapped.subscribe({
19 | next: actionOrActions => store.dispatch(actionOrActions)
20 | });
21 |
22 | return wrapped;
23 | }
24 |
25 | function unwrapPromise(store: Store, wrapped: Promise): Promise {
26 | return wrapped.then(actionOrActions => {
27 | store.dispatch(actionOrActions);
28 | return actionOrActions;
29 | });
30 | }
31 |
32 | export function unwrapAndDispatch(
33 | store: Store,
34 | wrapped: Wrapped,
35 | actionCompleter: ActionCompleter | null
36 | ) {
37 | if (ɵisPromise(wrapped)) {
38 | return unwrapPromise(store, wrapped);
39 | } else if (isObservable(wrapped)) {
40 | return unwrapObservable(store, wrapped, actionCompleter);
41 | } else {
42 | store.dispatch(wrapped);
43 | return wrapped;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/lib/tests/dispatch.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { TestBed } from '@angular/core/testing';
4 | import { Injectable, NgZone } from '@angular/core';
5 | import { NgxsModule, State, Action, Store, StateContext } from '@ngxs/store';
6 |
7 | import { of, timer } from 'rxjs';
8 | import { delay, concatMapTo, mapTo } from 'rxjs/operators';
9 |
10 | import { Dispatch } from '../decorators/dispatch';
11 | import { NgxsDispatchPluginModule } from '../dispatch.module';
12 |
13 | describe(NgxsDispatchPluginModule.name, () => {
14 | class Increment {
15 | static readonly type = '[Counter] Increment';
16 | }
17 |
18 | class Decrement {
19 | static readonly type = '[Counter] Decrement';
20 | }
21 |
22 | @State({
23 | name: 'counter',
24 | defaults: 0
25 | })
26 | @Injectable()
27 | class CounterState {
28 | @Action(Increment)
29 | increment(ctx: StateContext) {
30 | ctx.setState(state => state + 1);
31 | }
32 |
33 | @Action(Decrement)
34 | decrement(ctx: StateContext) {
35 | ctx.setState(state => state - 1);
36 | }
37 | }
38 |
39 | it('should be possible to dispatch events using @Dispatch() decorator', () => {
40 | // Arrange
41 | class CounterFacade {
42 | @Dispatch() increment = () => new Increment();
43 | }
44 |
45 | // Act
46 | TestBed.configureTestingModule({
47 | imports: [
48 | NgxsModule.forRoot([CounterState], { developmentMode: true }),
49 | NgxsDispatchPluginModule.forRoot()
50 | ]
51 | });
52 |
53 | const facade = new CounterFacade();
54 | const store: Store = TestBed.inject(Store);
55 |
56 | facade.increment();
57 |
58 | const counter: number = store.selectSnapshot(CounterState);
59 | // Assert
60 | expect(counter).toBe(1);
61 | });
62 |
63 | it('should be possible to dispatch plain objects using @Dispatch() decorator', () => {
64 | // Arrange
65 | class CounterFacade {
66 | @Dispatch() increment = () => ({
67 | type: '[Counter] Increment'
68 | });
69 | }
70 |
71 | // Act
72 | TestBed.configureTestingModule({
73 | imports: [
74 | NgxsModule.forRoot([CounterState], { developmentMode: true }),
75 | NgxsDispatchPluginModule.forRoot()
76 | ]
77 | });
78 |
79 | const facade = new CounterFacade();
80 | const store: Store = TestBed.inject(Store);
81 |
82 | facade.increment();
83 |
84 | const counter: number = store.selectSnapshot(CounterState);
85 | // Assert
86 | expect(counter).toBe(1);
87 | });
88 |
89 | it('should dispatch if method returns a `Promise`', async () => {
90 | // Arrange
91 | class CounterFacade {
92 | @Dispatch() incrementAsync = () => Promise.resolve(new Increment());
93 | }
94 |
95 | // Act
96 | TestBed.configureTestingModule({
97 | imports: [
98 | NgxsModule.forRoot([CounterState], { developmentMode: true }),
99 | NgxsDispatchPluginModule.forRoot()
100 | ]
101 | });
102 |
103 | const facade = new CounterFacade();
104 | const store: Store = TestBed.inject(Store);
105 |
106 | await facade.incrementAsync();
107 |
108 | const counter: number = store.selectSnapshot(CounterState);
109 | // Assert
110 | expect(counter).toBe(1);
111 | });
112 |
113 | it('should dispatch if method returns an `Observable`', async () => {
114 | // Arrange
115 | class CounterFacade {
116 | @Dispatch() incrementAsync = () => of(new Increment()).pipe(delay(1000));
117 | }
118 |
119 | // Act
120 | TestBed.configureTestingModule({
121 | imports: [
122 | NgxsModule.forRoot([CounterState], { developmentMode: true }),
123 | NgxsDispatchPluginModule.forRoot()
124 | ]
125 | });
126 |
127 | const facade = new CounterFacade();
128 | const store: Store = TestBed.inject(Store);
129 |
130 | await facade.incrementAsync().toPromise();
131 |
132 | const counter: number = store.selectSnapshot(CounterState);
133 | // Assert
134 | expect(counter).toBe(1);
135 | });
136 |
137 | it('events should be handled outside of Angular zone but dispatched within', async () => {
138 | // Arrange
139 | function delay(timeout: number): Promise {
140 | return new Promise(resolve => setTimeout(resolve, timeout));
141 | }
142 |
143 | class CounterFacade {
144 | @Dispatch() incrementAsync = async () => {
145 | await delay(200);
146 | expect(NgZone.isInAngularZone()).toBeFalsy();
147 | await delay(200);
148 | return new Increment();
149 | };
150 | }
151 |
152 | // Act
153 | TestBed.configureTestingModule({
154 | imports: [
155 | NgxsModule.forRoot([CounterState], { developmentMode: true }),
156 | NgxsDispatchPluginModule.forRoot()
157 | ]
158 | });
159 |
160 | const facade = new CounterFacade();
161 | const store: Store = TestBed.inject(Store);
162 |
163 | await facade.incrementAsync();
164 |
165 | const counter: number = store.selectSnapshot(CounterState);
166 | // Assert
167 | expect(counter).toBe(1);
168 | });
169 |
170 | it('should be possible to dispatch an array of events', () => {
171 | // Arrange
172 | class CounterFacade {
173 | @Dispatch() increment = () => [new Increment(), new Increment()];
174 | }
175 |
176 | // Act
177 | TestBed.configureTestingModule({
178 | imports: [
179 | NgxsModule.forRoot([CounterState], { developmentMode: true }),
180 | NgxsDispatchPluginModule.forRoot()
181 | ]
182 | });
183 |
184 | const facade = new CounterFacade();
185 | const store: Store = TestBed.inject(Store);
186 |
187 | facade.increment();
188 |
189 | const counter: number = store.selectSnapshot(CounterState);
190 | // Assert
191 | expect(counter).toBe(2);
192 | });
193 |
194 | it('should be possible to use queue of events', () => {
195 | // Arrange
196 | class CounterFacade {
197 | @Dispatch() increment = () => of(null).pipe(concatMapTo([new Increment(), new Increment()]));
198 | }
199 |
200 | // Act
201 | TestBed.configureTestingModule({
202 | imports: [
203 | NgxsModule.forRoot([CounterState], { developmentMode: true }),
204 | NgxsDispatchPluginModule.forRoot()
205 | ]
206 | });
207 |
208 | const facade = new CounterFacade();
209 | const store: Store = TestBed.inject(Store);
210 |
211 | facade.increment();
212 |
213 | const counter: number = store.selectSnapshot(CounterState);
214 | // Assert
215 | expect(counter).toBe(2);
216 | });
217 |
218 | it('should be possible to access instance properties', () => {
219 | // Arrange
220 | abstract class BaseCounterFacade {
221 | private action = new Increment();
222 |
223 | protected getAction() {
224 | return this.action;
225 | }
226 | }
227 |
228 | class CounterFacade extends BaseCounterFacade {
229 | @Dispatch() increment() {
230 | return this.getAction();
231 | }
232 |
233 | @Dispatch() incrementLambda = () => this.getAction();
234 | }
235 |
236 | // Act
237 | TestBed.configureTestingModule({
238 | imports: [
239 | NgxsModule.forRoot([CounterState], { developmentMode: true }),
240 | NgxsDispatchPluginModule.forRoot()
241 | ]
242 | });
243 |
244 | const facade = new CounterFacade();
245 | const store: Store = TestBed.inject(Store);
246 |
247 | facade.increment();
248 | facade.incrementLambda();
249 |
250 | const counter: number = store.selectSnapshot(CounterState);
251 | // Assert
252 | expect(counter).toBe(2);
253 | });
254 |
255 | it('should not dispatch multiple times if subscribed underneath and directly', async () => {
256 | // Arrange
257 | class CounterFacade {
258 | @Dispatch() incrementAsync = () => timer(0).pipe(mapTo(new Increment()));
259 | @Dispatch() decrementAsync = () => timer(0).pipe(mapTo(new Decrement()));
260 | }
261 |
262 | // Act
263 | TestBed.configureTestingModule({
264 | imports: [
265 | NgxsModule.forRoot([CounterState], { developmentMode: true }),
266 | NgxsDispatchPluginModule.forRoot()
267 | ]
268 | });
269 |
270 | const facade = new CounterFacade();
271 | const store: Store = TestBed.inject(Store);
272 |
273 | // `toPromise` causes to `subscribe` under the hood
274 | await facade.incrementAsync().toPromise();
275 | await facade.decrementAsync().toPromise();
276 |
277 | const counter = store.selectSnapshot(CounterState);
278 | // Assert
279 | expect(counter).toBe(0);
280 | });
281 |
282 | it('should cancel previously uncompleted asynchronous operations', async () => {
283 | // Arrange
284 | class CounterFacade {
285 | @Dispatch({ cancelUncompleted: true }) increment = () =>
286 | timer(500).pipe(mapTo(new Increment()));
287 | }
288 |
289 | // Act
290 | TestBed.configureTestingModule({
291 | imports: [
292 | NgxsModule.forRoot([CounterState], { developmentMode: true }),
293 | NgxsDispatchPluginModule.forRoot()
294 | ]
295 | });
296 |
297 | const facade = new CounterFacade();
298 | const store: Store = TestBed.inject(Store);
299 |
300 | facade.increment();
301 | facade.increment();
302 | await facade.increment().toPromise();
303 |
304 | const counter = store.selectSnapshot(CounterState);
305 | // Assert
306 | expect(counter).toBe(1);
307 | });
308 | });
309 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/lib/tests/fresh-platform.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { DOCUMENT } from '@angular/common';
3 | import { ɵgetDOM as getDOM } from '@angular/platform-browser';
4 | import { destroyPlatform, createPlatform } from '@angular/core';
5 |
6 | function createRootElement() {
7 | const document = TestBed.inject(DOCUMENT);
8 | const root = getDOM().createElement('app-root', document);
9 | document.body.appendChild(root);
10 | }
11 |
12 | function removeRootElement() {
13 | const root: Element = document.getElementsByTagName('app-root').item(0)!;
14 | try {
15 | document.body.removeChild(root);
16 | // eslint-disable-next-line no-empty
17 | } catch {}
18 | }
19 |
20 | function destroyPlatformBeforeBootstrappingTheNewOne() {
21 | destroyPlatform();
22 | createRootElement();
23 | }
24 |
25 | // As we create our custom platform via `bootstrapModule`
26 | // we have to destroy it after assetions and revert
27 | // the previous one
28 | function resetPlatformAfterBootstrapping() {
29 | removeRootElement();
30 | destroyPlatform();
31 | createPlatform(TestBed);
32 | }
33 |
34 | // eslint-disable-next-line @typescript-eslint/ban-types
35 | export function freshPlatform(fn: Function): (...args: any[]) => any {
36 | return async function testWithAFreshPlatform(this: any, ...args: any[]) {
37 | try {
38 | destroyPlatformBeforeBootstrappingTheNewOne();
39 | return await fn.apply(this, args);
40 | } finally {
41 | resetPlatformAfterBootstrapping();
42 | }
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/lib/tests/parallel-modules.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, Injectable, NgModule, ɵivyEnabled } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
4 | import { Action, NgxsModule, State, StateContext, Store } from '@ngxs/store';
5 |
6 | import { Dispatch } from '../decorators/dispatch';
7 |
8 | import { freshPlatform } from './fresh-platform';
9 | import { skipConsoleLogging } from './skip-console-logging';
10 |
11 | describe('Parallel modules', () => {
12 | if (!ɵivyEnabled) {
13 | throw new Error('This test requires Ivy to be enabled.');
14 | }
15 |
16 | class Increment {
17 | static readonly type = '[Counter] Increment';
18 | }
19 |
20 | class Decrement {
21 | static readonly type = '[Counter] Decrement';
22 | }
23 |
24 | @State({
25 | name: 'counter',
26 | defaults: 0
27 | })
28 | @Injectable()
29 | class CounterState {
30 | @Action(Increment)
31 | increment(ctx: StateContext) {
32 | ctx.setState(state => state + 1);
33 | }
34 |
35 | @Action(Decrement)
36 | decrement(ctx: StateContext) {
37 | ctx.setState(state => state - 1);
38 | }
39 | }
40 |
41 | it(
42 | 'should be possible to bootstrap modules in parallel like in server-side environment',
43 | freshPlatform(async () => {
44 | // Arrange & act
45 | @Injectable()
46 | class CounterFacade {
47 | @Dispatch() increment = () => new Increment();
48 | }
49 |
50 | @Component({ selector: 'app-root', template: '' })
51 | class TestComponent {}
52 |
53 | @NgModule({
54 | imports: [BrowserModule, NgxsModule.forRoot([CounterState], { developmentMode: true })],
55 | declarations: [TestComponent],
56 | bootstrap: [TestComponent],
57 | providers: [CounterFacade]
58 | })
59 | class TestModule {}
60 |
61 | const platform = platformBrowserDynamic();
62 |
63 | // Now let's bootstrap 2 different apps in parallel, this is basically the same what
64 | // Angular Universal does internally for concurrent HTTP requests.
65 | const [firstNgModuleRef, secondNgModuleRef] = await skipConsoleLogging(() =>
66 | Promise.all([platform.bootstrapModule(TestModule), platform.bootstrapModule(TestModule)])
67 | );
68 |
69 | const firstStore = firstNgModuleRef.injector.get(Store);
70 | const secondStore = secondNgModuleRef.injector.get(Store);
71 |
72 | const firstCounterFacade = firstNgModuleRef.injector.get(CounterFacade);
73 | const secondCounterFacade = secondNgModuleRef.injector.get(CounterFacade);
74 |
75 | firstCounterFacade.increment();
76 | firstCounterFacade.increment();
77 |
78 | // Assert
79 | expect(firstStore.selectSnapshot(CounterState)).toEqual(2);
80 | expect(secondStore.selectSnapshot(CounterState)).toEqual(0);
81 |
82 | secondCounterFacade.increment();
83 | secondCounterFacade.increment();
84 | secondCounterFacade.increment();
85 |
86 | expect(firstStore.selectSnapshot(CounterState)).toEqual(2);
87 | expect(secondStore.selectSnapshot(CounterState)).toEqual(3);
88 |
89 | firstNgModuleRef.destroy();
90 |
91 | secondCounterFacade.increment();
92 |
93 | expect(firstStore.selectSnapshot(CounterState)).toEqual(2);
94 | expect(secondStore.selectSnapshot(CounterState)).toEqual(4);
95 | })
96 | );
97 | });
98 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/lib/tests/skip-console-logging.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | function createFn() {
4 | // eslint-disable-next-line @typescript-eslint/no-empty-function
5 | return () => {};
6 | }
7 |
8 | export function skipConsoleLogging any>(fn: T): ReturnType {
9 | const consoleSpies = [
10 | jest.spyOn(console, 'log').mockImplementation(createFn()),
11 | jest.spyOn(console, 'warn').mockImplementation(createFn()),
12 | jest.spyOn(console, 'error').mockImplementation(createFn()),
13 | jest.spyOn(console, 'info').mockImplementation(createFn())
14 | ];
15 | function restoreSpies() {
16 | consoleSpies.forEach(spy => spy.mockRestore());
17 | }
18 | let restoreSpyAsync = false;
19 | try {
20 | const returnValue = fn();
21 | if (returnValue instanceof Promise) {
22 | restoreSpyAsync = true;
23 | return returnValue.finally(() => restoreSpies()) as ReturnType;
24 | }
25 | return returnValue;
26 | } finally {
27 | if (!restoreSpyAsync) {
28 | restoreSpies();
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/src/test-setup.ts:
--------------------------------------------------------------------------------
1 | import 'jest-preset-angular/setup-jest';
2 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "references": [
4 | {
5 | "path": "./tsconfig.lib.json"
6 | },
7 | {
8 | "path": "./tsconfig.spec.json"
9 | }
10 | ],
11 | "compilerOptions": {
12 | "target": "es2020"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2020",
5 | "module": "es2015",
6 | "inlineSources": true,
7 | "importHelpers": true,
8 | "downlevelIteration": true,
9 | "lib": ["dom", "es2018"],
10 | "types": ["node"],
11 | "useDefineForClassFields": false
12 | },
13 | "angularCompilerOptions": {
14 | "compilationMode": "partial",
15 | "annotateForClosureCompiler": true,
16 | "skipTemplateCodegen": true,
17 | "strictMetadataEmit": true,
18 | "fullTemplateTypeCheck": true,
19 | "strictInjectionParameters": true,
20 | "enableResourceInlining": true
21 | },
22 | "include": ["**/*.ts"],
23 | "exclude": ["**/*.spec.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/libs/dispatch-decorator/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "files": ["src/test-setup.ts"],
9 | "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "affected": {
3 | "defaultBase": "master"
4 | },
5 | "npmScope": "dispatch-decorator",
6 | "tasksRunnerOptions": {
7 | "default": {
8 | "runner": "nx/tasks-runners/default",
9 | "options": {
10 | "cacheableOperations": ["build", "lint", "test", "e2e"],
11 | "parallel": 1
12 | }
13 | }
14 | },
15 | "defaultProject": "dispatch-decorator",
16 | "generators": {
17 | "@nrwl/angular:application": {
18 | "linter": "eslint",
19 | "unitTestRunner": "jest"
20 | },
21 | "@nrwl/angular:library": {
22 | "linter": "eslint",
23 | "unitTestRunner": "jest"
24 | },
25 | "@nrwl/angular:component": {}
26 | },
27 | "$schema": "./node_modules/nx/schemas/nx-schema.json",
28 | "targetDefaults": {
29 | "build": {
30 | "dependsOn": ["^build"],
31 | "inputs": ["production", "^production"]
32 | },
33 | "e2e": {
34 | "inputs": ["default", "^production"]
35 | },
36 | "test": {
37 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"]
38 | },
39 | "lint": {
40 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"]
41 | }
42 | },
43 | "namedInputs": {
44 | "default": ["{projectRoot}/**/*", "sharedGlobals"],
45 | "sharedGlobals": [
46 | "{workspaceRoot}/angular.json",
47 | "{workspaceRoot}/tsconfig.json",
48 | "{workspaceRoot}/tslint.json",
49 | "{workspaceRoot}/nx.json"
50 | ],
51 | "production": [
52 | "default",
53 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
54 | "!{projectRoot}/tsconfig.spec.json",
55 | "!{projectRoot}/jest.config.[jt]s",
56 | "!{projectRoot}/.eslintrc.json"
57 | ]
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dispatch",
3 | "version": "0.0.0",
4 | "repository": {
5 | "type": "git",
6 | "url": "git+https://github.com/ngxs-labs/dispatch.git"
7 | },
8 | "license": "MIT",
9 | "homepage": "https://github.com/ngxs-labs/dispatch#readme",
10 | "bugs": {
11 | "url": "https://github.com/ngxs-labs/dispatch/issues"
12 | },
13 | "keywords": [
14 | "ngxs",
15 | "redux",
16 | "store"
17 | ],
18 | "scripts": {
19 | "prebuild": "nx build dispatch-decorator --skip-nx-cache",
20 | "build": "yarn prebuild && yarn postbuild",
21 | "postbuild": "cpy README.md dist/libs/dispatch-decorator",
22 | "test": "nx test dispatch-decorator",
23 | "e2e": "nx e2e demos-e2e"
24 | },
25 | "private": true,
26 | "devDependencies": {
27 | "@angular-devkit/build-angular": "15.1.1",
28 | "@angular-devkit/core": "15.1.1",
29 | "@angular-devkit/schematics": "15.1.1",
30 | "@angular-eslint/eslint-plugin": "15.0.0",
31 | "@angular-eslint/eslint-plugin-template": "15.0.0",
32 | "@angular-eslint/template-parser": "15.0.0",
33 | "@angular/animations": "15.1.0",
34 | "@angular/cli": "15.1.1",
35 | "@angular/common": "15.1.0",
36 | "@angular/compiler": "15.1.0",
37 | "@angular/compiler-cli": "15.1.0",
38 | "@angular/core": "15.1.0",
39 | "@angular/platform-browser": "15.1.0",
40 | "@angular/platform-browser-dynamic": "15.1.0",
41 | "@angular/platform-server": "15.1.0",
42 | "@commitlint/cli": "^17.5.0",
43 | "@commitlint/config-conventional": "^17.4.4",
44 | "@nguniversal/builders": "15.1.0",
45 | "@nguniversal/express-engine": "15.1.0",
46 | "@ngxs/store": "3.7.6",
47 | "@nrwl/angular": "15.8.9",
48 | "@nrwl/cli": "15.8.9",
49 | "@nrwl/cypress": "15.8.9",
50 | "@nrwl/eslint-plugin-nx": "15.8.9",
51 | "@nrwl/jest": "15.8.9",
52 | "@nrwl/linter": "15.8.9",
53 | "@nrwl/workspace": "15.8.9",
54 | "@schematics/angular": "15.1.1",
55 | "@types/express": "^4.17.2",
56 | "@types/jest": "29.4.4",
57 | "@types/node": "^16.11.7",
58 | "@typescript-eslint/eslint-plugin": "5.43.0",
59 | "@typescript-eslint/parser": "5.43.0",
60 | "cpy-cli": "^3.1.1",
61 | "cypress": "^10.0.0",
62 | "eslint": "8.15.0",
63 | "eslint-config-prettier": "^8.3.0",
64 | "eslint-plugin-cypress": "^2.12.1",
65 | "express": "^4.17.1",
66 | "husky": "^8.0.0",
67 | "jest": "29.4.3",
68 | "jest-environment-jsdom": "29.4.3",
69 | "jest-preset-angular": "13.0.0",
70 | "lint-staged": "^13.0.0",
71 | "ng-packagr": "~15.1.0",
72 | "nx": "15.8.9",
73 | "postcss": "8.4.16",
74 | "postcss-import": "14.1.0",
75 | "postcss-preset-env": "7.5.0",
76 | "postcss-url": "10.1.3",
77 | "prettier": "2.7.1",
78 | "rxjs": "^7.5.4",
79 | "ts-jest": "29.0.5",
80 | "ts-node": "^10.0.0",
81 | "tslib": "^2.3.1",
82 | "typescript": "4.8.4",
83 | "zone.js": "0.12.0"
84 | },
85 | "lint-staged": {
86 | "*.{js,ts,html,md}": [
87 | "prettier --write"
88 | ]
89 | },
90 | "prettier": {
91 | "semi": true,
92 | "endOfLine": "lf",
93 | "arrowParens": "avoid",
94 | "tabWidth": 2,
95 | "printWidth": 100,
96 | "trailingComma": "none",
97 | "bracketSpacing": true,
98 | "singleQuote": true
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "paths": {
6 | "@ngxs-labs/dispatch-decorator": ["libs/dispatch-decorator/src/index.ts"]
7 | },
8 | "target": "es5",
9 | "lib": ["es2017", "dom"],
10 | "moduleResolution": "node",
11 | "typeRoots": ["node_modules/@types"],
12 | "strict": true,
13 | "sourceMap": true,
14 | "declaration": false,
15 | "noImplicitAny": false,
16 | "resolveJsonModule": true,
17 | "downlevelIteration": true,
18 | "noStrictGenericChecks": true,
19 | "emitDecoratorMetadata": true,
20 | "experimentalDecorators": true,
21 | "suppressImplicitAnyIndexErrors": true
22 | },
23 | "exclude": ["cypress"]
24 | }
25 |
--------------------------------------------------------------------------------