├── .commitlintrc.json ├── .editorconfig ├── .github └── workflows │ └── master.yml ├── .gitignore ├── .huskyrc.json ├── .lintstagedrc.json ├── .prettierrc.json ├── .versionrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── integration ├── .browserslistrc ├── _redirects ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ └── examples │ │ ├── multiple │ │ ├── multiple.component.html │ │ ├── multiple.component.ts │ │ └── multiple.module.ts │ │ ├── single │ │ ├── single.component.html │ │ ├── single.component.ts │ │ └── single.module.ts │ │ └── states │ │ └── zoo │ │ ├── zoo.actions.ts │ │ ├── zoo.model.ts │ │ └── zoo.state.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── tsconfig.app.json └── tsconfig.spec.json ├── jest.config.js ├── package.json ├── renovate.json ├── setup-jest.ts ├── src ├── index.ts ├── lib │ ├── actions-executed.selector.ts │ ├── actions-executed.state.ts │ ├── actions-executing.module.ts │ ├── actions-executing.selector.ts │ └── actions-executing.state.ts ├── ng-package.json ├── ng-package.prod.json ├── package.json ├── public-api.ts ├── tests │ ├── actions-executed.spec.ts │ ├── actions-executing-module.spec.ts │ ├── actions-executing.spec.ts │ └── has-actions-executing.spec.ts ├── tsconfig.lib.json ├── tsconfig.lib.prod.json └── tsconfig.spec.json ├── tools └── copy-readme.ts ├── tsconfig.json ├── tsconfig.spec.json ├── tsconfig.tools.json ├── tslint.json ├── yarn.lock └── yarn.lock.readme.md /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional" 3 | ] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: master 2 | on: push 3 | 4 | jobs: 5 | build-and-test: 6 | runs-on: ubuntu-latest 7 | name: Build and test 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: '18.x' 13 | - name: Get yarn cache directory path 14 | id: yarn-cache-dir-path 15 | run: echo "::set-output name=dir::$(yarn cache dir)" 16 | - uses: actions/cache@v3 17 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 18 | with: 19 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 20 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 21 | restore-keys: | 22 | ${{ runner.os }}-yarn- 23 | - run: yarn install 24 | - run: yarn test:ci 25 | - run: yarn build 26 | - name: Coveralls 27 | uses: coverallsapp/github-action@master 28 | with: 29 | github-token: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.angular/cache 2 | dist 3 | dist-integration 4 | dist-integration-server 5 | node_modules 6 | .idea 7 | .vscode 8 | coverage 9 | yarn-error.log 10 | .cache 11 | package-lock.json 12 | *.iml 13 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,js,html,css,scss,md,json,yml}": ["prettier --write", "git add"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "htmlWhitespaceSensitivity": "css", 9 | "jsxBracketSameLine": true, 10 | "bracketSpacing": true, 11 | "arrowParens": "always", 12 | "proseWrap": "always" 13 | } -------------------------------------------------------------------------------- /.versionrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageFiles": [ 3 | { 4 | "filename": "src/package.json", 5 | "type": "json" 6 | } 7 | ], 8 | "bumpFiles": [ 9 | { 10 | "filename": "src/package.json", 11 | "type": "json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See 4 | [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 5 | 6 | ## [19.0.0](https://github.com/ngxs-labs/actions-executing/compare/v18.0.0...v19.0.0) (2024-12-24) 7 | 8 | ## [18.0.0](https://github.com/ngxs-labs/actions-executing/compare/v1.0.7...v18.0.0) (2024-07-02) 9 | 10 | ### [1.0.7](https://github.com/ngxs-labs/actions-executing/compare/v1.0.6...v1.0.7) (2024-07-02) 11 | 12 | ### [1.0.6](https://github.com/ngxs-labs/actions-executing/compare/v1.0.5...v1.0.6) (2023-04-06) 13 | 14 | ### [1.0.5](https://github.com/ngxs-labs/actions-executing/compare/v1.0.4...v1.0.5) (2023-04-04) 15 | 16 | ### [1.0.4](https://github.com/ngxs-labs/actions-executing/compare/v1.0.3...v1.0.4) (2023-03-21) 17 | 18 | ### [1.0.3](https://github.com/ngxs-labs/actions-executing/compare/v1.0.2...v1.0.3) (2023-02-02) 19 | 20 | ### [1.0.2](https://github.com/ngxs-labs/actions-executing/compare/v1.0.1...v1.0.2) (2023-02-02) 21 | 22 | ### [1.0.1](https://github.com/ngxs-labs/actions-executing/compare/v1.0.0...v1.0.1) (2023-02-02) 23 | 24 | ## [1.0.0](https://github.com/ngxs-labs/actions-executing/compare/v0.2.4...v1.0.0) (2023-02-02) 25 | 26 | ### [0.2.4](https://github.com/ngxs-labs/actions-executing/compare/v0.2.3...v0.2.4) (2023-02-02) 27 | 28 | ### [0.2.3](https://github.com/ngxs-labs/actions-executing/compare/v0.2.2...v0.2.3) (2023-02-02) 29 | 30 | ### [0.2.2](https://github.com/ngxs-labs/actions-executing/compare/v0.2.1...v0.2.2) (2023-02-01) 31 | 32 | ### [0.2.1](https://github.com/ngxs-labs/actions-executing/compare/v0.1.0...v0.2.1) (2023-02-01) 33 | 34 | ### Features 35 | 36 | - actions-executed selector 37 | ([48821c4](https://github.com/ngxs-labs/actions-executing/commit/48821c409661f0325c55647ff7d9982f9cb52891)) 38 | - add hasActionsExecuting to return boolean 39 | ([e602772](https://github.com/ngxs-labs/actions-executing/commit/e6027723926af9258d8d98294ba6c3bd65bedfeb)) 40 | - support empty array and return whole state 41 | ([b8ddbcc](https://github.com/ngxs-labs/actions-executing/commit/b8ddbcc64aa822fff016ccf9ebaf3b778cae2caa)) 42 | 43 | ### Bug Fixes 44 | 45 | - inmutable 46 | ([7bcdd7b](https://github.com/ngxs-labs/actions-executing/commit/7bcdd7b3e46846f0e24d90134cbf528003abc9f9)) 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 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: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | 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. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![master](https://github.com/ngxs-labs/actions-executing/workflows/main/badge.svg?branch=master) 2 | [![npm version](https://badge.fury.io/js/%40ngxs-labs%2Factions-executing.svg)](https://badge.fury.io/js/%40ngxs-labs%2Factions-executing) 3 | [![Coverage Status](https://coveralls.io/repos/github/ngxs-labs/actions-executing/badge.svg?branch=master)](https://coveralls.io/github/ngxs-labs/actions-executing?branch=master) 4 | 5 |

6 | 7 |

8 | 9 | --- 10 | 11 | # NGXS Actions Executing 12 | 13 | ## Demo 14 | 15 | [Link](https://ngxs-labs-actions-executing.netlify.app/) 16 | 17 | ## Description 18 | 19 | This plugin allows you to easily know if an action is being executed and control UI elements or control flow of your 20 | code to execute. The most common scenarios for using this plugin are to display loading spinner or disable a button 21 | while an action is executing. 22 | 23 | ## Compatibility 24 | 25 | | Angular | @ngxs-labs/actions-executing | 26 | | ------- | ------------------------------ | 27 | | non-ivy | 0.x | 28 | | ivy | 1.x (compiled in partial mode) | 29 | 30 | ## Quick start 31 | 32 | Install the plugin: 33 | 34 | - npm 35 | 36 | ```console 37 | npm install --save @ngxs-labs/actions-executing 38 | ``` 39 | 40 | - yarn 41 | 42 | ```console 43 | yarn add @ngxs-labs/actions-executing 44 | ``` 45 | 46 | Next, include it in you `app.module.ts` 47 | 48 | ```ts 49 | //... 50 | import { NgxsModule } from '@ngxs/store'; 51 | import { NgxsActionsExecutingModule } from '@ngxs-labs/actions-executing'; 52 | 53 | @NgModule({ 54 | //... 55 | imports: [ 56 | //... 57 | NgxsModule.forRoot([ 58 | //... your states 59 | ]), 60 | NgxsActionsExecutingModule.forRoot() 61 | ] 62 | //... 63 | }) 64 | export class AppModule {} 65 | ``` 66 | 67 | To use it on your components you just need to include the following `@Select()` 68 | 69 | ```ts 70 | //... 71 | import { actionsExecuting, ActionsExecuting } from '@ngxs-labs/actions-executing'; 72 | 73 | //... 74 | export class SingleComponent { 75 | @Select(actionsExecuting([MyAction])) myActionIsExecuting$: Observable; 76 | } 77 | ``` 78 | 79 | then you can disable a button or display a loading indicator very easily 80 | 81 | ```html 82 | 85 | 86 | 87 | Loading... 88 | 89 | ``` 90 | 91 | ## More examples 92 | 93 | `actionsExecuting` selector returns the type `ActionsExecuting` 94 | 95 | ```ts 96 | type ActionsExecuting = { [action: string]: number } | null; 97 | ``` 98 | 99 | This allows you to know which actions and how many of them are being executed at any given time. 100 | 101 | You can also pass multiple actions to the selector and this way you'll receive updates when any of those actions are 102 | executing. 103 | 104 | ```ts 105 | @Select(actionsExecuting([Action1, Action2])) multipleActions$: Observable; 106 | ``` 107 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "integration": { 7 | "root": "", 8 | "sourceRoot": "integration", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": { 21 | "base": "dist/integration" 22 | }, 23 | "index": "integration/index.html", 24 | "polyfills": ["integration/polyfills.ts"], 25 | "tsConfig": "integration/tsconfig.app.json", 26 | "assets": ["integration/favicon.ico", "integration/assets", "integration/_redirects"], 27 | "styles": ["integration/styles.scss"], 28 | "scripts": [], 29 | "extractLicenses": false, 30 | "sourceMap": true, 31 | "optimization": false, 32 | "namedChunks": true, 33 | "browser": "integration/main.ts" 34 | }, 35 | "configurations": { 36 | "production": { 37 | "budgets": [ 38 | { 39 | "type": "anyComponentStyle", 40 | "maximumWarning": "6kb" 41 | } 42 | ], 43 | "fileReplacements": [ 44 | { 45 | "replace": "integration/environments/environment.ts", 46 | "with": "integration/environments/environment.prod.ts" 47 | } 48 | ], 49 | "optimization": true, 50 | "outputHashing": "all", 51 | "sourceMap": false, 52 | "namedChunks": false, 53 | "extractLicenses": true 54 | } 55 | }, 56 | "defaultConfiguration": "" 57 | }, 58 | "serve": { 59 | "builder": "@angular-devkit/build-angular:dev-server", 60 | "options": { 61 | "buildTarget": "integration:build" 62 | }, 63 | "configurations": { 64 | "production": { 65 | "buildTarget": "integration:build:production" 66 | } 67 | } 68 | } 69 | } 70 | }, 71 | "ngxs-actions-executing": { 72 | "root": "src", 73 | "sourceRoot": "src", 74 | "projectType": "library", 75 | "prefix": "lib", 76 | "architect": { 77 | "build": { 78 | "builder": "@angular-devkit/build-angular:ng-packagr", 79 | "options": { 80 | "tsConfig": "src/tsconfig.lib.json", 81 | "project": "src/ng-package.json" 82 | }, 83 | "configurations": { 84 | "production": { 85 | "project": "src/ng-package.prod.json", 86 | "tsConfig": "src/tsconfig.lib.prod.json" 87 | } 88 | } 89 | } 90 | } 91 | } 92 | }, 93 | "cli": { 94 | "analytics": false 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /integration/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /integration/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /integration/app/app.component.html: -------------------------------------------------------------------------------- 1 | 9 |
10 |
11 |
12 | 13 |
14 | 15 |
16 |

Store:

17 |
{{ store$ | async | json }}
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /integration/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/actions-executing/51685dd4af0961c88075786494cb8ebc7ea9c600/integration/app/app.component.scss -------------------------------------------------------------------------------- /integration/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'] 9 | }) 10 | export class AppComponent { 11 | public store$: Observable = this.store.select((state) => state); 12 | 13 | constructor(private store: Store) {} 14 | } 15 | -------------------------------------------------------------------------------- /integration/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgxsModule } from '@ngxs/store'; 2 | import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { NgxsActionsExecutingModule } from '@ngxs-labs/actions-executing'; 5 | import { NgModule } from '@angular/core'; 6 | import { AppComponent } from './app.component'; 7 | import { environment } from '../environments/environment'; 8 | import { RouterModule } from '@angular/router'; 9 | import { FormsModule } from '@angular/forms'; 10 | import { ZooState } from './examples/states/zoo/zoo.state'; 11 | import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; 12 | 13 | @NgModule({ 14 | declarations: [AppComponent], 15 | imports: [ 16 | BrowserModule, 17 | FormsModule, 18 | RouterModule.forRoot( 19 | [ 20 | { 21 | path: '', 22 | redirectTo: 'single', 23 | pathMatch: 'full' 24 | }, 25 | { 26 | path: 'single', 27 | loadChildren: () => import('./examples/single/single.module').then((m) => m.FirstModule) 28 | }, 29 | { 30 | path: 'multiple', 31 | loadChildren: () => import('./examples/multiple/multiple.module').then((m) => m.SecondModule) 32 | } 33 | ], 34 | {} 35 | ), 36 | NgxsModule.forRoot([ZooState], { 37 | developmentMode: !environment.production 38 | }), 39 | NgxsLoggerPluginModule.forRoot(), 40 | NgxsActionsExecutingModule.forRoot(), 41 | NgxsReduxDevtoolsPluginModule.forRoot() 42 | ], 43 | providers: [], 44 | bootstrap: [AppComponent] 45 | }) 46 | export class AppModule {} 47 | -------------------------------------------------------------------------------- /integration/app/examples/multiple/multiple.component.html: -------------------------------------------------------------------------------- 1 |

Zoo State

2 |

3 | {{ zoo$ | async | json }} 4 |

5 | 6 |

7 | Dispatch Action 8 |

9 | 10 | 11 | 12 | 13 |
14 |
15 | Loading... 16 |
17 | Adding Panda... 18 | Adding Bear... 19 |
20 | 21 |
22 |
addPandaExecuting$ = {{ addPandaExecuting$ | async | json }}
23 |
addBearExecuting$ = {{ addBearExecuting$ | async | json }}
24 |
25 | -------------------------------------------------------------------------------- /integration/app/examples/multiple/multiple.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { Select, Store } from '@ngxs/store'; 3 | import { actionsExecuting, ActionsExecuting } from '@ngxs-labs/actions-executing'; 4 | import { AddPanda, AddBear } from '../states/zoo/zoo.actions'; 5 | import { Observable } from 'rxjs'; 6 | import { ZooState } from '../states/zoo/zoo.state'; 7 | import { ZooStateModel } from '../states/zoo/zoo.model'; 8 | 9 | @Component({ 10 | selector: 'multiple', 11 | templateUrl: './multiple.component.html', 12 | changeDetection: ChangeDetectionStrategy.OnPush 13 | }) 14 | export class MultipleComponent { 15 | @Select(actionsExecuting([AddPanda])) public addPandaExecuting$: Observable; 16 | @Select(actionsExecuting([AddBear])) public addBearExecuting$: Observable; 17 | @Select(actionsExecuting([AddPanda, AddBear])) public addPandaOrAddBearExecuting$: Observable; 18 | @Select(ZooState) public zoo$: Observable; 19 | 20 | constructor(private store: Store) {} 21 | 22 | public addPanda() { 23 | this.store.dispatch(new AddPanda()); 24 | } 25 | 26 | public addBear() { 27 | this.store.dispatch(new AddBear()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /integration/app/examples/multiple/multiple.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { MultipleComponent } from './multiple.component'; 5 | 6 | @NgModule({ 7 | declarations: [MultipleComponent], 8 | imports: [CommonModule, RouterModule.forChild([{ path: '', component: MultipleComponent }])] 9 | }) 10 | export class SecondModule {} 11 | -------------------------------------------------------------------------------- /integration/app/examples/single/single.component.html: -------------------------------------------------------------------------------- 1 |

Zoo State

2 |

3 | {{ zoo$ | async | json }} 4 |

5 | 6 |

7 | Dispatch Action 8 |

9 | 10 | 11 | 12 | 13 |
14 |
15 | Loading... 16 |
17 | Adding Panda... 18 |
19 | 20 |
21 |
22 | Loading... 23 |
24 | Adding {{ addBearExecutingCount$ | async }} Bear... 25 |
26 | 27 |
28 |
addPandaExecuting$ = {{ addPandaExecuting$ | async | json }}
29 |
addBearExecuting$ = {{ addBearExecuting$ | async | json }}
30 |
31 | -------------------------------------------------------------------------------- /integration/app/examples/single/single.component.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { actionsExecuting, ActionsExecuting } from '@ngxs-labs/actions-executing'; 3 | import { Component, OnInit } from '@angular/core'; 4 | import { Select, Store } from '@ngxs/store'; 5 | import { ZooState } from '../states/zoo/zoo.state'; 6 | import { ZooStateModel } from '../states/zoo/zoo.model'; 7 | import { AddBear, AddPanda } from '../states/zoo/zoo.actions'; 8 | import { map } from 'rxjs/operators'; 9 | 10 | @Component({ 11 | selector: 'single', 12 | templateUrl: 'single.component.html' 13 | }) 14 | export class SingleComponent implements OnInit { 15 | @Select(actionsExecuting([AddPanda])) public addPandaExecuting$: Observable; 16 | @Select(actionsExecuting([AddBear])) public addBearExecuting$: Observable; 17 | addBearExecutingCount$: Observable; 18 | @Select(ZooState) public zoo$: Observable; 19 | 20 | constructor(private store: Store) {} 21 | 22 | ngOnInit() { 23 | this.addBearExecutingCount$ = this.addBearExecuting$.pipe( 24 | map((_actionsExecuting) => _actionsExecuting[AddBear.type]) 25 | ); 26 | } 27 | 28 | public addPanda() { 29 | this.store.dispatch(new AddPanda()); 30 | } 31 | 32 | public addBear() { 33 | this.store.dispatch(new AddBear()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /integration/app/examples/single/single.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { SingleComponent } from './single.component'; 5 | import { FormsModule } from '@angular/forms'; 6 | 7 | @NgModule({ 8 | declarations: [SingleComponent], 9 | imports: [CommonModule, FormsModule, RouterModule.forChild([{ path: '', component: SingleComponent }])] 10 | }) 11 | export class FirstModule {} 12 | -------------------------------------------------------------------------------- /integration/app/examples/states/zoo/zoo.actions.ts: -------------------------------------------------------------------------------- 1 | export class AddPanda { 2 | public static type = '[Zoo] AddPanda'; 3 | } 4 | export class AddBear { 5 | public static type = '[Zoo] AddBear'; 6 | } 7 | export class AddPandaAndBear { 8 | public static type = '[Zoo] AddPandaAndBear'; 9 | } 10 | -------------------------------------------------------------------------------- /integration/app/examples/states/zoo/zoo.model.ts: -------------------------------------------------------------------------------- 1 | export interface ZooStateModel { 2 | pandas: string[]; 3 | bears: string[]; 4 | } 5 | -------------------------------------------------------------------------------- /integration/app/examples/states/zoo/zoo.state.ts: -------------------------------------------------------------------------------- 1 | import { State, Action, StateContext, Selector } from '@ngxs/store'; 2 | import { ZooStateModel } from './zoo.model'; 3 | import { AddPanda, AddBear, AddPandaAndBear } from './zoo.actions'; 4 | import { of } from 'rxjs'; 5 | import { tap, delay } from 'rxjs/operators'; 6 | 7 | @State({ 8 | name: 'test', 9 | defaults: { 10 | pandas: [], 11 | bears: [] 12 | } 13 | }) 14 | export class ZooState { 15 | @Selector() public static pandas(state: ZooStateModel) { 16 | return state.pandas; 17 | } 18 | @Selector() public static bears(state: ZooStateModel) { 19 | return state.bears; 20 | } 21 | 22 | ngxsOnInit(ctx: StateContext) { 23 | ctx.dispatch(AddPanda); 24 | } 25 | 26 | @Action([AddPanda]) 27 | public addPanda({ patchState, getState }: StateContext) { 28 | return of({}).pipe( 29 | delay(1000), 30 | tap((_) => { 31 | patchState({ pandas: getState().pandas.concat('🐼') }); 32 | }) 33 | ); 34 | } 35 | 36 | @Action([AddBear]) 37 | public addBear({ patchState, getState }: StateContext) { 38 | return of({}).pipe( 39 | delay(2000), 40 | tap((_) => { 41 | patchState({ bears: getState().bears.concat('🐻') }); 42 | }) 43 | ); 44 | } 45 | 46 | @Action([AddPandaAndBear]) 47 | public addPandaAndBear({ patchState, getState, dispatch }: StateContext) { 48 | return dispatch([new AddBear(), new AddPanda()]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /integration/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/actions-executing/51685dd4af0961c88075786494cb8ebc7ea9c600/integration/assets/.gitkeep -------------------------------------------------------------------------------- /integration/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /integration/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 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /integration/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/actions-executing/51685dd4af0961c88075786494cb8ebc7ea9c600/integration/favicon.ico -------------------------------------------------------------------------------- /integration/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ngxs Actions Executing 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /integration/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.log(err)); 14 | -------------------------------------------------------------------------------- /integration/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/proposals/reflect-metadata'; 2 | import 'zone.js'; 3 | -------------------------------------------------------------------------------- /integration/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/scss/bootstrap'; 2 | 3 | html { 4 | height: 100%; 5 | @extend .bg-light; 6 | } 7 | 8 | body { 9 | @extend .bg-light; 10 | } 11 | -------------------------------------------------------------------------------- /integration/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": ["main.ts", "polyfills.ts"], 8 | "include": ["integration/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /integration/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": ["jasmine", "node"] 7 | }, 8 | "files": ["test.ts", "polyfills.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest'); 2 | const { paths } = require('./tsconfig.json').compilerOptions; 3 | 4 | // eslint-disable-next-line no-undef 5 | globalThis.ngJest = { 6 | skipNgcc: false, 7 | tsconfig: 'tsconfig.spec.json' 8 | }; 9 | 10 | /** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ 11 | module.exports = { 12 | preset: 'jest-preset-angular', 13 | moduleNameMapper: pathsToModuleNameMapper(paths, { prefix: '' }), 14 | setupFilesAfterEnv: ['/setup-jest.ts'] 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngxs-actions-executing", 3 | "scripts": { 4 | "start": "ng serve --port=4256", 5 | "build": "ng build ngxs-actions-executing --configuration production && yarn copy-readme", 6 | "copy-readme": "ts-node --project tsconfig.tools.json ./tools/copy-readme", 7 | "release": "standard-version --release-as patch", 8 | "build-integration": "ng build integration", 9 | "test": "jest --config ./jest.config.js", 10 | "test:ci": "jest --config ./jest.config.js --coverage", 11 | "test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --config ./jest.config.js", 12 | "format": "prettier --write", 13 | "lint": "tslint '{src,integration}/**/*.ts' --project tsconfig.json" 14 | }, 15 | "dependencies": { 16 | "@angular/animations": "^18.0.5", 17 | "@angular/common": "^18.0.5", 18 | "@angular/compiler": "^18.0.5", 19 | "@angular/core": "^18.0.5", 20 | "@angular/forms": "^18.0.5", 21 | "@angular/platform-browser": "^18.0.5", 22 | "@angular/platform-browser-dynamic": "^18.0.5", 23 | "@angular/router": "^18.0.5", 24 | "@ngxs/logger-plugin": "^18.0.0", 25 | "@ngxs/store": "^18.0.0", 26 | "bootstrap": "^4.3.1", 27 | "core-js": "^3.6.0", 28 | "rxjs": "^6.5.3", 29 | "standard-version": "^9.5.0", 30 | "tslib": "^2.0.0", 31 | "zone.js": "~0.14.7" 32 | }, 33 | "devDependencies": { 34 | "@angular-devkit/build-angular": "^18.0.6", 35 | "@angular/cli": "^18.0.6", 36 | "@angular/compiler-cli": "^18.0.5", 37 | "@angular/language-service": "^18.0.5", 38 | "@commitlint/cli": "^8.2.0", 39 | "@commitlint/config-angular": "^8.1.0", 40 | "@commitlint/config-conventional": "^8.2.0", 41 | "@ngxs/devtools-plugin": "^18.0.0", 42 | "@types/jest": "^29.4.0", 43 | "codelyzer": "^6.0.0", 44 | "colors": "^1.3.3", 45 | "cypress": "^3.4.1", 46 | "husky": "^3.1.0", 47 | "jest": "^29.7.0", 48 | "jest-preset-angular": "^14.1.1", 49 | "lint-staged": "^9.2.5", 50 | "ng-packagr": "^18.0.0", 51 | "prettier": "^1.18.2", 52 | "start-server-and-test": "^1.10.0", 53 | "ts-node": "^8.3.0", 54 | "typescript": "~5.4.5" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": false 3 | } 4 | -------------------------------------------------------------------------------- /setup-jest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The public api for consumers of @ngxs-labs/package 3 | */ 4 | export * from './public-api'; 5 | -------------------------------------------------------------------------------- /src/lib/actions-executed.selector.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, getActionTypeFromInstance, ActionType } from '@ngxs/store'; 2 | import { ActionsExecutedState, ActionsExecutedStateModel } from './actions-executed.state'; 3 | 4 | export type ActionsExecuted = { [action: string]: number } | null; 5 | 6 | export function actionsExecuted(actionTypes: ActionType[]) { 7 | return createSelector([ActionsExecutedState], (state: ActionsExecutedStateModel) => { 8 | if (!actionTypes || actionTypes.length === 0) { 9 | if (Object.keys(state).length === 0) { 10 | return null; 11 | } 12 | return state; 13 | } 14 | 15 | return actionTypes.reduce((acc: ActionsExecuted, type: ActionType) => { 16 | const actionType = getActionTypeFromInstance(type); 17 | 18 | if (!actionType) { 19 | return acc; 20 | } 21 | 22 | if (state[actionType]) { 23 | return { ...acc, [actionType]: state[actionType] }; 24 | } 25 | 26 | return acc; 27 | }, null); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/actions-executed.state.ts: -------------------------------------------------------------------------------- 1 | import { State, NgxsOnInit, StateContext, Actions, getActionTypeFromInstance } from '@ngxs/store'; 2 | import { Subscription } from 'rxjs'; 3 | import { tap } from 'rxjs/operators'; 4 | import { OnDestroy, Injectable } from '@angular/core'; 5 | import { ActionStatus } from '@ngxs/store/src/actions-stream'; 6 | 7 | export interface ActionsExecutedStateModel { 8 | [action: string]: number; 9 | } 10 | 11 | @State({ 12 | name: 'ngxs_actions_executed' 13 | }) 14 | @Injectable() 15 | export class ActionsExecutedState implements NgxsOnInit, OnDestroy { 16 | private actionsExecutedSub: Subscription = new Subscription(); 17 | 18 | constructor(private actions$: Actions) {} 19 | 20 | public ngxsOnInit({ patchState, getState }: StateContext) { 21 | this.actionsExecutedSub = this.actions$ 22 | .pipe( 23 | tap((actionContext) => { 24 | const actionType = getActionTypeFromInstance(actionContext.action); 25 | if (!actionType) { 26 | return; 27 | } 28 | 29 | let count = getState()?.[actionType] || 0; 30 | 31 | if (actionContext.status !== ActionStatus.Dispatched) { 32 | count++; 33 | } 34 | 35 | patchState({ 36 | [actionType]: count 37 | }); 38 | }) 39 | ) 40 | .subscribe(); 41 | } 42 | 43 | public ngOnDestroy() { 44 | this.actionsExecutedSub.unsubscribe(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/actions-executing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { NgxsModule } from '@ngxs/store'; 3 | import { ActionsExecutedState } from './actions-executed.state'; 4 | import { ActionsExecutingState } from './actions-executing.state'; 5 | 6 | @NgModule({ 7 | imports: [NgxsModule.forFeature([ActionsExecutingState, ActionsExecutedState])] 8 | }) 9 | export class NgxsActionsExecutingModule { 10 | public static forRoot(): ModuleWithProviders { 11 | return { 12 | ngModule: NgxsActionsExecutingModule 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/actions-executing.selector.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, getActionTypeFromInstance, ActionType } from '@ngxs/store'; 2 | import { ActionsExecutingState, ActionsExecutingStateModel } from './actions-executing.state'; 3 | 4 | export type ActionsExecuting = { [action: string]: number } | null; 5 | 6 | function actionsExecutingFn(actionTypes: ActionType[], state: ActionsExecutingStateModel): ActionsExecuting { 7 | if (!actionTypes || actionTypes.length === 0) { 8 | if (Object.keys(state).length === 0) { 9 | return null; 10 | } 11 | return state; 12 | } 13 | 14 | return actionTypes.reduce((acc: ActionsExecuting, type: ActionType) => { 15 | const actionType = getActionTypeFromInstance(type); 16 | 17 | if (!actionType) { 18 | return acc; 19 | } 20 | 21 | if (state[actionType]) { 22 | return { ...acc, [actionType]: state[actionType] }; 23 | } 24 | 25 | return acc; 26 | }, null); 27 | } 28 | 29 | export function actionsExecuting(actionTypes?: ActionType[]): (state: ActionsExecutingStateModel) => ActionsExecuting { 30 | return createSelector( 31 | [ActionsExecutingState], 32 | (state: ActionsExecutingStateModel): ActionsExecuting => { 33 | return actionsExecutingFn(actionTypes, state); 34 | } 35 | ); 36 | } 37 | 38 | export function hasActionsExecuting(actionTypes?: ActionType[]): (state: ActionsExecutingStateModel) => boolean { 39 | return createSelector([ActionsExecutingState], (state: ActionsExecutingStateModel): boolean => { 40 | const result = actionsExecutingFn(actionTypes, state); 41 | return result === null ? false : Object.values(result).some((value) => value > 0); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/actions-executing.state.ts: -------------------------------------------------------------------------------- 1 | import { State, NgxsOnInit, StateContext, Actions, getActionTypeFromInstance } from '@ngxs/store'; 2 | import { Subscription } from 'rxjs'; 3 | import { tap } from 'rxjs/operators'; 4 | import { ActionStatus } from '@ngxs/store/src/actions-stream'; 5 | import { OnDestroy, Injectable } from '@angular/core'; 6 | 7 | export interface ActionsExecutingStateModel { 8 | [action: string]: number; 9 | } 10 | 11 | @State({ 12 | name: 'ngxs_actions_executing' 13 | }) 14 | @Injectable() 15 | export class ActionsExecutingState implements NgxsOnInit, OnDestroy { 16 | private _sub: Subscription = new Subscription(); 17 | 18 | constructor(private actions$: Actions) {} 19 | 20 | public ngxsOnInit({ patchState, getState }: StateContext) { 21 | this._sub = this.actions$ 22 | .pipe( 23 | tap((actionContext) => { 24 | const actionType = getActionTypeFromInstance(actionContext.action); 25 | if (!actionType) { 26 | return; 27 | } 28 | 29 | let count = getState()?.[actionType] || 0; 30 | 31 | if (actionContext.status === ActionStatus.Dispatched) { 32 | count++; 33 | } else if (count > 0) { 34 | count--; 35 | } 36 | 37 | patchState({ 38 | [actionType]: count 39 | }); 40 | }) 41 | ) 42 | .subscribe(); 43 | } 44 | 45 | public ngOnDestroy() { 46 | this._sub.unsubscribe(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../dist/ngxs-actions-executing", 4 | "lib": { 5 | "entryFile": "public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/ng-package.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../dist/ngxs-actions-executing", 4 | "lib": { 5 | "entryFile": "public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngxs-labs/actions-executing", 3 | "version": "19.0.0", 4 | "peerDependencies": { 5 | "@ngxs/store": ">=4.0.0 <20.0.0" 6 | }, 7 | "homepage": "https://github.com/ngxs-labs/actions-executing#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/ngxs-labs/actions-executing.git" 11 | }, 12 | "dependencies": { 13 | "tslib": "^2.0.0" 14 | }, 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /src/public-api.ts: -------------------------------------------------------------------------------- 1 | export { NgxsActionsExecutingModule } from './lib/actions-executing.module'; 2 | export { actionsExecuting, hasActionsExecuting, ActionsExecuting } from './lib/actions-executing.selector'; 3 | export { actionsExecuted, ActionsExecuted } from './lib/actions-executed.selector'; 4 | -------------------------------------------------------------------------------- /src/tests/actions-executed.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgxsActionsExecutingModule, actionsExecuted, ActionsExecuted } from '..'; 2 | import { TestBed, fakeAsync, tick } from '@angular/core/testing'; 3 | import { NgxsModule, Store, State, Action, StateContext, NgxsOnInit } from '@ngxs/store'; 4 | import { of, throwError } from 'rxjs'; 5 | import { delay } from 'rxjs/operators'; 6 | import { Injectable } from '@angular/core'; 7 | 8 | describe('actionsExecuted', () => { 9 | let store: Store; 10 | 11 | class Action1 { 12 | public static type = 'ACTION 1'; 13 | } 14 | 15 | class Action2 { 16 | public static type = 'ACTION 2'; 17 | } 18 | 19 | class Action3 { 20 | public static type = 'ACTION 3'; 21 | } 22 | 23 | class ErrorAction1 { 24 | public static type = 'ERROR ACTION 1'; 25 | } 26 | 27 | class AsyncAction1 { 28 | public static type = 'ASYNC ACTION 1'; 29 | } 30 | 31 | class AsyncAction2 { 32 | public static type = 'ASYNC ACTION 2'; 33 | } 34 | 35 | class AsyncAction3 { 36 | public static type = 'ASYNC ACTION 3'; 37 | } 38 | 39 | class AsyncErrorAction1 { 40 | public static type = 'ASYNC ERROR ACTION 1'; 41 | } 42 | 43 | class NestedAsyncAction1 { 44 | public static type = 'NESTED ASYNC ACTION 1'; 45 | } 46 | 47 | class NestedAsyncAction2 { 48 | public static type = 'NESTED ASYNC ACTION 2'; 49 | } 50 | 51 | class NestedAsyncAction3 { 52 | public static type = 'NESTED ASYNC ACTION 3'; 53 | } 54 | 55 | class NestedAsyncAction4 { 56 | public static type = 'NESTED ASYNC ACTION 4'; 57 | } 58 | 59 | class NestedAsyncAction5 { 60 | public static type = 'NESTED ASYNC ACTION 5'; 61 | } 62 | 63 | class NestedAsyncAction6 { 64 | public static type = 'NESTED ASYNC ACTION 6'; 65 | } 66 | 67 | @State({ 68 | name: 'test' 69 | }) 70 | @Injectable() 71 | class TestState { 72 | @Action([Action1]) 73 | public action1() {} 74 | 75 | @Action([AsyncAction1]) 76 | public asyncAction1() { 77 | return of({}).pipe(delay(0)); 78 | } 79 | 80 | @Action([AsyncAction2]) 81 | public asyncAction2() { 82 | return of({}).pipe(delay(0)); 83 | } 84 | 85 | @Action(AsyncErrorAction1) 86 | public asyncError() { 87 | return throwError(new Error('this is a test error')).pipe(delay(0)); 88 | } 89 | 90 | @Action(ErrorAction1) 91 | public onError() { 92 | return throwError(new Error('this is a test error')); 93 | } 94 | } 95 | 96 | @State<{}>({ 97 | name: 'nested_actions_1' 98 | }) 99 | @Injectable() 100 | class NestedActions1State { 101 | @Action(NestedAsyncAction1) 102 | public nestedAsyncAction1({ dispatch }: StateContext<{}>) { 103 | return dispatch(new NestedAsyncAction2()).pipe(delay(0)); 104 | } 105 | 106 | @Action(NestedAsyncAction2) 107 | public nestedAsyncAction2({ dispatch }: StateContext<{}>) { 108 | return dispatch(new NestedAsyncAction3()).pipe(delay(0)); 109 | } 110 | 111 | @Action(NestedAsyncAction3) 112 | public nestedAsyncAction3() { 113 | return of({}).pipe(delay(0)); 114 | } 115 | } 116 | 117 | @State({ 118 | name: 'nested_actions_2' 119 | }) 120 | @Injectable() 121 | class NestedActions2State { 122 | @Action([NestedAsyncAction4, NestedAsyncAction5]) 123 | public combinedAction({ dispatch }: StateContext<{}>) { 124 | return dispatch(new NestedAsyncAction6()).pipe(delay(0)); 125 | } 126 | 127 | @Action(NestedAsyncAction5) 128 | public nestedAsyncAction5() { 129 | return of({}).pipe(delay(100)); 130 | } 131 | 132 | @Action(NestedAsyncAction6) 133 | public nestedAsyncAction6() { 134 | return of({}).pipe(delay(0)); 135 | } 136 | } 137 | 138 | @State({ 139 | name: 'ngxs_on_init_state' 140 | }) 141 | @Injectable() 142 | class NgxsOnInitState implements NgxsOnInit { 143 | ngxsOnInit(ctx: StateContext<{}>) { 144 | ctx.dispatch(new Action3()); 145 | ctx.dispatch(new AsyncAction3()); 146 | } 147 | 148 | @Action([Action3]) 149 | public action3() {} 150 | 151 | @Action([AsyncAction3]) 152 | public asyncAction3() { 153 | return of({}).pipe(delay(0)); 154 | } 155 | } 156 | 157 | describe('NgxsOnInit', () => { 158 | describe('Sync Action', () => { 159 | it('should be null when dispatching sync from ngxsOnInit', fakeAsync(() => { 160 | TestBed.configureTestingModule({ 161 | imports: [NgxsModule.forRoot([NgxsOnInitState]), NgxsActionsExecutingModule.forRoot()] 162 | }); 163 | 164 | store = TestBed.get(Store); 165 | 166 | tick(1); 167 | 168 | expect(store.selectSnapshot(actionsExecuted([Action3]))).toBe(null); 169 | })); 170 | }); 171 | 172 | describe('Async Action', () => { 173 | it('should count when dispatching async from ngxsOnInit', fakeAsync(() => { 174 | TestBed.configureTestingModule({ 175 | imports: [NgxsModule.forRoot([NgxsOnInitState]), NgxsActionsExecutingModule.forRoot()] 176 | }); 177 | 178 | store = TestBed.get(Store); 179 | 180 | tick(1); 181 | expect(store.selectSnapshot(actionsExecuted([AsyncAction3]))).toEqual({ [AsyncAction3.type]: 1 }); 182 | tick(1); 183 | expect(store.selectSnapshot(actionsExecuted([AsyncAction3]))).toEqual({ [AsyncAction3.type]: 1 }); 184 | })); 185 | }); 186 | }); 187 | 188 | describe('', () => { 189 | beforeEach(() => { 190 | TestBed.configureTestingModule({ 191 | imports: [ 192 | NgxsModule.forRoot([TestState, NestedActions1State, NestedActions2State]), 193 | NgxsActionsExecutingModule.forRoot() 194 | ] 195 | }); 196 | 197 | store = TestBed.inject(Store); 198 | }); 199 | 200 | describe('Single Action', () => { 201 | describe('Sync Action', () => { 202 | it('should be 1', () => { 203 | store.dispatch(Action1); 204 | 205 | const snapshot = store.selectSnapshot(actionsExecuted([Action1])); 206 | expect(snapshot).toEqual({ [Action1.type]: 1 }); 207 | }); 208 | 209 | it('should be null before dispatch and 1 after complete', () => { 210 | const actionStatus: ActionsExecuted[] = []; 211 | 212 | store.select(actionsExecuted([Action1])).subscribe((_actionsExecuted) => { 213 | actionStatus.push(_actionsExecuted); 214 | }); 215 | 216 | store.dispatch(new Action1()); 217 | 218 | expect(actionStatus).toEqual([null, { [Action1.type]: 1 }]); 219 | }); 220 | 221 | it('should be null before dispatch and 1 after error', () => { 222 | const actionStatus: ActionsExecuted[] = []; 223 | 224 | store.select(actionsExecuted([ErrorAction1])).subscribe((_actionsExecuted) => { 225 | actionStatus.push(_actionsExecuted); 226 | }); 227 | 228 | store.dispatch(new ErrorAction1()); 229 | expect(actionStatus).toEqual([null, { [ErrorAction1.type]: 1 }]); 230 | }); 231 | }); 232 | 233 | describe('Async Action', () => { 234 | it('should be null before is completed', fakeAsync(() => { 235 | store.dispatch(AsyncAction1); 236 | 237 | let snapshot = store.selectSnapshot(actionsExecuted([AsyncAction1])); 238 | expect(snapshot).toEqual(null); 239 | 240 | tick(); 241 | 242 | snapshot = store.selectSnapshot(actionsExecuted([AsyncAction1])); 243 | expect(snapshot).toEqual({ 'ASYNC ACTION 1': 1 }); 244 | })); 245 | 246 | it('should be null before dispatch and 1 after complete ', fakeAsync(() => { 247 | const actionStatus: ActionsExecuted[] = []; 248 | 249 | store.select(actionsExecuted([AsyncAction1])).subscribe((_actionsExecuted) => { 250 | actionStatus.push(_actionsExecuted); 251 | }); 252 | 253 | store.dispatch(new AsyncAction1()); 254 | tick(1); 255 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 1 }]); 256 | })); 257 | 258 | it('should be null before dispatch and q after error', fakeAsync(() => { 259 | const actionStatus: ActionsExecuted[] = []; 260 | 261 | store.select(actionsExecuted([AsyncErrorAction1])).subscribe((_actionsExecuted) => { 262 | actionStatus.push(_actionsExecuted); 263 | }); 264 | 265 | store.dispatch(new AsyncErrorAction1()).subscribe({ 266 | error: (err) => { 267 | expect(err).toBeDefined(); 268 | } 269 | }); 270 | 271 | tick(1); 272 | expect(actionStatus).toEqual([null, { [AsyncErrorAction1.type]: 1 }]); 273 | })); 274 | }); 275 | }); 276 | 277 | describe('Multiple Actions', () => { 278 | describe('sync', () => { 279 | it('should count actions when completed', () => { 280 | const actionStatus: ActionsExecuted[] = []; 281 | 282 | store.select(actionsExecuted([Action1, Action2])).subscribe((_actionsExecuted) => { 283 | actionStatus.push(_actionsExecuted); 284 | }); 285 | 286 | store.dispatch(new Action1()); 287 | store.dispatch(new Action2()); 288 | expect(actionStatus).toEqual([ 289 | null, 290 | { [Action1.type]: 1 }, 291 | { [Action1.type]: 1 }, 292 | { [Action1.type]: 1, [Action2.type]: 1 } 293 | ]); 294 | }); 295 | 296 | it('should count action and error when completed', () => { 297 | const actionStatus: ActionsExecuted[] = []; 298 | 299 | store.select(actionsExecuted([Action1, ErrorAction1])).subscribe((_actionsExecuted) => { 300 | actionStatus.push(_actionsExecuted); 301 | }); 302 | 303 | store.dispatch(new Action1()); 304 | store.dispatch(new ErrorAction1()); 305 | expect(actionStatus).toEqual([ 306 | null, 307 | { [Action1.type]: 1 }, 308 | { [Action1.type]: 1 }, 309 | { [Action1.type]: 1, [ErrorAction1.type]: 1 } 310 | ]); 311 | }); 312 | }); 313 | describe('async', () => { 314 | it('should count actions when completed', fakeAsync(() => { 315 | const actionStatus: ActionsExecuted[] = []; 316 | 317 | store.select(actionsExecuted([AsyncAction1, AsyncAction2])).subscribe((_actionsExecuted) => { 318 | actionStatus.push(_actionsExecuted); 319 | }); 320 | 321 | store.dispatch(new AsyncAction1()); 322 | tick(1); 323 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 1 }]); 324 | store.dispatch(new AsyncAction2()); 325 | tick(1); 326 | expect(actionStatus).toEqual([ 327 | null, 328 | { [AsyncAction1.type]: 1 }, 329 | { [AsyncAction1.type]: 1 }, 330 | { [AsyncAction1.type]: 1, [AsyncAction2.type]: 1 } 331 | ]); 332 | })); 333 | 334 | it('should count action and error when completed', fakeAsync(() => { 335 | const actionStatus: ActionsExecuted[] = []; 336 | 337 | store.select(actionsExecuted([AsyncAction1, AsyncErrorAction1])).subscribe((_actionsExecuted) => { 338 | actionStatus.push(_actionsExecuted); 339 | }); 340 | 341 | store.dispatch(new AsyncAction1()); 342 | tick(1); 343 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 1 }]); 344 | store.dispatch(new AsyncErrorAction1()); 345 | tick(1); 346 | expect(actionStatus).toEqual([ 347 | null, 348 | { [AsyncAction1.type]: 1 }, 349 | { [AsyncAction1.type]: 1 }, 350 | { [AsyncAction1.type]: 1, [AsyncErrorAction1.type]: 1 } 351 | ]); 352 | })); 353 | 354 | it('should count all actions when completed', fakeAsync(() => { 355 | const actionStatus: ActionsExecuted[] = []; 356 | 357 | store.select(actionsExecuted([AsyncAction1])).subscribe((_actionsExecuted) => { 358 | actionStatus.push(_actionsExecuted); 359 | }); 360 | 361 | store.dispatch(new AsyncAction1()); 362 | expect(actionStatus).toEqual([null]); 363 | store.dispatch(new AsyncAction1()); 364 | tick(1); 365 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 1 }, { [AsyncAction1.type]: 2 }]); 366 | })); 367 | 368 | it('should count all actions when completed (case 2)', fakeAsync(() => { 369 | const actionStatus: ActionsExecuted[] = []; 370 | 371 | store.select(actionsExecuted([AsyncAction1])).subscribe((_actionsExecuted) => { 372 | actionStatus.push(_actionsExecuted); 373 | }); 374 | 375 | store.dispatch(new AsyncAction1()); 376 | store.dispatch(new AsyncAction1()); 377 | expect(actionStatus).toEqual([null]); 378 | tick(1); 379 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 1 }, { [AsyncAction1.type]: 2 }]); 380 | })); 381 | 382 | describe('nested actions 1', () => { 383 | it('should be executing on nested actions', fakeAsync(() => { 384 | const nestedAction1Status: ActionsExecuted[] = []; 385 | const nestedAction2Status: ActionsExecuted[] = []; 386 | const nestedAction3Status: ActionsExecuted[] = []; 387 | 388 | const combinedActionStatus: ActionsExecuted[] = []; 389 | 390 | store.select(actionsExecuted([NestedAsyncAction1])).subscribe((_actionsExecuted) => { 391 | nestedAction1Status.push(_actionsExecuted); 392 | }); 393 | 394 | store.select(actionsExecuted([NestedAsyncAction2])).subscribe((_actionsExecuted) => { 395 | nestedAction2Status.push(_actionsExecuted); 396 | }); 397 | 398 | store.select(actionsExecuted([NestedAsyncAction3])).subscribe((_actionsExecuted) => { 399 | nestedAction3Status.push(_actionsExecuted); 400 | }); 401 | 402 | store 403 | .select(actionsExecuted([NestedAsyncAction1, NestedAsyncAction2, NestedAsyncAction3])) 404 | .subscribe((_actionsExecuted) => { 405 | combinedActionStatus.push(_actionsExecuted); 406 | }); 407 | 408 | store.dispatch(new NestedAsyncAction1()); 409 | tick(1); 410 | expect(nestedAction1Status).toEqual([null, { [NestedAsyncAction1.type]: 1 }]); 411 | expect(nestedAction2Status).toEqual([ 412 | null, 413 | { [NestedAsyncAction2.type]: 1 }, 414 | { [NestedAsyncAction2.type]: 1 } 415 | ]); 416 | expect(nestedAction3Status).toEqual([ 417 | null, 418 | { [NestedAsyncAction3.type]: 1 }, 419 | { [NestedAsyncAction3.type]: 1 }, 420 | { [NestedAsyncAction3.type]: 1 } 421 | ]); 422 | 423 | expect(combinedActionStatus).toEqual([ 424 | null, 425 | { [NestedAsyncAction3.type]: 1 }, 426 | { [NestedAsyncAction2.type]: 1, [NestedAsyncAction3.type]: 1 }, 427 | { 428 | [NestedAsyncAction1.type]: 1, 429 | [NestedAsyncAction2.type]: 1, 430 | [NestedAsyncAction3.type]: 1 431 | } 432 | ]); 433 | })); 434 | }); 435 | 436 | describe('nested actions 2', () => { 437 | it('should be executing on nested actions (scenario 1)', fakeAsync(() => { 438 | const nestedAction4Status: ActionsExecuted[] = []; 439 | const nestedAction5Status: ActionsExecuted[] = []; 440 | const nestedAction6Status: ActionsExecuted[] = []; 441 | 442 | const combinedAction45Status: ActionsExecuted[] = []; 443 | const combinedAction456Status: ActionsExecuted[] = []; 444 | 445 | store.select(actionsExecuted([NestedAsyncAction4])).subscribe((_actionsExecuted) => { 446 | nestedAction4Status.push(_actionsExecuted); 447 | }); 448 | 449 | store.select(actionsExecuted([NestedAsyncAction5])).subscribe((_actionsExecuted) => { 450 | nestedAction5Status.push(_actionsExecuted); 451 | }); 452 | 453 | store.select(actionsExecuted([NestedAsyncAction6])).subscribe((_actionsExecuted) => { 454 | nestedAction6Status.push(_actionsExecuted); 455 | }); 456 | 457 | store 458 | .select(actionsExecuted([NestedAsyncAction4, NestedAsyncAction5])) 459 | .subscribe((_actionsExecuted) => { 460 | combinedAction45Status.push(_actionsExecuted); 461 | }); 462 | 463 | store 464 | .select(actionsExecuted([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 465 | .subscribe((_actionsExecuted) => { 466 | combinedAction456Status.push(_actionsExecuted); 467 | }); 468 | 469 | store.dispatch(new NestedAsyncAction4()); 470 | tick(1); 471 | expect(nestedAction4Status).toEqual([null, { [NestedAsyncAction4.type]: 1 }]); 472 | expect(nestedAction5Status).toEqual([null]); 473 | expect(nestedAction6Status).toEqual([ 474 | null, 475 | { [NestedAsyncAction6.type]: 1 }, 476 | { [NestedAsyncAction6.type]: 1 } 477 | ]); 478 | 479 | expect(combinedAction45Status).toEqual([null, { [NestedAsyncAction4.type]: 1 }]); 480 | expect(combinedAction456Status).toEqual([ 481 | null, 482 | { [NestedAsyncAction6.type]: 1 }, 483 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction6.type]: 1 } 484 | ]); 485 | })); 486 | 487 | it('should be executing on nested actions (scenario 2)', fakeAsync(() => { 488 | const nestedAction4Status: ActionsExecuted[] = []; 489 | const nestedAction5Status: ActionsExecuted[] = []; 490 | const nestedAction6Status: ActionsExecuted[] = []; 491 | 492 | const combinedAction45Status: ActionsExecuted[] = []; 493 | const combinedAction456Status: ActionsExecuted[] = []; 494 | 495 | store.select(actionsExecuted([NestedAsyncAction4])).subscribe((_actionsExecuted) => { 496 | nestedAction4Status.push(_actionsExecuted); 497 | }); 498 | 499 | store.select(actionsExecuted([NestedAsyncAction5])).subscribe((_actionsExecuted) => { 500 | nestedAction5Status.push(_actionsExecuted); 501 | }); 502 | 503 | store.select(actionsExecuted([NestedAsyncAction6])).subscribe((_actionsExecuted) => { 504 | nestedAction6Status.push(_actionsExecuted); 505 | }); 506 | 507 | store 508 | .select(actionsExecuted([NestedAsyncAction4, NestedAsyncAction5])) 509 | .subscribe((_actionsExecuted) => { 510 | combinedAction45Status.push(_actionsExecuted); 511 | }); 512 | 513 | store 514 | .select(actionsExecuted([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 515 | .subscribe((_actionsExecuted) => { 516 | combinedAction456Status.push(_actionsExecuted); 517 | }); 518 | 519 | store.dispatch([new NestedAsyncAction4(), new NestedAsyncAction5()]); 520 | tick(1); 521 | expect(nestedAction4Status).toEqual([null, { [NestedAsyncAction4.type]: 1 }]); 522 | expect(nestedAction5Status).toEqual([null]); 523 | expect(nestedAction6Status).toEqual([ 524 | null, 525 | { [NestedAsyncAction6.type]: 1 }, 526 | { [NestedAsyncAction6.type]: 2 }, 527 | { [NestedAsyncAction6.type]: 2 } 528 | ]); 529 | tick(100); 530 | expect(nestedAction4Status).toEqual([ 531 | null, 532 | { [NestedAsyncAction4.type]: 1 }, 533 | { [NestedAsyncAction4.type]: 1 } 534 | ]); 535 | expect(nestedAction5Status).toEqual([null, { [NestedAsyncAction5.type]: 1 }]); 536 | expect(nestedAction6Status).toEqual([ 537 | null, 538 | { [NestedAsyncAction6.type]: 1 }, 539 | { [NestedAsyncAction6.type]: 2 }, 540 | { [NestedAsyncAction6.type]: 2 }, 541 | { [NestedAsyncAction6.type]: 2 } 542 | ]); 543 | 544 | expect(combinedAction45Status).toEqual([ 545 | null, 546 | { [NestedAsyncAction4.type]: 1 }, 547 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 } 548 | ]); 549 | expect(combinedAction456Status).toEqual([ 550 | null, 551 | { [NestedAsyncAction6.type]: 1 }, 552 | { [NestedAsyncAction6.type]: 2 }, 553 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction6.type]: 2 }, 554 | { 555 | [NestedAsyncAction4.type]: 1, 556 | [NestedAsyncAction5.type]: 1, 557 | [NestedAsyncAction6.type]: 2 558 | } 559 | ]); 560 | })); 561 | 562 | it('should be executing on nested actions (scenario 3)', fakeAsync(() => { 563 | const nestedAction4Status: ActionsExecuted[] = []; 564 | const nestedAction5Status: ActionsExecuted[] = []; 565 | const nestedAction6Status: ActionsExecuted[] = []; 566 | 567 | const combinedAction45Status: ActionsExecuted[] = []; 568 | const combinedAction456Status: ActionsExecuted[] = []; 569 | 570 | store.select(actionsExecuted([NestedAsyncAction4])).subscribe((_actionsExecuted) => { 571 | nestedAction4Status.push(_actionsExecuted); 572 | }); 573 | 574 | store.select(actionsExecuted([NestedAsyncAction5])).subscribe((_actionsExecuted) => { 575 | nestedAction5Status.push(_actionsExecuted); 576 | }); 577 | 578 | store.select(actionsExecuted([NestedAsyncAction6])).subscribe((_actionsExecuted) => { 579 | nestedAction6Status.push(_actionsExecuted); 580 | }); 581 | 582 | store 583 | .select(actionsExecuted([NestedAsyncAction4, NestedAsyncAction5])) 584 | .subscribe((_actionsExecuted) => { 585 | combinedAction45Status.push(_actionsExecuted); 586 | }); 587 | 588 | store 589 | .select(actionsExecuted([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 590 | .subscribe((_actionsExecuted) => { 591 | combinedAction456Status.push(_actionsExecuted); 592 | }); 593 | 594 | store.dispatch(new NestedAsyncAction5()); 595 | tick(1); 596 | expect(nestedAction4Status).toEqual([null]); 597 | expect(nestedAction5Status).toEqual([null]); 598 | expect(nestedAction6Status).toEqual([null, { [NestedAsyncAction6.type]: 1 }]); 599 | tick(100); 600 | expect(nestedAction4Status).toEqual([null]); 601 | expect(nestedAction5Status).toEqual([null, { [NestedAsyncAction5.type]: 1 }]); 602 | expect(nestedAction6Status).toEqual([ 603 | null, 604 | { [NestedAsyncAction6.type]: 1 }, 605 | { [NestedAsyncAction6.type]: 1 } 606 | ]); 607 | 608 | expect(combinedAction45Status).toEqual([null, { [NestedAsyncAction5.type]: 1 }]); 609 | expect(combinedAction456Status).toEqual([ 610 | null, 611 | { [NestedAsyncAction6.type]: 1 }, 612 | { [NestedAsyncAction5.type]: 1, [NestedAsyncAction6.type]: 1 } 613 | ]); 614 | })); 615 | 616 | it('should be executing on nested actions (scenario 4)', fakeAsync(() => { 617 | const nestedAction4Status: ActionsExecuted[] = []; 618 | const nestedAction5Status: ActionsExecuted[] = []; 619 | const nestedAction6Status: ActionsExecuted[] = []; 620 | 621 | const combinedAction45Status: ActionsExecuted[] = []; 622 | const combinedAction456Status: ActionsExecuted[] = []; 623 | 624 | store.select(actionsExecuted([NestedAsyncAction4])).subscribe((_actionsExecuted) => { 625 | nestedAction4Status.push(_actionsExecuted); 626 | }); 627 | 628 | store.select(actionsExecuted([NestedAsyncAction5])).subscribe((_actionsExecuted) => { 629 | nestedAction5Status.push(_actionsExecuted); 630 | }); 631 | 632 | store.select(actionsExecuted([NestedAsyncAction6])).subscribe((_actionsExecuted) => { 633 | nestedAction6Status.push(_actionsExecuted); 634 | }); 635 | 636 | store 637 | .select(actionsExecuted([NestedAsyncAction4, NestedAsyncAction5])) 638 | .subscribe((_actionsExecuted) => { 639 | combinedAction45Status.push(_actionsExecuted); 640 | }); 641 | 642 | store 643 | .select(actionsExecuted([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 644 | .subscribe((_actionsExecuted) => { 645 | combinedAction456Status.push(_actionsExecuted); 646 | }); 647 | 648 | store.dispatch([new NestedAsyncAction4(), new NestedAsyncAction5(), new NestedAsyncAction6()]); 649 | tick(1); 650 | expect(nestedAction4Status).toEqual([null, { [NestedAsyncAction4.type]: 1 }]); 651 | expect(nestedAction5Status).toEqual([null]); 652 | expect(nestedAction6Status).toEqual([ 653 | null, 654 | { [NestedAsyncAction6.type]: 1 }, 655 | { [NestedAsyncAction6.type]: 2 }, 656 | { [NestedAsyncAction6.type]: 3 }, 657 | { [NestedAsyncAction6.type]: 3 } 658 | ]); 659 | tick(100); 660 | expect(nestedAction4Status).toEqual([ 661 | null, 662 | { [NestedAsyncAction4.type]: 1 }, 663 | { [NestedAsyncAction4.type]: 1 } 664 | ]); 665 | expect(nestedAction5Status).toEqual([null, { [NestedAsyncAction5.type]: 1 }]); 666 | expect(nestedAction6Status).toEqual([ 667 | null, 668 | { [NestedAsyncAction6.type]: 1 }, 669 | { [NestedAsyncAction6.type]: 2 }, 670 | { [NestedAsyncAction6.type]: 3 }, 671 | { [NestedAsyncAction6.type]: 3 }, 672 | { [NestedAsyncAction6.type]: 3 } 673 | ]); 674 | 675 | expect(combinedAction45Status).toEqual([ 676 | null, 677 | { [NestedAsyncAction4.type]: 1 }, 678 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 } 679 | ]); 680 | expect(combinedAction456Status).toEqual([ 681 | null, 682 | { [NestedAsyncAction6.type]: 1 }, 683 | { [NestedAsyncAction6.type]: 2 }, 684 | { 685 | [NestedAsyncAction6.type]: 3 686 | }, 687 | { 688 | [NestedAsyncAction4.type]: 1, 689 | [NestedAsyncAction6.type]: 3 690 | }, 691 | { 692 | [NestedAsyncAction4.type]: 1, 693 | [NestedAsyncAction5.type]: 1, 694 | [NestedAsyncAction6.type]: 3 695 | } 696 | ]); 697 | })); 698 | }); 699 | }); 700 | }); 701 | 702 | describe('No Actions', () => { 703 | describe('sync', () => { 704 | it('should be executing between dispatch and complete', () => { 705 | const actionStatus: ActionsExecuted[] = []; 706 | 707 | store.select(actionsExecuted([])).subscribe((_actionsExecuted) => { 708 | actionStatus.push(_actionsExecuted); 709 | }); 710 | 711 | store.dispatch(new Action1()); 712 | store.dispatch(new Action2()); 713 | expect(actionStatus).toEqual([ 714 | null, 715 | { [Action1.type]: 0 }, 716 | { [Action1.type]: 1 }, 717 | { [Action1.type]: 1, [Action2.type]: 0 }, 718 | { [Action1.type]: 1, [Action2.type]: 1 } 719 | ]); 720 | }); 721 | }); 722 | 723 | describe('async', () => { 724 | it('should be executing between dispatch and complete ', fakeAsync(() => { 725 | const actionStatus: ActionsExecuted[] = []; 726 | 727 | store.select(actionsExecuted([])).subscribe((_actionsExecuted) => { 728 | actionStatus.push(_actionsExecuted); 729 | }); 730 | 731 | store.dispatch(new AsyncAction1()); 732 | tick(1); 733 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 0 }, { [AsyncAction1.type]: 1 }]); 734 | store.dispatch(new AsyncAction2()); 735 | tick(1); 736 | expect(actionStatus).toEqual([ 737 | null, 738 | { [AsyncAction1.type]: 0 }, 739 | { [AsyncAction1.type]: 1 }, 740 | { [AsyncAction1.type]: 1, [AsyncAction2.type]: 0 }, 741 | { [AsyncAction1.type]: 1, [AsyncAction2.type]: 1 } 742 | ]); 743 | })); 744 | }); 745 | }); 746 | }); 747 | }); 748 | -------------------------------------------------------------------------------- /src/tests/actions-executing-module.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgxsActionsExecutingModule } from '..'; 2 | 3 | describe('actions-executing', () => { 4 | it('should successfully create module', () => { 5 | const ofExecModule = new NgxsActionsExecutingModule(); 6 | 7 | expect(ofExecModule).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/tests/actions-executing.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgxsActionsExecutingModule, ActionsExecuting, actionsExecuting } from '..'; 2 | import { TestBed, fakeAsync, tick } from '@angular/core/testing'; 3 | import { NgxsModule, Store, State, Action, StateContext, NgxsOnInit } from '@ngxs/store'; 4 | import { of, throwError } from 'rxjs'; 5 | import { delay } from 'rxjs/operators'; 6 | import { Injectable } from '@angular/core'; 7 | 8 | describe('actionsExecuting', () => { 9 | let store: Store; 10 | 11 | class Action1 { 12 | public static type = 'ACTION 1'; 13 | } 14 | 15 | class Action2 { 16 | public static type = 'ACTION 2'; 17 | } 18 | 19 | class Action3 { 20 | public static type = 'ACTION 3'; 21 | } 22 | 23 | class ErrorAction1 { 24 | public static type = 'ERROR ACTION 1'; 25 | } 26 | 27 | class AsyncAction1 { 28 | public static type = 'ASYNC ACTION 1'; 29 | } 30 | 31 | class AsyncAction2 { 32 | public static type = 'ASYNC ACTION 2'; 33 | } 34 | 35 | class AsyncAction3 { 36 | public static type = 'ASYNC ACTION 3'; 37 | } 38 | 39 | class AsyncErrorAction1 { 40 | public static type = 'ASYNC ERROR ACTION 1'; 41 | } 42 | 43 | class NestedAsyncAction1 { 44 | public static type = 'NESTED ASYNC ACTION 1'; 45 | } 46 | 47 | class NestedAsyncAction2 { 48 | public static type = 'NESTED ASYNC ACTION 2'; 49 | } 50 | 51 | class NestedAsyncAction3 { 52 | public static type = 'NESTED ASYNC ACTION 3'; 53 | } 54 | 55 | class NestedAsyncAction4 { 56 | public static type = 'NESTED ASYNC ACTION 4'; 57 | } 58 | 59 | class NestedAsyncAction5 { 60 | public static type = 'NESTED ASYNC ACTION 5'; 61 | } 62 | 63 | class NestedAsyncAction6 { 64 | public static type = 'NESTED ASYNC ACTION 6'; 65 | } 66 | 67 | @State({ 68 | name: 'test' 69 | }) 70 | @Injectable() 71 | class TestState { 72 | @Action([Action1]) 73 | public action1() {} 74 | 75 | @Action([AsyncAction1]) 76 | public asyncAction1() { 77 | return of({}).pipe(delay(0)); 78 | } 79 | 80 | @Action([AsyncAction2]) 81 | public asyncAction2() { 82 | return of({}).pipe(delay(0)); 83 | } 84 | 85 | @Action(AsyncErrorAction1) 86 | public asyncError() { 87 | return throwError(new Error('this is a test error')).pipe(delay(0)); 88 | } 89 | 90 | @Action(ErrorAction1) 91 | public onError() { 92 | return throwError(new Error('this is a test error')); 93 | } 94 | } 95 | 96 | @State<{}>({ 97 | name: 'nested_actions_1' 98 | }) 99 | @Injectable() 100 | class NestedActions1State { 101 | @Action(NestedAsyncAction1) 102 | public nestedAsyncAction1({ dispatch }: StateContext<{}>) { 103 | return dispatch(new NestedAsyncAction2()).pipe(delay(0)); 104 | } 105 | 106 | @Action(NestedAsyncAction2) 107 | public nestedAsyncAction2({ dispatch }: StateContext<{}>) { 108 | return dispatch(new NestedAsyncAction3()).pipe(delay(0)); 109 | } 110 | 111 | @Action(NestedAsyncAction3) 112 | public nestedAsyncAction3() { 113 | return of({}).pipe(delay(0)); 114 | } 115 | } 116 | 117 | @State({ 118 | name: 'nested_actions_2' 119 | }) 120 | @Injectable() 121 | class NestedActions2State { 122 | @Action([NestedAsyncAction4, NestedAsyncAction5]) 123 | public combinedAction({ dispatch }: StateContext<{}>) { 124 | return dispatch(new NestedAsyncAction6()).pipe(delay(0)); 125 | } 126 | 127 | @Action(NestedAsyncAction5) 128 | public nestedAsyncAction5() { 129 | return of({}).pipe(delay(100)); 130 | } 131 | 132 | @Action(NestedAsyncAction6) 133 | public nestedAsyncAction6() { 134 | return of({}).pipe(delay(0)); 135 | } 136 | } 137 | 138 | @State({ 139 | name: 'ngxs_on_init_state' 140 | }) 141 | @Injectable() 142 | class NgxsOnInitState implements NgxsOnInit { 143 | ngxsOnInit(ctx: StateContext<{}>) { 144 | ctx.dispatch(new Action3()); 145 | ctx.dispatch(new AsyncAction3()); 146 | } 147 | 148 | @Action([Action3]) 149 | public action3() {} 150 | 151 | @Action([AsyncAction3]) 152 | public asyncAction3() { 153 | return of({}).pipe(delay(0)); 154 | } 155 | } 156 | 157 | describe('NgxsOnInit', () => { 158 | describe('Sync and Async Action', () => { 159 | it('should be null when dispatching async or sync from ngxsOnInit', fakeAsync(() => { 160 | TestBed.configureTestingModule({ 161 | imports: [NgxsModule.forRoot([NgxsOnInitState]), NgxsActionsExecutingModule.forRoot()] 162 | }); 163 | 164 | store = TestBed.get(Store); 165 | 166 | tick(1); 167 | 168 | expect(store.selectSnapshot(actionsExecuting([Action3]))).toBe(null); 169 | expect(store.selectSnapshot(actionsExecuting([AsyncAction3]))).toEqual(null); 170 | })); 171 | }); 172 | }); 173 | 174 | describe('', () => { 175 | beforeEach(() => { 176 | TestBed.configureTestingModule({ 177 | imports: [ 178 | NgxsModule.forRoot([TestState, NestedActions1State, NestedActions2State]), 179 | NgxsActionsExecutingModule.forRoot() 180 | ] 181 | }); 182 | 183 | store = TestBed.get(Store); 184 | }); 185 | 186 | describe('Single Action', () => { 187 | describe('Sync Action', () => { 188 | it('should be null', () => { 189 | store.dispatch(Action1); 190 | 191 | const snapshot = store.selectSnapshot(actionsExecuting([Action1])); 192 | expect(snapshot).toBe(null); 193 | }); 194 | 195 | it('should be executing between dispatch and complete', () => { 196 | const actionStatus: ActionsExecuting[] = []; 197 | 198 | store.select(actionsExecuting([Action1])).subscribe((_actionsExecuting) => { 199 | actionStatus.push(_actionsExecuting); 200 | }); 201 | 202 | store.dispatch(new Action1()); 203 | expect(actionStatus).toEqual([null, { [Action1.type]: 1 }, null]); 204 | }); 205 | 206 | it('should be executing between dispatch and error', () => { 207 | const actionStatus: ActionsExecuting[] = []; 208 | 209 | store.select(actionsExecuting([ErrorAction1])).subscribe((_actionsExecuting) => { 210 | actionStatus.push(_actionsExecuting); 211 | }); 212 | 213 | store.dispatch(new ErrorAction1()); 214 | expect(actionStatus).toEqual([null, { [ErrorAction1.type]: 1 }, null]); 215 | }); 216 | }); 217 | 218 | describe('Async Action', () => { 219 | it('should be null', fakeAsync(() => { 220 | store.dispatch(AsyncAction1); 221 | 222 | let snapshot = store.selectSnapshot(actionsExecuting([AsyncAction1])); 223 | expect(snapshot).toEqual({ 'ASYNC ACTION 1': 1 }); 224 | 225 | tick(); 226 | 227 | snapshot = store.selectSnapshot(actionsExecuting([AsyncAction1])); 228 | expect(snapshot).toBe(null); 229 | })); 230 | 231 | it('should be executing between dispatch and complete ', fakeAsync(() => { 232 | const actionStatus: ActionsExecuting[] = []; 233 | 234 | store.select(actionsExecuting([AsyncAction1])).subscribe((_actionsExecuting) => { 235 | actionStatus.push(_actionsExecuting); 236 | }); 237 | 238 | store.dispatch(new AsyncAction1()); 239 | tick(1); 240 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 1 }, null]); 241 | })); 242 | 243 | it('should be executing between dispatch and error', fakeAsync(() => { 244 | const actionStatus: ActionsExecuting[] = []; 245 | 246 | store.select(actionsExecuting([AsyncErrorAction1])).subscribe((_actionsExecuting) => { 247 | actionStatus.push(_actionsExecuting); 248 | }); 249 | 250 | store.dispatch(new AsyncErrorAction1()).subscribe({ 251 | error: (err) => { 252 | expect(err).toBeDefined(); 253 | } 254 | }); 255 | 256 | tick(1); 257 | expect(actionStatus).toEqual([null, { [AsyncErrorAction1.type]: 1 }, null]); 258 | })); 259 | }); 260 | }); 261 | 262 | describe('Multiple Actions', () => { 263 | describe('sync', () => { 264 | it('should be executing between dispatch and complete', () => { 265 | const actionStatus: ActionsExecuting[] = []; 266 | 267 | store.select(actionsExecuting([Action1, Action2])).subscribe((_actionsExecuting) => { 268 | actionStatus.push(_actionsExecuting); 269 | }); 270 | 271 | store.dispatch(new Action1()); 272 | store.dispatch(new Action2()); 273 | expect(actionStatus).toEqual([null, { [Action1.type]: 1 }, null, { [Action2.type]: 1 }, null]); 274 | }); 275 | 276 | it('should be executing between dispatch and error', () => { 277 | const actionStatus: ActionsExecuting[] = []; 278 | 279 | store.select(actionsExecuting([Action1, ErrorAction1])).subscribe((_actionsExecuting) => { 280 | actionStatus.push(_actionsExecuting); 281 | }); 282 | 283 | store.dispatch(new Action1()); 284 | store.dispatch(new ErrorAction1()); 285 | expect(actionStatus).toEqual([null, { [Action1.type]: 1 }, null, { [ErrorAction1.type]: 1 }, null]); 286 | }); 287 | }); 288 | describe('async', () => { 289 | it('should be executing between dispatch and complete ', fakeAsync(() => { 290 | const actionStatus: ActionsExecuting[] = []; 291 | 292 | store.select(actionsExecuting([AsyncAction1, AsyncAction2])).subscribe((_actionsExecuting) => { 293 | actionStatus.push(_actionsExecuting); 294 | }); 295 | 296 | store.dispatch(new AsyncAction1()); 297 | tick(1); 298 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 1 }, null]); 299 | store.dispatch(new AsyncAction2()); 300 | tick(1); 301 | expect(actionStatus).toEqual([ 302 | null, 303 | { [AsyncAction1.type]: 1 }, 304 | null, 305 | { [AsyncAction2.type]: 1 }, 306 | null 307 | ]); 308 | })); 309 | 310 | it('should be executing between dispatch and error', fakeAsync(() => { 311 | const actionStatus: ActionsExecuting[] = []; 312 | 313 | store.select(actionsExecuting([AsyncAction1, AsyncErrorAction1])).subscribe((_actionsExecuting) => { 314 | actionStatus.push(_actionsExecuting); 315 | }); 316 | 317 | store.dispatch(new AsyncAction1()); 318 | tick(1); 319 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 1 }, null]); 320 | store.dispatch(new AsyncErrorAction1()); 321 | tick(1); 322 | expect(actionStatus).toEqual([ 323 | null, 324 | { [AsyncAction1.type]: 1 }, 325 | null, 326 | { [AsyncErrorAction1.type]: 1 }, 327 | null 328 | ]); 329 | })); 330 | 331 | it('should be executing when action is dispatched multiple times', fakeAsync(() => { 332 | const actionStatus: ActionsExecuting[] = []; 333 | 334 | store.select(actionsExecuting([AsyncAction1])).subscribe((_actionsExecuting) => { 335 | actionStatus.push(_actionsExecuting); 336 | }); 337 | 338 | store.dispatch(new AsyncAction1()); 339 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 1 }]); 340 | store.dispatch(new AsyncAction1()); 341 | tick(1); 342 | expect(actionStatus).toEqual([ 343 | null, 344 | { [AsyncAction1.type]: 1 }, 345 | { [AsyncAction1.type]: 2 }, 346 | { [AsyncAction1.type]: 1 }, 347 | null 348 | ]); 349 | })); 350 | 351 | it('should be executing when action is dispatched multiple times (case 2)', fakeAsync(() => { 352 | const actionStatus: ActionsExecuting[] = []; 353 | 354 | store.select(actionsExecuting([AsyncAction1])).subscribe((_actionsExecuting) => { 355 | actionStatus.push(_actionsExecuting); 356 | }); 357 | 358 | store.dispatch(new AsyncAction1()); 359 | store.dispatch(new AsyncAction1()); 360 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 1 }, { [AsyncAction1.type]: 2 }]); 361 | tick(1); 362 | expect(actionStatus).toEqual([ 363 | null, 364 | { [AsyncAction1.type]: 1 }, 365 | { [AsyncAction1.type]: 2 }, 366 | { [AsyncAction1.type]: 1 }, 367 | null 368 | ]); 369 | })); 370 | 371 | describe('nested actions 1', () => { 372 | it('should be executing on nested actions', fakeAsync(() => { 373 | const nestedAction1Status: ActionsExecuting[] = []; 374 | const nestedAction2Status: ActionsExecuting[] = []; 375 | const nestedAction3Status: ActionsExecuting[] = []; 376 | 377 | const combinedActionStatus: ActionsExecuting[] = []; 378 | 379 | store.select(actionsExecuting([NestedAsyncAction1])).subscribe((_actionsExecuting) => { 380 | nestedAction1Status.push(_actionsExecuting); 381 | }); 382 | 383 | store.select(actionsExecuting([NestedAsyncAction2])).subscribe((_actionsExecuting) => { 384 | nestedAction2Status.push(_actionsExecuting); 385 | }); 386 | 387 | store.select(actionsExecuting([NestedAsyncAction3])).subscribe((_actionsExecuting) => { 388 | nestedAction3Status.push(_actionsExecuting); 389 | }); 390 | 391 | store 392 | .select(actionsExecuting([NestedAsyncAction1, NestedAsyncAction2, NestedAsyncAction3])) 393 | .subscribe((_actionsExecuting) => { 394 | combinedActionStatus.push(_actionsExecuting); 395 | }); 396 | 397 | store.dispatch(new NestedAsyncAction1()); 398 | tick(1); 399 | expect(nestedAction1Status).toEqual([ 400 | null, 401 | { [NestedAsyncAction1.type]: 1 }, 402 | { [NestedAsyncAction1.type]: 1 }, 403 | { [NestedAsyncAction1.type]: 1 }, 404 | { [NestedAsyncAction1.type]: 1 }, 405 | { [NestedAsyncAction1.type]: 1 }, 406 | null 407 | ]); 408 | expect(nestedAction2Status).toEqual([ 409 | null, 410 | { [NestedAsyncAction2.type]: 1 }, 411 | { [NestedAsyncAction2.type]: 1 }, 412 | { [NestedAsyncAction2.type]: 1 }, 413 | null 414 | ]); 415 | expect(nestedAction3Status).toEqual([null, { [NestedAsyncAction3.type]: 1 }, null]); 416 | 417 | expect(combinedActionStatus).toEqual([ 418 | null, 419 | { [NestedAsyncAction1.type]: 1 }, 420 | { [NestedAsyncAction1.type]: 1, [NestedAsyncAction2.type]: 1 }, 421 | { 422 | [NestedAsyncAction1.type]: 1, 423 | [NestedAsyncAction2.type]: 1, 424 | [NestedAsyncAction3.type]: 1 425 | }, 426 | { [NestedAsyncAction1.type]: 1, [NestedAsyncAction2.type]: 1 }, 427 | { [NestedAsyncAction1.type]: 1 }, 428 | null 429 | ]); 430 | })); 431 | }); 432 | 433 | describe('nested actions 2', () => { 434 | it('should be executing on nested actions (scenario 1)', fakeAsync(() => { 435 | const nestedAction4Status: ActionsExecuting[] = []; 436 | const nestedAction5Status: ActionsExecuting[] = []; 437 | const nestedAction6Status: ActionsExecuting[] = []; 438 | 439 | const combinedAction45Status: ActionsExecuting[] = []; 440 | const combinedAction456Status: ActionsExecuting[] = []; 441 | 442 | store.select(actionsExecuting([NestedAsyncAction4])).subscribe((_actionsExecuting) => { 443 | nestedAction4Status.push(_actionsExecuting); 444 | }); 445 | 446 | store.select(actionsExecuting([NestedAsyncAction5])).subscribe((_actionsExecuting) => { 447 | nestedAction5Status.push(_actionsExecuting); 448 | }); 449 | 450 | store.select(actionsExecuting([NestedAsyncAction6])).subscribe((_actionsExecuting) => { 451 | nestedAction6Status.push(_actionsExecuting); 452 | }); 453 | 454 | store 455 | .select(actionsExecuting([NestedAsyncAction4, NestedAsyncAction5])) 456 | .subscribe((_actionsExecuting) => { 457 | combinedAction45Status.push(_actionsExecuting); 458 | }); 459 | 460 | store 461 | .select(actionsExecuting([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 462 | .subscribe((_actionsExecuting) => { 463 | combinedAction456Status.push(_actionsExecuting); 464 | }); 465 | 466 | store.dispatch(new NestedAsyncAction4()); 467 | tick(1); 468 | expect(nestedAction4Status).toEqual([ 469 | null, 470 | { [NestedAsyncAction4.type]: 1 }, 471 | { [NestedAsyncAction4.type]: 1 }, 472 | { [NestedAsyncAction4.type]: 1 }, 473 | null 474 | ]); 475 | expect(nestedAction5Status).toEqual([null]); 476 | expect(nestedAction6Status).toEqual([null, { [NestedAsyncAction6.type]: 1 }, null]); 477 | 478 | expect(combinedAction45Status).toEqual([ 479 | null, 480 | { [NestedAsyncAction4.type]: 1 }, 481 | { [NestedAsyncAction4.type]: 1 }, 482 | { [NestedAsyncAction4.type]: 1 }, 483 | null 484 | ]); 485 | expect(combinedAction456Status).toEqual([ 486 | null, 487 | { [NestedAsyncAction4.type]: 1 }, 488 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction6.type]: 1 }, 489 | { [NestedAsyncAction4.type]: 1 }, 490 | null 491 | ]); 492 | })); 493 | 494 | it('should be executing on nested actions (scenario 2)', fakeAsync(() => { 495 | const nestedAction4Status: ActionsExecuting[] = []; 496 | const nestedAction5Status: ActionsExecuting[] = []; 497 | const nestedAction6Status: ActionsExecuting[] = []; 498 | 499 | const combinedAction45Status: ActionsExecuting[] = []; 500 | const combinedAction456Status: ActionsExecuting[] = []; 501 | 502 | store.select(actionsExecuting([NestedAsyncAction4])).subscribe((_actionsExecuting) => { 503 | nestedAction4Status.push(_actionsExecuting); 504 | }); 505 | 506 | store.select(actionsExecuting([NestedAsyncAction5])).subscribe((_actionsExecuting) => { 507 | nestedAction5Status.push(_actionsExecuting); 508 | }); 509 | 510 | store.select(actionsExecuting([NestedAsyncAction6])).subscribe((_actionsExecuting) => { 511 | nestedAction6Status.push(_actionsExecuting); 512 | }); 513 | 514 | store 515 | .select(actionsExecuting([NestedAsyncAction4, NestedAsyncAction5])) 516 | .subscribe((_actionsExecuting) => { 517 | combinedAction45Status.push(_actionsExecuting); 518 | }); 519 | 520 | store 521 | .select(actionsExecuting([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 522 | .subscribe((_actionsExecuting) => { 523 | combinedAction456Status.push(_actionsExecuting); 524 | }); 525 | 526 | store.dispatch([new NestedAsyncAction4(), new NestedAsyncAction5()]); 527 | tick(1); 528 | expect(nestedAction4Status).toEqual([ 529 | null, 530 | { [NestedAsyncAction4.type]: 1 }, 531 | { [NestedAsyncAction4.type]: 1 }, 532 | { [NestedAsyncAction4.type]: 1 }, 533 | { [NestedAsyncAction4.type]: 1 }, 534 | { [NestedAsyncAction4.type]: 1 }, 535 | { [NestedAsyncAction4.type]: 1 }, 536 | null 537 | ]); 538 | expect(nestedAction5Status).toEqual([ 539 | null, 540 | { [NestedAsyncAction5.type]: 1 }, 541 | { [NestedAsyncAction5.type]: 1 }, 542 | { [NestedAsyncAction5.type]: 1 }, 543 | { [NestedAsyncAction5.type]: 1 }, 544 | { [NestedAsyncAction5.type]: 1 } 545 | ]); 546 | expect(nestedAction6Status).toEqual([ 547 | null, 548 | { [NestedAsyncAction6.type]: 1 }, 549 | { [NestedAsyncAction6.type]: 1 }, 550 | { [NestedAsyncAction6.type]: 2 }, 551 | { [NestedAsyncAction6.type]: 1 }, 552 | null 553 | ]); 554 | tick(100); 555 | expect(nestedAction4Status).toEqual([ 556 | null, 557 | { [NestedAsyncAction4.type]: 1 }, 558 | { [NestedAsyncAction4.type]: 1 }, 559 | { [NestedAsyncAction4.type]: 1 }, 560 | { [NestedAsyncAction4.type]: 1 }, 561 | { [NestedAsyncAction4.type]: 1 }, 562 | { [NestedAsyncAction4.type]: 1 }, 563 | null 564 | ]); 565 | expect(nestedAction5Status).toEqual([ 566 | null, 567 | { [NestedAsyncAction5.type]: 1 }, 568 | { [NestedAsyncAction5.type]: 1 }, 569 | { [NestedAsyncAction5.type]: 1 }, 570 | { [NestedAsyncAction5.type]: 1 }, 571 | { [NestedAsyncAction5.type]: 1 }, 572 | null 573 | ]); 574 | expect(nestedAction6Status).toEqual([ 575 | null, 576 | { [NestedAsyncAction6.type]: 1 }, 577 | { [NestedAsyncAction6.type]: 1 }, 578 | { [NestedAsyncAction6.type]: 2 }, 579 | { [NestedAsyncAction6.type]: 1 }, 580 | null 581 | ]); 582 | 583 | expect(combinedAction45Status).toEqual([ 584 | null, 585 | { [NestedAsyncAction4.type]: 1 }, 586 | { [NestedAsyncAction4.type]: 1 }, 587 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 588 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 589 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 590 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 591 | { [NestedAsyncAction5.type]: 1 }, 592 | null 593 | ]); 594 | expect(combinedAction456Status).toEqual([ 595 | null, 596 | { [NestedAsyncAction4.type]: 1 }, 597 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction6.type]: 1 }, 598 | { 599 | [NestedAsyncAction4.type]: 1, 600 | [NestedAsyncAction5.type]: 1, 601 | [NestedAsyncAction6.type]: 1 602 | }, 603 | { 604 | [NestedAsyncAction4.type]: 1, 605 | [NestedAsyncAction5.type]: 1, 606 | [NestedAsyncAction6.type]: 2 607 | }, 608 | { 609 | [NestedAsyncAction4.type]: 1, 610 | [NestedAsyncAction5.type]: 1, 611 | [NestedAsyncAction6.type]: 1 612 | }, 613 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 614 | { [NestedAsyncAction5.type]: 1 }, 615 | null 616 | ]); 617 | })); 618 | 619 | it('should be executing on nested actions (scenario 3)', fakeAsync(() => { 620 | const nestedAction4Status: ActionsExecuting[] = []; 621 | const nestedAction5Status: ActionsExecuting[] = []; 622 | const nestedAction6Status: ActionsExecuting[] = []; 623 | 624 | const combinedAction45Status: ActionsExecuting[] = []; 625 | const combinedAction456Status: ActionsExecuting[] = []; 626 | 627 | store.select(actionsExecuting([NestedAsyncAction4])).subscribe((_actionsExecuting) => { 628 | nestedAction4Status.push(_actionsExecuting); 629 | }); 630 | 631 | store.select(actionsExecuting([NestedAsyncAction5])).subscribe((_actionsExecuting) => { 632 | nestedAction5Status.push(_actionsExecuting); 633 | }); 634 | 635 | store.select(actionsExecuting([NestedAsyncAction6])).subscribe((_actionsExecuting) => { 636 | nestedAction6Status.push(_actionsExecuting); 637 | }); 638 | 639 | store 640 | .select(actionsExecuting([NestedAsyncAction4, NestedAsyncAction5])) 641 | .subscribe((_actionsExecuting) => { 642 | combinedAction45Status.push(_actionsExecuting); 643 | }); 644 | 645 | store 646 | .select(actionsExecuting([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 647 | .subscribe((_actionsExecuting) => { 648 | combinedAction456Status.push(_actionsExecuting); 649 | }); 650 | 651 | store.dispatch(new NestedAsyncAction5()); 652 | tick(1); 653 | expect(nestedAction4Status).toEqual([null]); 654 | expect(nestedAction5Status).toEqual([ 655 | null, 656 | { [NestedAsyncAction5.type]: 1 }, 657 | { [NestedAsyncAction5.type]: 1 }, 658 | { [NestedAsyncAction5.type]: 1 } 659 | ]); 660 | expect(nestedAction6Status).toEqual([null, { [NestedAsyncAction6.type]: 1 }, null]); 661 | tick(100); 662 | expect(nestedAction4Status).toEqual([null]); 663 | expect(nestedAction5Status).toEqual([ 664 | null, 665 | { [NestedAsyncAction5.type]: 1 }, 666 | { [NestedAsyncAction5.type]: 1 }, 667 | { [NestedAsyncAction5.type]: 1 }, 668 | null 669 | ]); 670 | expect(nestedAction6Status).toEqual([null, { [NestedAsyncAction6.type]: 1 }, null]); 671 | 672 | expect(combinedAction45Status).toEqual([ 673 | null, 674 | { [NestedAsyncAction5.type]: 1 }, 675 | { [NestedAsyncAction5.type]: 1 }, 676 | { [NestedAsyncAction5.type]: 1 }, 677 | null 678 | ]); 679 | expect(combinedAction456Status).toEqual([ 680 | null, 681 | { [NestedAsyncAction5.type]: 1 }, 682 | { [NestedAsyncAction5.type]: 1, [NestedAsyncAction6.type]: 1 }, 683 | { [NestedAsyncAction5.type]: 1 }, 684 | null 685 | ]); 686 | })); 687 | 688 | it('should be executing on nested actions (scenario 4)', fakeAsync(() => { 689 | const nestedAction4Status: ActionsExecuting[] = []; 690 | const nestedAction5Status: ActionsExecuting[] = []; 691 | const nestedAction6Status: ActionsExecuting[] = []; 692 | 693 | const combinedAction45Status: ActionsExecuting[] = []; 694 | const combinedAction456Status: ActionsExecuting[] = []; 695 | 696 | store.select(actionsExecuting([NestedAsyncAction4])).subscribe((_actionsExecuting) => { 697 | nestedAction4Status.push(_actionsExecuting); 698 | }); 699 | 700 | store.select(actionsExecuting([NestedAsyncAction5])).subscribe((_actionsExecuting) => { 701 | nestedAction5Status.push(_actionsExecuting); 702 | }); 703 | 704 | store.select(actionsExecuting([NestedAsyncAction6])).subscribe((_actionsExecuting) => { 705 | nestedAction6Status.push(_actionsExecuting); 706 | }); 707 | 708 | store 709 | .select(actionsExecuting([NestedAsyncAction4, NestedAsyncAction5])) 710 | .subscribe((_actionsExecuting) => { 711 | combinedAction45Status.push(_actionsExecuting); 712 | }); 713 | 714 | store 715 | .select(actionsExecuting([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 716 | .subscribe((_actionsExecuting) => { 717 | combinedAction456Status.push(_actionsExecuting); 718 | }); 719 | 720 | store.dispatch([new NestedAsyncAction4(), new NestedAsyncAction5(), new NestedAsyncAction6()]); 721 | tick(1); 722 | expect(nestedAction4Status).toEqual([ 723 | null, 724 | { [NestedAsyncAction4.type]: 1 }, 725 | { [NestedAsyncAction4.type]: 1 }, 726 | { [NestedAsyncAction4.type]: 1 }, 727 | { [NestedAsyncAction4.type]: 1 }, 728 | { [NestedAsyncAction4.type]: 1 }, 729 | { [NestedAsyncAction4.type]: 1 }, 730 | { [NestedAsyncAction4.type]: 1 }, 731 | { [NestedAsyncAction4.type]: 1 }, 732 | null 733 | ]); 734 | expect(nestedAction5Status).toEqual([ 735 | null, 736 | { [NestedAsyncAction5.type]: 1 }, 737 | { [NestedAsyncAction5.type]: 1 }, 738 | { [NestedAsyncAction5.type]: 1 }, 739 | { [NestedAsyncAction5.type]: 1 }, 740 | { [NestedAsyncAction5.type]: 1 }, 741 | { [NestedAsyncAction5.type]: 1 }, 742 | { [NestedAsyncAction5.type]: 1 } 743 | ]); 744 | expect(nestedAction6Status).toEqual([ 745 | null, 746 | { [NestedAsyncAction6.type]: 1 }, 747 | { [NestedAsyncAction6.type]: 1 }, 748 | { [NestedAsyncAction6.type]: 2 }, 749 | { [NestedAsyncAction6.type]: 3 }, 750 | { [NestedAsyncAction6.type]: 2 }, 751 | { [NestedAsyncAction6.type]: 1 }, 752 | null 753 | ]); 754 | tick(100); 755 | expect(nestedAction4Status).toEqual([ 756 | null, 757 | { [NestedAsyncAction4.type]: 1 }, 758 | { [NestedAsyncAction4.type]: 1 }, 759 | { [NestedAsyncAction4.type]: 1 }, 760 | { [NestedAsyncAction4.type]: 1 }, 761 | { [NestedAsyncAction4.type]: 1 }, 762 | { [NestedAsyncAction4.type]: 1 }, 763 | { [NestedAsyncAction4.type]: 1 }, 764 | { [NestedAsyncAction4.type]: 1 }, 765 | null 766 | ]); 767 | expect(nestedAction5Status).toEqual([ 768 | null, 769 | { [NestedAsyncAction5.type]: 1 }, 770 | { [NestedAsyncAction5.type]: 1 }, 771 | { [NestedAsyncAction5.type]: 1 }, 772 | { [NestedAsyncAction5.type]: 1 }, 773 | { [NestedAsyncAction5.type]: 1 }, 774 | { [NestedAsyncAction5.type]: 1 }, 775 | { [NestedAsyncAction5.type]: 1 }, 776 | null 777 | ]); 778 | expect(nestedAction6Status).toEqual([ 779 | null, 780 | { [NestedAsyncAction6.type]: 1 }, 781 | { [NestedAsyncAction6.type]: 1 }, 782 | { [NestedAsyncAction6.type]: 2 }, 783 | { [NestedAsyncAction6.type]: 3 }, 784 | { [NestedAsyncAction6.type]: 2 }, 785 | { [NestedAsyncAction6.type]: 1 }, 786 | null 787 | ]); 788 | 789 | expect(combinedAction45Status).toEqual([ 790 | null, 791 | { [NestedAsyncAction4.type]: 1 }, 792 | { [NestedAsyncAction4.type]: 1 }, 793 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 794 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 795 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 796 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 797 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 798 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 799 | { [NestedAsyncAction5.type]: 1 }, 800 | null 801 | ]); 802 | expect(combinedAction456Status).toEqual([ 803 | null, 804 | { [NestedAsyncAction4.type]: 1 }, 805 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction6.type]: 1 }, 806 | { 807 | [NestedAsyncAction4.type]: 1, 808 | [NestedAsyncAction5.type]: 1, 809 | [NestedAsyncAction6.type]: 1 810 | }, 811 | { 812 | [NestedAsyncAction4.type]: 1, 813 | [NestedAsyncAction5.type]: 1, 814 | [NestedAsyncAction6.type]: 2 815 | }, 816 | { 817 | [NestedAsyncAction4.type]: 1, 818 | [NestedAsyncAction5.type]: 1, 819 | [NestedAsyncAction6.type]: 3 820 | }, 821 | { 822 | [NestedAsyncAction4.type]: 1, 823 | [NestedAsyncAction5.type]: 1, 824 | [NestedAsyncAction6.type]: 2 825 | }, 826 | { 827 | [NestedAsyncAction4.type]: 1, 828 | [NestedAsyncAction5.type]: 1, 829 | [NestedAsyncAction6.type]: 1 830 | }, 831 | { [NestedAsyncAction4.type]: 1, [NestedAsyncAction5.type]: 1 }, 832 | { [NestedAsyncAction5.type]: 1 }, 833 | null 834 | ]); 835 | })); 836 | }); 837 | }); 838 | }); 839 | 840 | describe('No Actions', () => { 841 | describe('sync', () => { 842 | it('should be executing between dispatch and complete', () => { 843 | const actionStatus: ActionsExecuting[] = []; 844 | 845 | store.select(actionsExecuting()).subscribe((_actionsExecuting) => { 846 | actionStatus.push(_actionsExecuting); 847 | }); 848 | 849 | store.dispatch(new Action1()); 850 | store.dispatch(new Action2()); 851 | expect(actionStatus).toEqual([ 852 | null, 853 | { [Action1.type]: 1 }, 854 | { [Action1.type]: 0 }, 855 | { [Action1.type]: 0, [Action2.type]: 1 }, 856 | { [Action1.type]: 0, [Action2.type]: 0 } 857 | ]); 858 | }); 859 | }); 860 | 861 | describe('async', () => { 862 | it('should be executing between dispatch and complete ', fakeAsync(() => { 863 | const actionStatus: ActionsExecuting[] = []; 864 | 865 | store.select(actionsExecuting()).subscribe((_actionsExecuting) => { 866 | actionStatus.push(_actionsExecuting); 867 | }); 868 | 869 | store.dispatch(new AsyncAction1()); 870 | tick(1); 871 | expect(actionStatus).toEqual([null, { [AsyncAction1.type]: 1 }, { [AsyncAction1.type]: 0 }]); 872 | store.dispatch(new AsyncAction2()); 873 | tick(1); 874 | expect(actionStatus).toEqual([ 875 | null, 876 | { [AsyncAction1.type]: 1 }, 877 | { [AsyncAction1.type]: 0 }, 878 | { [AsyncAction1.type]: 0, [AsyncAction2.type]: 1 }, 879 | { [AsyncAction1.type]: 0, [AsyncAction2.type]: 0 } 880 | ]); 881 | })); 882 | }); 883 | }); 884 | }); 885 | }); 886 | -------------------------------------------------------------------------------- /src/tests/has-actions-executing.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgxsActionsExecutingModule, hasActionsExecuting } from '..'; 2 | import { TestBed, fakeAsync, tick } from '@angular/core/testing'; 3 | import { NgxsModule, Store, State, Action, StateContext, NgxsOnInit } from '@ngxs/store'; 4 | import { of, throwError } from 'rxjs'; 5 | import { delay } from 'rxjs/operators'; 6 | import { Injectable } from '@angular/core'; 7 | 8 | describe('hasActionsExecuting', () => { 9 | let store: Store; 10 | 11 | class Action1 { 12 | public static type = 'ACTION 1'; 13 | } 14 | 15 | class Action2 { 16 | public static type = 'ACTION 2'; 17 | } 18 | 19 | class Action3 { 20 | public static type = 'ACTION 3'; 21 | } 22 | 23 | class ErrorAction1 { 24 | public static type = 'ERROR ACTION 1'; 25 | } 26 | 27 | class AsyncAction1 { 28 | public static type = 'ASYNC ACTION 1'; 29 | } 30 | 31 | class AsyncAction2 { 32 | public static type = 'ASYNC ACTION 2'; 33 | } 34 | 35 | class AsyncAction3 { 36 | public static type = 'ASYNC ACTION 3'; 37 | } 38 | 39 | class AsyncErrorAction1 { 40 | public static type = 'ASYNC ERROR ACTION 1'; 41 | } 42 | 43 | class NestedAsyncAction1 { 44 | public static type = 'NESTED ASYNC ACTION 1'; 45 | } 46 | 47 | class NestedAsyncAction2 { 48 | public static type = 'NESTED ASYNC ACTION 2'; 49 | } 50 | 51 | class NestedAsyncAction3 { 52 | public static type = 'NESTED ASYNC ACTION 3'; 53 | } 54 | 55 | class NestedAsyncAction4 { 56 | public static type = 'NESTED ASYNC ACTION 4'; 57 | } 58 | 59 | class NestedAsyncAction5 { 60 | public static type = 'NESTED ASYNC ACTION 5'; 61 | } 62 | 63 | class NestedAsyncAction6 { 64 | public static type = 'NESTED ASYNC ACTION 6'; 65 | } 66 | 67 | @State({ 68 | name: 'test' 69 | }) 70 | @Injectable() 71 | class TestState { 72 | @Action([Action1]) 73 | public action1() {} 74 | 75 | @Action([AsyncAction1]) 76 | public asyncAction1() { 77 | return of({}).pipe(delay(0)); 78 | } 79 | 80 | @Action([AsyncAction2]) 81 | public asyncAction2() { 82 | return of({}).pipe(delay(0)); 83 | } 84 | 85 | @Action(AsyncErrorAction1) 86 | public asyncError() { 87 | return throwError(new Error('this is a test error')).pipe(delay(0)); 88 | } 89 | 90 | @Action(ErrorAction1) 91 | public onError() { 92 | return throwError(new Error('this is a test error')); 93 | } 94 | } 95 | 96 | @State<{}>({ 97 | name: 'nested_actions_1' 98 | }) 99 | @Injectable() 100 | class NestedActions1State { 101 | @Action(NestedAsyncAction1) 102 | public nestedAsyncAction1({ dispatch }: StateContext<{}>) { 103 | return dispatch(new NestedAsyncAction2()).pipe(delay(0)); 104 | } 105 | 106 | @Action(NestedAsyncAction2) 107 | public nestedAsyncAction2({ dispatch }: StateContext<{}>) { 108 | return dispatch(new NestedAsyncAction3()).pipe(delay(0)); 109 | } 110 | 111 | @Action(NestedAsyncAction3) 112 | public nestedAsyncAction3() { 113 | return of({}).pipe(delay(0)); 114 | } 115 | } 116 | 117 | @State({ 118 | name: 'nested_actions_2' 119 | }) 120 | @Injectable() 121 | class NestedActions2State { 122 | @Action([NestedAsyncAction4, NestedAsyncAction5]) 123 | public combinedAction({ dispatch }: StateContext<{}>) { 124 | return dispatch(new NestedAsyncAction6()).pipe(delay(0)); 125 | } 126 | 127 | @Action(NestedAsyncAction5) 128 | public nestedAsyncAction5() { 129 | return of({}).pipe(delay(100)); 130 | } 131 | 132 | @Action(NestedAsyncAction6) 133 | public nestedAsyncAction6() { 134 | return of({}).pipe(delay(0)); 135 | } 136 | } 137 | 138 | @State({ 139 | name: 'ngxs_on_init_state' 140 | }) 141 | @Injectable() 142 | class NgxsOnInitState implements NgxsOnInit { 143 | ngxsOnInit(ctx: StateContext<{}>) { 144 | ctx.dispatch(new Action3()); 145 | ctx.dispatch(new AsyncAction3()); 146 | } 147 | 148 | @Action([Action3]) 149 | public action3() {} 150 | 151 | @Action([AsyncAction3]) 152 | public asyncAction3() { 153 | return of({}).pipe(delay(0)); 154 | } 155 | } 156 | 157 | describe('NgxsOnInit', () => { 158 | describe('Sync and Async Action', () => { 159 | it('should be false when dispatching async or sync from ngxsOnInit', fakeAsync(() => { 160 | TestBed.configureTestingModule({ 161 | imports: [NgxsModule.forRoot([NgxsOnInitState]), NgxsActionsExecutingModule.forRoot()] 162 | }); 163 | 164 | store = TestBed.get(Store); 165 | 166 | tick(1); 167 | 168 | expect(store.selectSnapshot(hasActionsExecuting([Action3]))).toBe(false); 169 | expect(store.selectSnapshot(hasActionsExecuting([AsyncAction3]))).toEqual(false); 170 | })); 171 | }); 172 | }); 173 | 174 | describe('', () => { 175 | beforeEach(() => { 176 | TestBed.configureTestingModule({ 177 | imports: [ 178 | NgxsModule.forRoot([TestState, NestedActions1State, NestedActions2State]), 179 | NgxsActionsExecutingModule.forRoot() 180 | ] 181 | }); 182 | 183 | store = TestBed.get(Store); 184 | }); 185 | 186 | describe('Single Action', () => { 187 | describe('Sync Action', () => { 188 | it('should be false', () => { 189 | store.dispatch(Action1); 190 | 191 | const snapshot = store.selectSnapshot(hasActionsExecuting([Action1])); 192 | expect(snapshot).toBe(false); 193 | }); 194 | 195 | it('should be executing between dispatch and complete', () => { 196 | const actionStatus: boolean[] = []; 197 | 198 | store.select(hasActionsExecuting([Action1])).subscribe((_actionsExecuting) => { 199 | actionStatus.push(_actionsExecuting); 200 | }); 201 | 202 | store.dispatch(new Action1()); 203 | expect(actionStatus).toEqual([false, true, false]); 204 | }); 205 | 206 | it('should be executing between dispatch and error', () => { 207 | const actionStatus: boolean[] = []; 208 | 209 | store.select(hasActionsExecuting([ErrorAction1])).subscribe((_actionsExecuting) => { 210 | actionStatus.push(_actionsExecuting); 211 | }); 212 | 213 | store.dispatch(new ErrorAction1()); 214 | expect(actionStatus).toEqual([false, true, false]); 215 | }); 216 | }); 217 | 218 | describe('Async Action', () => { 219 | it('should be false', fakeAsync(() => { 220 | store.dispatch(AsyncAction1); 221 | 222 | let snapshot = store.selectSnapshot(hasActionsExecuting([AsyncAction1])); 223 | expect(snapshot).toEqual(true); 224 | 225 | tick(); 226 | 227 | snapshot = store.selectSnapshot(hasActionsExecuting([AsyncAction1])); 228 | expect(snapshot).toBe(false); 229 | })); 230 | 231 | it('should be executing between dispatch and complete ', fakeAsync(() => { 232 | const actionStatus: boolean[] = []; 233 | 234 | store.select(hasActionsExecuting([AsyncAction1])).subscribe((_actionsExecuting) => { 235 | actionStatus.push(_actionsExecuting); 236 | }); 237 | 238 | store.dispatch(new AsyncAction1()); 239 | tick(1); 240 | expect(actionStatus).toEqual([false, true, false]); 241 | })); 242 | 243 | it('should be executing between dispatch and error', fakeAsync(() => { 244 | const actionStatus: boolean[] = []; 245 | 246 | store.select(hasActionsExecuting([AsyncErrorAction1])).subscribe((_actionsExecuting) => { 247 | actionStatus.push(_actionsExecuting); 248 | }); 249 | 250 | store.dispatch(new AsyncErrorAction1()).subscribe({ 251 | error: (err) => { 252 | expect(err).toBeDefined(); 253 | } 254 | }); 255 | 256 | tick(1); 257 | expect(actionStatus).toEqual([false, true, false]); 258 | })); 259 | }); 260 | }); 261 | 262 | describe('Multiple Actions', () => { 263 | describe('sync', () => { 264 | it('should be executing between dispatch and complete', () => { 265 | const actionStatus: boolean[] = []; 266 | 267 | store.select(hasActionsExecuting([Action1, Action2])).subscribe((_actionsExecuting) => { 268 | actionStatus.push(_actionsExecuting); 269 | }); 270 | 271 | store.dispatch(new Action1()); 272 | store.dispatch(new Action2()); 273 | expect(actionStatus).toEqual([false, true, false, true, false]); 274 | }); 275 | 276 | it('should be executing between dispatch and error', () => { 277 | const actionStatus: boolean[] = []; 278 | 279 | store.select(hasActionsExecuting([Action1, ErrorAction1])).subscribe((_actionsExecuting) => { 280 | actionStatus.push(_actionsExecuting); 281 | }); 282 | 283 | store.dispatch(new Action1()); 284 | store.dispatch(new ErrorAction1()); 285 | expect(actionStatus).toEqual([false, true, false, true, false]); 286 | }); 287 | }); 288 | describe('async', () => { 289 | it('should be executing between dispatch and complete ', fakeAsync(() => { 290 | const actionStatus: boolean[] = []; 291 | 292 | store.select(hasActionsExecuting([AsyncAction1, AsyncAction2])).subscribe((_actionsExecuting) => { 293 | actionStatus.push(_actionsExecuting); 294 | }); 295 | 296 | store.dispatch(new AsyncAction1()); 297 | tick(1); 298 | expect(actionStatus).toEqual([false, true, false]); 299 | store.dispatch(new AsyncAction2()); 300 | tick(1); 301 | expect(actionStatus).toEqual([false, true, false, true, false]); 302 | })); 303 | 304 | it('should be executing between dispatch and error', fakeAsync(() => { 305 | const actionStatus: boolean[] = []; 306 | 307 | store 308 | .select(hasActionsExecuting([AsyncAction1, AsyncErrorAction1])) 309 | .subscribe((_actionsExecuting) => { 310 | actionStatus.push(_actionsExecuting); 311 | }); 312 | 313 | store.dispatch(new AsyncAction1()); 314 | tick(1); 315 | expect(actionStatus).toEqual([false, true, false]); 316 | store.dispatch(new AsyncErrorAction1()); 317 | tick(1); 318 | expect(actionStatus).toEqual([false, true, false, true, false]); 319 | })); 320 | 321 | it('should be executing when action is dispatched multiple times', fakeAsync(() => { 322 | const actionStatus: boolean[] = []; 323 | 324 | store.select(hasActionsExecuting([AsyncAction1])).subscribe((_actionsExecuting) => { 325 | actionStatus.push(_actionsExecuting); 326 | }); 327 | 328 | store.dispatch(new AsyncAction1()); 329 | expect(actionStatus).toEqual([false, true]); 330 | store.dispatch(new AsyncAction1()); 331 | tick(1); 332 | expect(actionStatus).toEqual([false, true, false]); 333 | })); 334 | 335 | it('should be executing when action is dispatched multiple times (case 2)', fakeAsync(() => { 336 | const actionStatus: boolean[] = []; 337 | 338 | store.select(hasActionsExecuting([AsyncAction1])).subscribe((_actionsExecuting) => { 339 | actionStatus.push(_actionsExecuting); 340 | }); 341 | 342 | store.dispatch(new AsyncAction1()); 343 | store.dispatch(new AsyncAction1()); 344 | expect(actionStatus).toEqual([false, true]); 345 | tick(1); 346 | expect(actionStatus).toEqual([false, true, false]); 347 | })); 348 | 349 | describe('nested actions 1', () => { 350 | it('should be executing on nested actions', fakeAsync(() => { 351 | const nestedAction1Status: boolean[] = []; 352 | const nestedAction2Status: boolean[] = []; 353 | const nestedAction3Status: boolean[] = []; 354 | 355 | const combinedActionStatus: boolean[] = []; 356 | 357 | store.select(hasActionsExecuting([NestedAsyncAction1])).subscribe((_actionsExecuting) => { 358 | nestedAction1Status.push(_actionsExecuting); 359 | }); 360 | 361 | store.select(hasActionsExecuting([NestedAsyncAction2])).subscribe((_actionsExecuting) => { 362 | nestedAction2Status.push(_actionsExecuting); 363 | }); 364 | 365 | store.select(hasActionsExecuting([NestedAsyncAction3])).subscribe((_actionsExecuting) => { 366 | nestedAction3Status.push(_actionsExecuting); 367 | }); 368 | 369 | store 370 | .select(hasActionsExecuting([NestedAsyncAction1, NestedAsyncAction2, NestedAsyncAction3])) 371 | .subscribe((_actionsExecuting) => { 372 | combinedActionStatus.push(_actionsExecuting); 373 | }); 374 | 375 | store.dispatch(new NestedAsyncAction1()); 376 | tick(1); 377 | expect(nestedAction1Status).toEqual([false, true, false]); 378 | expect(nestedAction2Status).toEqual([false, true, false]); 379 | expect(nestedAction3Status).toEqual([false, true, false]); 380 | expect(combinedActionStatus).toEqual([false, true, false]); 381 | })); 382 | }); 383 | 384 | describe('nested actions 2', () => { 385 | it('should be executing on nested actions (scenario 1)', fakeAsync(() => { 386 | const nestedAction4Status: boolean[] = []; 387 | const nestedAction5Status: boolean[] = []; 388 | const nestedAction6Status: boolean[] = []; 389 | 390 | const combinedAction45Status: boolean[] = []; 391 | const combinedAction456Status: boolean[] = []; 392 | 393 | store.select(hasActionsExecuting([NestedAsyncAction4])).subscribe((_actionsExecuting) => { 394 | nestedAction4Status.push(_actionsExecuting); 395 | }); 396 | 397 | store.select(hasActionsExecuting([NestedAsyncAction5])).subscribe((_actionsExecuting) => { 398 | nestedAction5Status.push(_actionsExecuting); 399 | }); 400 | 401 | store.select(hasActionsExecuting([NestedAsyncAction6])).subscribe((_actionsExecuting) => { 402 | nestedAction6Status.push(_actionsExecuting); 403 | }); 404 | 405 | store 406 | .select(hasActionsExecuting([NestedAsyncAction4, NestedAsyncAction5])) 407 | .subscribe((_actionsExecuting) => { 408 | combinedAction45Status.push(_actionsExecuting); 409 | }); 410 | 411 | store 412 | .select(hasActionsExecuting([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 413 | .subscribe((_actionsExecuting) => { 414 | combinedAction456Status.push(_actionsExecuting); 415 | }); 416 | 417 | store.dispatch(new NestedAsyncAction4()); 418 | tick(1); 419 | expect(nestedAction4Status).toEqual([false, true, false]); 420 | expect(nestedAction5Status).toEqual([false]); 421 | expect(nestedAction6Status).toEqual([false, true, false]); 422 | 423 | expect(combinedAction45Status).toEqual([false, true, false]); 424 | expect(combinedAction456Status).toEqual([false, true, false]); 425 | })); 426 | 427 | it('should be executing on nested actions (scenario 2)', fakeAsync(() => { 428 | const nestedAction4Status: boolean[] = []; 429 | const nestedAction5Status: boolean[] = []; 430 | const nestedAction6Status: boolean[] = []; 431 | 432 | const combinedAction45Status: boolean[] = []; 433 | const combinedAction456Status: boolean[] = []; 434 | 435 | store.select(hasActionsExecuting([NestedAsyncAction4])).subscribe((_actionsExecuting) => { 436 | nestedAction4Status.push(_actionsExecuting); 437 | }); 438 | 439 | store.select(hasActionsExecuting([NestedAsyncAction5])).subscribe((_actionsExecuting) => { 440 | nestedAction5Status.push(_actionsExecuting); 441 | }); 442 | 443 | store.select(hasActionsExecuting([NestedAsyncAction6])).subscribe((_actionsExecuting) => { 444 | nestedAction6Status.push(_actionsExecuting); 445 | }); 446 | 447 | store 448 | .select(hasActionsExecuting([NestedAsyncAction4, NestedAsyncAction5])) 449 | .subscribe((_actionsExecuting) => { 450 | combinedAction45Status.push(_actionsExecuting); 451 | }); 452 | 453 | store 454 | .select(hasActionsExecuting([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 455 | .subscribe((_actionsExecuting) => { 456 | combinedAction456Status.push(_actionsExecuting); 457 | }); 458 | 459 | store.dispatch([new NestedAsyncAction4(), new NestedAsyncAction5()]); 460 | tick(1); 461 | expect(nestedAction4Status).toEqual([false, true, false]); 462 | expect(nestedAction5Status).toEqual([false, true]); 463 | expect(nestedAction6Status).toEqual([false, true, false]); 464 | tick(100); 465 | expect(nestedAction4Status).toEqual([false, true, false]); 466 | expect(nestedAction5Status).toEqual([false, true, false]); 467 | expect(nestedAction6Status).toEqual([false, true, false]); 468 | expect(combinedAction45Status).toEqual([false, true, false]); 469 | expect(combinedAction456Status).toEqual([false, true, false]); 470 | })); 471 | 472 | it('should be executing on nested actions (scenario 3)', fakeAsync(() => { 473 | const nestedAction4Status: boolean[] = []; 474 | const nestedAction5Status: boolean[] = []; 475 | const nestedAction6Status: boolean[] = []; 476 | 477 | const combinedAction45Status: boolean[] = []; 478 | const combinedAction456Status: boolean[] = []; 479 | 480 | store.select(hasActionsExecuting([NestedAsyncAction4])).subscribe((_actionsExecuting) => { 481 | nestedAction4Status.push(_actionsExecuting); 482 | }); 483 | 484 | store.select(hasActionsExecuting([NestedAsyncAction5])).subscribe((_actionsExecuting) => { 485 | nestedAction5Status.push(_actionsExecuting); 486 | }); 487 | 488 | store.select(hasActionsExecuting([NestedAsyncAction6])).subscribe((_actionsExecuting) => { 489 | nestedAction6Status.push(_actionsExecuting); 490 | }); 491 | 492 | store 493 | .select(hasActionsExecuting([NestedAsyncAction4, NestedAsyncAction5])) 494 | .subscribe((_actionsExecuting) => { 495 | combinedAction45Status.push(_actionsExecuting); 496 | }); 497 | 498 | store 499 | .select(hasActionsExecuting([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 500 | .subscribe((_actionsExecuting) => { 501 | combinedAction456Status.push(_actionsExecuting); 502 | }); 503 | 504 | store.dispatch(new NestedAsyncAction5()); 505 | tick(1); 506 | expect(nestedAction4Status).toEqual([false]); 507 | expect(nestedAction5Status).toEqual([false, true]); 508 | expect(nestedAction6Status).toEqual([false, true, false]); 509 | tick(100); 510 | expect(nestedAction4Status).toEqual([false]); 511 | expect(nestedAction5Status).toEqual([false, true, false]); 512 | expect(nestedAction6Status).toEqual([false, true, false]); 513 | expect(combinedAction45Status).toEqual([false, true, false]); 514 | expect(combinedAction456Status).toEqual([false, true, false]); 515 | })); 516 | 517 | it('should be executing on nested actions (scenario 4)', fakeAsync(() => { 518 | const nestedAction4Status: boolean[] = []; 519 | const nestedAction5Status: boolean[] = []; 520 | const nestedAction6Status: boolean[] = []; 521 | 522 | const combinedAction45Status: boolean[] = []; 523 | const combinedAction456Status: boolean[] = []; 524 | 525 | store.select(hasActionsExecuting([NestedAsyncAction4])).subscribe((_actionsExecuting) => { 526 | nestedAction4Status.push(_actionsExecuting); 527 | }); 528 | 529 | store.select(hasActionsExecuting([NestedAsyncAction5])).subscribe((_actionsExecuting) => { 530 | nestedAction5Status.push(_actionsExecuting); 531 | }); 532 | 533 | store.select(hasActionsExecuting([NestedAsyncAction6])).subscribe((_actionsExecuting) => { 534 | nestedAction6Status.push(_actionsExecuting); 535 | }); 536 | 537 | store 538 | .select(hasActionsExecuting([NestedAsyncAction4, NestedAsyncAction5])) 539 | .subscribe((_actionsExecuting) => { 540 | combinedAction45Status.push(_actionsExecuting); 541 | }); 542 | 543 | store 544 | .select(hasActionsExecuting([NestedAsyncAction4, NestedAsyncAction5, NestedAsyncAction6])) 545 | .subscribe((_actionsExecuting) => { 546 | combinedAction456Status.push(_actionsExecuting); 547 | }); 548 | 549 | store.dispatch([new NestedAsyncAction4(), new NestedAsyncAction5(), new NestedAsyncAction6()]); 550 | tick(1); 551 | expect(nestedAction4Status).toEqual([false, true, false]); 552 | expect(nestedAction5Status).toEqual([false, true]); 553 | expect(nestedAction6Status).toEqual([false, true, false]); 554 | tick(100); 555 | expect(nestedAction4Status).toEqual([false, true, false]); 556 | expect(nestedAction5Status).toEqual([false, true, false]); 557 | expect(nestedAction6Status).toEqual([false, true, false]); 558 | 559 | expect(combinedAction45Status).toEqual([false, true, false]); 560 | expect(combinedAction456Status).toEqual([false, true, false]); 561 | })); 562 | }); 563 | }); 564 | }); 565 | 566 | describe('No Actions', () => { 567 | describe('sync', () => { 568 | it('should be executing between dispatch and complete', () => { 569 | const actionStatus: boolean[] = []; 570 | 571 | store.select(hasActionsExecuting()).subscribe((_actionsExecuting) => { 572 | actionStatus.push(_actionsExecuting); 573 | }); 574 | 575 | store.dispatch(new Action1()); 576 | store.dispatch(new Action2()); 577 | expect(actionStatus).toEqual([false, true, false, true, false]); 578 | }); 579 | }); 580 | 581 | describe('async', () => { 582 | it('should be executing between dispatch and complete ', fakeAsync(() => { 583 | const actionStatus: boolean[] = []; 584 | 585 | store.select(hasActionsExecuting()).subscribe((_actionsExecuting) => { 586 | actionStatus.push(_actionsExecuting); 587 | }); 588 | 589 | store.dispatch(new AsyncAction1()); 590 | tick(1); 591 | expect(actionStatus).toEqual([false, true, false]); 592 | store.dispatch(new AsyncAction2()); 593 | tick(1); 594 | expect(actionStatus).toEqual([false, true, false, true, false]); 595 | })); 596 | }); 597 | }); 598 | }); 599 | }); 600 | -------------------------------------------------------------------------------- /src/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/lib", 5 | "declarationMap": true, 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": ["dom", "es2018"] 10 | }, 11 | "angularCompilerOptions": { 12 | "skipTemplateCodegen": true, 13 | "strictMetadataEmit": true, 14 | "fullTemplateTypeCheck": true, 15 | "strictInjectionParameters": true, 16 | "enableResourceInlining": true 17 | }, 18 | "exclude": ["src/test.ts", "**/*.spec.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /src/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": ["jest", "node"] 6 | }, 7 | "files": ["src/test.ts"], 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tools/copy-readme.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { existsSync, createReadStream, createWriteStream } from 'fs'; 3 | import { name } from '../package.json'; 4 | 5 | function copyReadmeAfterSuccessfulBuild(): void { 6 | const path = join(__dirname, '../README.md'); 7 | const noReadme = !existsSync(path); 8 | 9 | if (noReadme) { 10 | return console.log(`README.md doesn't exist on the root level!`); 11 | } 12 | 13 | createReadStream(path) 14 | .pipe(createWriteStream(join(__dirname, `../dist/${name}/README.md`))) 15 | .on('finish', () => { 16 | console.log(`Successfully copied README.md into dist/${name} folder!`); 17 | }); 18 | } 19 | 20 | copyReadmeAfterSuccessfulBuild(); 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "noUnusedLocals": true, 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "target": "ES2022", 13 | "lib": ["es2017", "dom"], 14 | "module": "es2020", 15 | "typeRoots": ["node_modules/@types"], 16 | "paths": { 17 | "@ngxs-labs/actions-executing": ["src/public-api.ts"] 18 | }, 19 | "useDefineForClassFields": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "module": "CommonJs", 6 | "types": ["jest"] 7 | }, 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "allowJs": true, 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-ordering": [ 35 | true, 36 | { 37 | "order": [ 38 | "static-field", 39 | "instance-field", 40 | "static-method", 41 | "instance-method" 42 | ] 43 | } 44 | ], 45 | "no-arg": true, 46 | "no-bitwise": true, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-construct": true, 56 | "no-debugger": true, 57 | "no-duplicate-super": true, 58 | "no-empty": false, 59 | "no-empty-interface": true, 60 | "no-eval": true, 61 | "no-misused-new": true, 62 | "no-non-null-assertion": true, 63 | "no-shadowed-variable": true, 64 | "no-string-literal": false, 65 | "no-string-throw": true, 66 | "no-switch-case-fall-through": true, 67 | "no-trailing-whitespace": true, 68 | "no-unused-expression": true, 69 | "no-var-keyword": true, 70 | "object-literal-sort-keys": false, 71 | "one-line": [ 72 | true, 73 | "check-open-brace", 74 | "check-catch", 75 | "check-else", 76 | "check-whitespace" 77 | ], 78 | "prefer-const": true, 79 | "quotemark": [ 80 | true, 81 | "single" 82 | ], 83 | "radix": true, 84 | "semicolon": [ 85 | true, 86 | "always" 87 | ], 88 | "triple-equals": [ 89 | true, 90 | "allow-null-check" 91 | ], 92 | "typedef-whitespace": [ 93 | true, 94 | { 95 | "call-signature": "nospace", 96 | "index-signature": "nospace", 97 | "parameter": "nospace", 98 | "property-declaration": "nospace", 99 | "variable-declaration": "nospace" 100 | } 101 | ], 102 | "unified-signatures": true, 103 | "variable-name": false, 104 | "whitespace": [ 105 | true, 106 | "check-branch", 107 | "check-decl", 108 | "check-operator", 109 | "check-separator", 110 | "check-type" 111 | ], 112 | "no-output-rename": true, 113 | "use-life-cycle-interface": true, 114 | "use-pipe-transform-interface": true, 115 | "component-class-suffix": true, 116 | "directive-class-suffix": true, 117 | "directive-selector-prefix": [ 118 | false, 119 | "app" 120 | ], 121 | "component-selector-prefix": [ 122 | false, 123 | "app" 124 | ], 125 | "directive-selector-name": [ 126 | false, 127 | "camelCase" 128 | ], 129 | "component-selector-name": [ 130 | false, 131 | "kebab-case" 132 | ], 133 | "directive-selector-type": [ 134 | false, 135 | "attribute" 136 | ], 137 | "component-selector": [ 138 | false, 139 | "element", 140 | "my-prefix", 141 | "kebab-case" 142 | ], 143 | "no-unnecessary-initializer": false, 144 | "no-inferrable-types": false, 145 | "no-any": true, 146 | "no-duplicate-variable": true, 147 | "member-access": false 148 | } 149 | } -------------------------------------------------------------------------------- /yarn.lock.readme.md: -------------------------------------------------------------------------------- 1 | All of our npm dependencies are locked via the `yarn.lock` file for the following reasons: 2 | 3 | - our project has lots of dependencies which update at unpredictable times, so it's important that 4 | we update them explicitly once in a while rather than implicitly when any of us runs `yarn install` 5 | - locked dependencies allow us to reuse yarn cache on travis, significantly speeding up our builds 6 | (by 5 minutes or more) 7 | - locked dependencies allow us to detect when node_modules folder is out of date after a branch switch 8 | which allows us to build the project with the correct dependencies every time 9 | 10 | Before changing a dependency, do the following: 11 | 12 | - make sure you are in sync with `upstream/master`: `git fetch upstream && git rebase upstream/master` 13 | - ensure that your `node_modules` directory is not stale by running `yarn install` 14 | 15 | 16 | To add a new dependency do the following: `yarn add --dev` 17 | 18 | To update an existing dependency do the following: run `yarn upgrade @ --dev` 19 | or `yarn upgrade --dev` to update to the latest version that matches version constraint 20 | in `package.json` 21 | 22 | To Remove an existing dependency do the following: run `yarn remove ` 23 | 24 | 25 | Once you've changed the dependency, commit the changes to `package.json` & `yarn.lock`, and you are done. 26 | --------------------------------------------------------------------------------