├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .idea ├── dictionaries │ └── kay.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── ngx-redux.iml ├── typescript-compiler.xml └── vcs.xml ├── .npmrc ├── .travis.yml ├── LICENSE ├── README.md ├── angular.json ├── docs ├── api │ ├── index.md │ └── redux-state-provider.md ├── articles │ ├── index.md │ ├── select-pattern.md │ └── testing-guide.md ├── decorators │ ├── index.md │ ├── redux-action-context.md │ ├── redux-action.md │ ├── redux-reducer.md │ └── redux-state.md ├── how-to │ ├── create-an-actions-provider.md │ ├── index.md │ ├── provide-your-own-store.md │ └── use-lazy-loading.md ├── index.md ├── pipes │ ├── index.md │ └── redux-select.md ├── reducer-switch-case.gif └── ts-support.gif ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── protractor.conf.js ├── renovate.json ├── src ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── example-app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ └── todo │ │ ├── list │ │ ├── todo-list-item.ts │ │ ├── todo-list.component.html │ │ ├── todo-list.component.spec.ts │ │ ├── todo-list.component.ts │ │ ├── todo-list.reducer.spec.ts │ │ └── todo-list.reducer.ts │ │ ├── todo.module.state.provider.ts │ │ ├── todo.module.state.ts │ │ └── todo.module.ts ├── favicon.ico ├── harmowatch │ └── ngx-redux-core │ │ ├── decorators │ │ ├── index.ts │ │ ├── redux-select.decorator.spec.ts │ │ └── redux-select.decorator.ts │ │ ├── index.ts │ │ ├── interfaces │ │ ├── redux-action-with-payload.interface.ts │ │ ├── redux-child-module-config.interface.ts │ │ ├── redux-root-module-config.interface.ts │ │ ├── redux-root-state.interface.ts │ │ └── redux-state-definition.interface.ts │ │ ├── pipes │ │ ├── redux-select.pipe.spec.ts │ │ └── redux-select.pipe.ts │ │ ├── providers │ │ ├── redux-reducer.provider.spec.ts │ │ ├── redux-reducer.provider.ts │ │ ├── redux-registry.ts │ │ ├── redux-state.provider.spec.ts │ │ └── redux-state.provider.ts │ │ ├── redux-selector.spec.ts │ │ ├── redux-selector.ts │ │ ├── redux.module.spec.ts │ │ ├── redux.module.ts │ │ ├── testing │ │ ├── selector │ │ │ └── suite.config.ts │ │ ├── state.ts │ │ ├── store.spec.ts │ │ └── store.ts │ │ └── tokens │ │ ├── redux-middlewares.token.ts │ │ ├── redux-state-definition.token.ts │ │ └── redux-store.token.ts ├── index.html ├── main.ts ├── plugins │ └── karma │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── matchers │ │ ├── index.spec.ts │ │ ├── index.ts │ │ ├── not-to-mutate-the-given-state.spec.ts │ │ ├── not-to-mutate-the-given-state.ts │ │ ├── to-dispatch-action.spec.ts │ │ ├── to-dispatch-action.ts │ │ ├── to-reduce-on.spec.ts │ │ └── to-reduce-on.ts │ │ └── webpack.config.js ├── polyfills.ts ├── styles.css ├── test.ts ├── test │ ├── test-bed.spec.ts │ └── vehicle │ │ ├── bike │ │ ├── bike.actions.provider.spec.ts │ │ ├── bike.actions.provider.ts │ │ ├── bike.reducer.spec.ts │ │ ├── bike.reducer.ts │ │ └── bike.state.ts │ │ ├── car │ │ ├── car.actions.provider.ts │ │ ├── car.reducer.spec.ts │ │ ├── car.reducer.ts │ │ └── car.state.ts │ │ ├── vehicle.actions.provider.ts │ │ ├── vehicle.module.ts │ │ ├── vehicle.spec.ts │ │ ├── vehicle.state.provider.spec.ts │ │ └── vehicle.state.provider.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json └── tslint.json /.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/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **I'm submitting a ...** (check one with "x") 2 | ``` 3 | [ ] bug report => check the FAQ and search github for a similar issue or PR before submitting 4 | [ ] support request => check the FAQ and search github for a similar issue before submitting 5 | [ ] feature request 6 | ``` 7 | 8 | **Current behavior** 9 | 10 | 11 | **Expected/desired behavior** 12 | 13 | 14 | **Reproduction of the problem** 15 | If the current behavior is a bug or you can illustrate your feature request better with an example, please provide the steps to reproduce and if possible a minimal demo of the problem via https://plnkr.co or similar. You can use this template as a starting point: http://plnkr.co/edit/tpl:WccVZSBM0rUgq2sXSUbe 16 | 17 | 18 | **What is the expected behavior?** 19 | 20 | 21 | **What is the motivation / use case for changing the behavior?** 22 | 23 | 24 | **Please tell us about your environment:** 25 | 26 | * **ngx-redux version:** x.x.x 27 | 28 | * **Angular version:** 2.x.x 29 | 30 | * **Browser:** [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /.ng_build 8 | /.npm 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | *.sublime-workspace 21 | 22 | # IDE - VSCode 23 | .vscode/* 24 | !.vscode/settings.json 25 | !.vscode/tasks.json 26 | !.vscode/launch.json 27 | !.vscode/extensions.json 28 | 29 | # misc 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | testem.log 36 | /typings 37 | .coveralls.yml 38 | 39 | # e2e 40 | /e2e/*.js 41 | /e2e/*.map 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | github-page/ 47 | dist.tgz 48 | -------------------------------------------------------------------------------- /.idea/dictionaries/kay.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | everytime 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/ngx-redux.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/typescript-compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | safe-exact=true 2 | registry=https://registry.npmjs.org/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | before_install: 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | 10 | install: 11 | - npm install 12 | 13 | before_script: 14 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 15 | - chmod +x ./cc-test-reporter 16 | - ./cc-test-reporter before-build 17 | 18 | script: 19 | - npm run lint 20 | - npm run test 21 | - npm run build:package 22 | 23 | after_script: 24 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 25 | 26 | addons: 27 | chrome: stable 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 HarmoWatch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @harmowatch/ngx-redux-core 2 | 3 | [![Join the chat at https://gitter.im/harmowatch/ngx-redux-core](https://badges.gitter.im/harmowatch/ngx-redux-core.svg)](https://gitter.im/harmowatch/ngx-redux-core?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | [![npm version](https://badge.fury.io/js/%40harmowatch%2Fngx-redux-core.svg)](https://badge.fury.io/js/%40harmowatch%2Fngx-redux-core) 6 | [![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovateapp.com/) 7 | [![Build Status](https://travis-ci.org/HarmoWatch/ngx-redux-core.svg?branch=master)](https://travis-ci.org/HarmoWatch/ngx-redux-core) 8 | [![HitCount](http://hits.dwyl.io/harmowatch/ngx-redux-core.svg)](http://hits.dwyl.com/harmowatch/ngx-redux-core) 9 | [![Maintainability](https://api.codeclimate.com/v1/badges/24a417a5e870fbe5e94e/maintainability)](https://codeclimate.com/github/HarmoWatch/ngx-redux-core/maintainability) 10 | [![Test Coverage](https://api.codeclimate.com/v1/badges/24a417a5e870fbe5e94e/test_coverage)](https://codeclimate.com/github/HarmoWatch/ngx-redux-core/test_coverage) 11 | 12 | ## The modern [Redux](http://redux.js.org/) integration for [Angular](https://angular.io/) 6+ 13 | 14 | This package contains a number of features that makes working with [Angular](https://angular.io/) and [Redux](http://redux.js.org/) 15 | very easy and comfortable. This is achieved using [decorators](./docs/decorators/index.md). For example, you can decorate any class 16 | method with [@ReduxAction](./docs/decorators/redux-action.md). Every time the method is called it will dispatch a redux action. 17 | 18 | - [Main Features](#main-features) 19 | - [TypeScript support](#typescript-support) 20 | - [Reduced boilerplate](#reduced-boilerplate) 21 | - [Refactoring support](#refactoring-support) 22 | - [Easy to test](#easy-to-test) 23 | - [Matchers](#matchers) 24 | - [toDispatchAction](#todispatchaction) 25 | - [toReduceOn](#toreduceon) 26 | - [notToMutateTheGivenState](#nottomutatethegivenstate) 27 | - [The Select Pattern](#the-select-pattern) 28 | - [Lazy Loaded Modules](#lazy-loaded-modules) 29 | - [Redux DevTools Extension support](#redux-devtools-extension-support) 30 | - [What is Redux?](#what-is-redux) 31 | - [Installation](#installation) 32 | - [Quickstart](#quickstart) 33 | - [Documentation](./docs/index.md) 34 | 35 | ## Main Features 36 | 37 | ### TypeScript support 38 | 39 | One big advantage of this package is the [TypeScript](https://www.typescriptlang.org/) support for reducer functions. 40 | By using this package, you'll get a compiler error, if the payload of the redux action is not compatible with the reducer. 41 | 42 | ![TypeScript support](./docs/ts-support.gif "TypeScript support") 43 | 44 | ---- 45 | 46 | ### Reduced boilerplate 47 | 48 | The decorators will save you a lot of boilerplate code, so for example you don't have to call an extra 49 | service to dispatch the redux action anymore. Also the annoying switch-cases on the action-types are replaced by the 50 | [@ReduxReducer](./docs/decorators/redux-reducer.md) decorator: 51 | 52 | ![No switch case](./docs/reducer-switch-case.gif "No switch case") 53 | 54 | ---- 55 | 56 | ### Refactoring support 57 | 58 | Refactoring is improved as well, since you refer directly to the action method and not to a string. 59 | Therefore, your IDE can also modify your reducer, when the action method was renamed. 60 | 61 | ---- 62 | 63 | ### Easy to test 64 | 65 | #### Matchers 66 | 67 | There are some jasmine matchers provided by this package. This makes it easy to test whether a method triggers a redux action and 68 | if the reducer really listens to it. There is also a matcher available which will ensure that the reducer does not work on the state reference. 69 | For more information about testing and matcher installation, please see the [Testing Guide](./docs/articles/testing-guide.md). 70 | 71 | ##### toDispatchAction 72 | 73 | ```ts 74 | it('will dispatch a redux action', () => { 75 | expect(TodoListComponent.prototype.toggleListMode).toDispatchAction(); 76 | // or test for a specific action name 77 | expect(TodoListComponent.prototype.toggleListMode).toDispatchAction('toggleListMode'); 78 | }); 79 | ``` 80 | 81 | ##### toReduceOn 82 | 83 | ```ts 84 | it('listens to the correct actions', () => { 85 | expect(TodoReducer.prototype.add).toReduceOn(TodoListComponent.prototype.add); 86 | }); 87 | ``` 88 | 89 | ##### notToMutateTheGivenState 90 | 91 | ```ts 92 | it('does not mutate the given state', () => { 93 | expect(TodoReducer.prototype.add).notToMutateTheGivenState(state); 94 | }); 95 | ``` 96 | 97 | ---- 98 | 99 | ### The Select Pattern 100 | 101 | The [Select Pattern](./docs/articles/select-pattern.md) gives you a powerful tool-set at your hand, to select slices of your state. 102 | The easiest way to access a state value is the [reduxSelect pipe](./docs/articles/select-pattern.md#the-reduxselect-decorator): 103 | 104 | ```angular2html 105 |
{{ 'some/state/path' | reduxSelect | async | json }}
106 | ``` 107 | 108 | ---- 109 | 110 | ### Lazy Loaded Modules 111 | 112 | [Lazy Loaded Modules](./docs/how-to/use-lazy-loading.md) are also supported. 113 | So you can only initialize the reducer and the state when the respective NgModule is loaded. 114 | 115 | ---- 116 | 117 | ### Redux DevTools Extension support 118 | 119 | The [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) is fully supported and automatically 120 | enabled if your Angular app is running [in dev mode](https://angular.io/api/core/isDevMode). 121 | 122 | ---- 123 | 124 | ### What is Redux? 125 | 126 | [Redux](http://redux.js.org/) is a popular and common approach to manage an application state. 127 | The three principles of redux are: 128 | 129 | - [Single source of truth](http://redux.js.org/docs/introduction/ThreePrinciples.html#single-source-of-truth) 130 | - [State is read-only](http://redux.js.org/docs/introduction/ThreePrinciples.html#state-is-read-only) 131 | - [Changes are made with pure functions](http://redux.js.org/docs/introduction/ThreePrinciples.html#changes-are-made-with-pure-functions) 132 | 133 | ---- 134 | 135 | ## Installation 136 | 137 | The [redux](https://github.com/reactjs/redux) package itself is not shipped with @harmowatch/ngx-redux-core. 138 | Therefore you also have to install the redux package: 139 | 140 | ```sh 141 | $ npm install redux @harmowatch/ngx-redux-core --save 142 | ``` 143 | 144 | ---- 145 | 146 | ## Quickstart 147 | 148 | ### 1. Import the root `ReduxModule`: 149 | 150 | As the first step, you need to add `ReduxModule.forRoot()` to the root NgModule of your application. 151 | 152 | The static [`forRoot`](https://angular.io/docs/ts/latest/guide/ngmodule.html#!#core-for-root) method is a convention 153 | that provides and configures services at the same time. Make sure you call this method only in your root NgModule! 154 | 155 | Please note that [Lazy loading](./docs/how-to/use-lazy-loading.md) is also supported. 156 | 157 | ```ts 158 | import { NgModule } from '@angular/core'; 159 | import { BrowserModule } from '@angular/platform-browser'; 160 | import { ReduxModule } from '@harmowatch/ngx-redux-core'; 161 | 162 | import {YourModuleStateProvider} from '...'; 163 | import {TodoListReducer} from '...'; 164 | 165 | @NgModule({ 166 | imports: [ 167 | BrowserModule, 168 | ReduxModule.forRoot({ 169 | state: { 170 | provider: YourModuleStateProvider, // You'll create it in step 2 171 | reducers: [ TodoListReducer ], // You'll create it in step 4 172 | } 173 | }), 174 | ], 175 | providers: [ 176 | YourModuleStateProvider // You'll create it in step 2 177 | ], 178 | }) 179 | export class AppModule {} 180 | ``` 181 | 182 | ### 2. Create a state provider 183 | 184 | Now you have to create a provider for your module in order to describe and initialize the state. 185 | 186 | ```ts 187 | import { Injectable } from '@angular/core'; 188 | import { ReduxState, ReduxStateProvider } from '@harmowatch/ngx-redux-core'; 189 | 190 | export interface YourModuleState { 191 | items: string[]; 192 | } 193 | 194 | @Injectable() 195 | @ReduxState({name: 'your-module'}) // Make sure you choose a application-wide unique name 196 | export class YourModuleStateProvider extends ReduxStateProvider { 197 | 198 | getInitialState(): Promise { // You can return Observable or YourModuleState as well 199 | return Promise.resolve({ 200 | items: [] 201 | }); 202 | }} 203 | 204 | } 205 | ``` 206 | 207 | > Don't forget to add the state as described in step 1 208 | 209 | You can have just one `ReduxStateProvider` per NgModule. But it's possible to have a state provider for each 210 | [lazy loaded](./docs/how-to/use-lazy-loading.md) module. 211 | 212 | ### 3. Create an action dispatcher 213 | 214 | To initiate a state change, a redux action must be dispatched. Let's assume that there is a component called 215 | `TodoListComponent` that displays a button. Each time the button is clicked, the view calls the function 216 | `addTodo` and passes the todo, which shall be added to the list. 217 | 218 | All you have to do is decorate the function with `@ReduxAction` and return the todo as a return value. 219 | 220 | ```ts 221 | import { Component } from '@angular/core'; 222 | import { ReduxAction } from '@harmowatch/ngx-redux-core'; 223 | 224 | @Component({templateUrl: './todo-list.component.html'}) 225 | export class TodoListComponent { 226 | 227 | @ReduxAction() 228 | addTodo(label: string): string { 229 | return label; // your return value is the payload 230 | } 231 | 232 | } 233 | ``` 234 | 235 | Now the following action is dispatched, every time the `addTodo` method was called: 236 | 237 | ```json 238 | { 239 | "type": "addTodo", 240 | "payload": "SampleTodo" 241 | } 242 | ``` 243 | 244 | [You can also create a provider to dispatch actions.](./docs/how-to/create-an-actions-provider.md) 245 | 246 | ### 4. Create the reducer 247 | 248 | There's one more thing you need to do. You dispatch an action, but at the moment no reducer is listening to it. 249 | In order to change this, we need to create a reducer function that can make the state change as soon as the action 250 | is fired: 251 | 252 | ```ts 253 | import { ReduxReducer, ReduxActionWithPayload } from '@harmowatch/ngx-redux-core'; 254 | 255 | import {TodoListComponent} from '...'; 256 | 257 | export class TodoListReducer { 258 | 259 | @ReduxReducer(TodoListComponent.prototype.add) 260 | addTodo(state: TodoState, action: ReduxActionWithPayload): TodoState { 261 | return { 262 | ...state, 263 | items: state.items.concat(action.payload), 264 | }; 265 | } 266 | 267 | } 268 | ``` 269 | 270 | > Don't forget to add the state as described in step 1 271 | 272 | ### 5. Select values from the state 273 | 274 | To select a state value, you just can use the [reduxSelect](./docs/pipes/redux-select.md) pipe. 275 | But you've several options to select a state value. Please check out the 276 | [Select Pattern](./docs/articles/select-pattern.md) article for more information. 277 | 278 | ```angular2html 279 |
    280 |
  • {{todo}}
  • 281 |
282 | ``` 283 | 284 | ---- 285 | 286 | ## Documentation 287 | 288 | You'll find the latest docs [here](./docs/index.md). 289 | 290 | ---- 291 | 292 | You like the project? Then please give me a Star and add you to the list of [Stargazers](https://github.com/HarmoWatch/ngx-redux-core/stargazers). 293 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-redux": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/favicon.ico" 22 | ], 23 | "styles": [ 24 | "src/styles.css" 25 | ], 26 | "scripts": [] 27 | }, 28 | "configurations": { 29 | "production": { 30 | "optimization": true, 31 | "outputHashing": "all", 32 | "sourceMap": false, 33 | "extractCss": true, 34 | "namedChunks": false, 35 | "aot": true, 36 | "extractLicenses": true, 37 | "vendorChunk": false, 38 | "buildOptimizer": true, 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ] 45 | } 46 | } 47 | }, 48 | "serve": { 49 | "builder": "@angular-devkit/build-angular:dev-server", 50 | "options": { 51 | "browserTarget": "ngx-redux:build" 52 | }, 53 | "configurations": { 54 | "production": { 55 | "browserTarget": "ngx-redux:build:production" 56 | } 57 | } 58 | }, 59 | "extract-i18n": { 60 | "builder": "@angular-devkit/build-angular:extract-i18n", 61 | "options": { 62 | "browserTarget": "ngx-redux:build" 63 | } 64 | }, 65 | "test": { 66 | "builder": "@angular-devkit/build-angular:karma", 67 | "options": { 68 | "main": "src/test.ts", 69 | "karmaConfig": "./karma.conf.js", 70 | "polyfills": "src/polyfills.ts", 71 | "tsConfig": "src/tsconfig.spec.json", 72 | "scripts": [], 73 | "styles": [ 74 | "src/styles.css" 75 | ], 76 | "assets": [ 77 | "src/assets", 78 | "src/favicon.ico" 79 | ] 80 | } 81 | }, 82 | "lint": { 83 | "builder": "@angular-devkit/build-angular:tslint", 84 | "options": { 85 | "tsConfig": [ 86 | "src/tsconfig.app.json", 87 | "src/tsconfig.spec.json" 88 | ], 89 | "exclude": [ 90 | "**/node_modules/**" 91 | ] 92 | } 93 | } 94 | } 95 | }, 96 | "ngx-redux-e2e": { 97 | "root": "e2e", 98 | "sourceRoot": "e2e", 99 | "projectType": "application", 100 | "architect": { 101 | "e2e": { 102 | "builder": "@angular-devkit/build-angular:protractor", 103 | "options": { 104 | "protractorConfig": "./protractor.conf.js", 105 | "devServerTarget": "ngx-redux:serve" 106 | } 107 | }, 108 | "lint": { 109 | "builder": "@angular-devkit/build-angular:tslint", 110 | "options": { 111 | "tsConfig": [ 112 | "e2e/tsconfig.e2e.json" 113 | ], 114 | "exclude": [ 115 | "**/node_modules/**" 116 | ] 117 | } 118 | } 119 | } 120 | } 121 | }, 122 | "defaultProject": "ngx-redux", 123 | "schematics": { 124 | "@schematics/angular:component": { 125 | "prefix": "app", 126 | "styleext": "css" 127 | }, 128 | "@schematics/angular:directive": { 129 | "prefix": "app" 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) 2 | 3 | # API 4 | 5 | - [ReduxStateProvider](./redux-state-provider.md) 6 | -------------------------------------------------------------------------------- /docs/api/redux-state-provider.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) / [api](./index.md) 2 | 3 | # ReduxStateProvider 4 | 5 | ### getInitialState 6 | 7 | To describe your module state, you need to create a state provider. This provider has to extend the `ReduxStateProvider` 8 | class and to provide the method `getInitialState`. As initial state you can return a `Promise`, `Observable` or directly 9 | a literal. Please note that the `ReduxStateProvider` is a generic class to which you must pass your state type. The 10 | `getInitialState` method is called by [@harmowatch/ngx-redux-core](../../README.md) during the initialization process. 11 | 12 | ##### `getInitialState` returns a literal 13 | 14 | ```ts 15 | import { Injectable } from '@angular/core'; 16 | import { ReduxState, ReduxStateProvider } from '@harmowatch/ngx-redux-core'; 17 | 18 | @Injectable() 19 | @ReduxState({name: 'your-module'}) 20 | export class YourModuleStateProvider extends ReduxStateProvider { 21 | 22 | getInitialState(): YourModuleState { 23 | return { 24 | items: [] 25 | }; 26 | }} 27 | 28 | } 29 | ``` 30 | 31 | ##### `getInitialState` returns a `Promise` 32 | 33 | The module state is not initialized until your Promise has been *resolved successfully*. 34 | If your `Promise` is rejected, the state will never be initialized! 35 | 36 | ```ts 37 | import { Injectable } from '@angular/core'; 38 | import { ReduxState, ReduxStateProvider } from '@harmowatch/ngx-redux-core'; 39 | 40 | @Injectable() 41 | @ReduxState({name: 'your-module'}) 42 | export class YourModuleStateProvider extends ReduxStateProvider { 43 | 44 | getInitialState(): Promise { 45 | return Promise.resolve({ 46 | items: [] 47 | }); 48 | }} 49 | 50 | } 51 | ``` 52 | 53 | ##### `getInitialState` returns a `Observable` 54 | 55 | The module state is initialized *after* the `Observable` has been completed. 56 | If your `Observable` never completes, the state will never be initialized! 57 | 58 | ```ts 59 | import 'rxjs/add/observable/from'; 60 | 61 | import { Injectable } from '@angular/core'; 62 | import { ReduxState, ReduxStateProvider } from '@harmowatch/ngx-redux-core'; 63 | 64 | @Injectable() 65 | @ReduxState({name: 'your-module'}) 66 | export class YourModuleStateProvider extends ReduxStateProvider { 67 | 68 | getInitialState(): Observable { 69 | return Observable.from([{ 70 | items: [] 71 | }]); 72 | } 73 | 74 | } 75 | ``` 76 | 77 | ### select 78 | 79 | This method is used by the [reduxSelect](../articles/select-pattern.md#the-reduxselect-pipe) pipe and the 80 | [@ReduxSelect](../articles/select-pattern.md#the-reduxselect-decorator) decorator to resolve the state. 81 | The `ReduxStateProvider`'s implementation caches the `select` calls, and returns the same instance of a 82 | [ReduxSelector](../articles/select-pattern.md) for the same selector string. You can overwrite this method to 83 | implement some custom `select` behavior. 84 | 85 | ### reduce 86 | 87 | This method is called by [@harmowatch/ngx-redux-core](../../README.md) to reduce your module's state. There's already a 88 | default implementation which will forward the reduce command to the appropriate 89 | [@ReduxReducer](../decorators/redux-reducer.md). You can overwrite this method to implement some custom reducer behavior. 90 | 91 | ### getState 92 | 93 | Returns a `Promise` with a snapshot of the state. A basic principle of [rxjs](https://github.com/ReactiveX/rxjs) is that 94 | you should not break out of the event chain, so it is better to use the [select](#select) method. In exceptional cases, the 95 | use of the `getState` method can also make sense. The `getState` method isn't used by [@harmowatch/ngx-redux-core](../../README.md) 96 | internally. 97 | -------------------------------------------------------------------------------- /docs/articles/index.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) 2 | 3 | # Articles 4 | 5 | - [Select Pattern](./select-pattern.md) 6 | - [Testing Guide](./testing-guide.md) 7 | -------------------------------------------------------------------------------- /docs/articles/select-pattern.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [articles](./index.md) 2 | 3 | # The Select Pattern 4 | 5 | The select pattern allows you to get slices of your state as `ReduxSelector`. The `ReduxSelector` 6 | is a class which extends [RxJS](https://github.com/ReactiveX/rxjs)'s `ReplaySubject`. The 7 | `bufferSize` of the `ReplaySubject` is set to 1. The `ReplaySubject` has the advantage that you 8 | get the current state value immediately when subscribing to the `ReduxSelector`. 9 | 10 | Because that the ReduxSelector is based on the `ReplaySubject`, you have all the power of 11 | [RxJS](https://github.com/ReactiveX/rxjs) at your fingertips. 12 | 13 | To select a value, you must always specify a path. That path is separated by a "/". If your selector starts 14 | with a "/" it's called **absolute selector**, otherwise it's an **relative selector**. The difference is that 15 | in case of an **absolute selector** you have to know the name of the [ReduxStateProvider](../api/redux-state-provider.md). 16 | 17 | Therefore it's recommended to use always a **relative selector**, otherwise you will spread the name over the whole 18 | program code. I just wanted to mention the **absolute selector**, so you know it exists. 19 | 20 | Let's suppose, you have defined the following [ReduxStateProvider](../api/redux-state-provider.md): 21 | 22 | ```ts 23 | import { Injectable } from '@angular/core'; 24 | import { ReduxState, ReduxStateProvider } from '@harmowatch/ngx-redux-core'; 25 | 26 | interface YourModuleState { 27 | todo: { 28 | items: string[]; 29 | isFetching: boolean; 30 | } 31 | } 32 | 33 | @Injectable() 34 | @ReduxState({name: 'your-module'}) 35 | export class YourModuleStateProvider extends ReduxStateProvider { 36 | 37 | getInitialState(): YourModuleState { 38 | return { 39 | todo: { 40 | items: [], 41 | isFetching: false, 42 | } 43 | }; 44 | }} 45 | 46 | } 47 | ``` 48 | 49 | Then you can use the following selectors: 50 | 51 | | **relative selector** (recommended) | **absolute selector** (avoid) | 52 | | ----------------------------------- | ------------------------------ | 53 | | "" | "/your-module" | 54 | | "todo" | "/your-module/todo" | 55 | | "todo/items" | "/your-module/todo/items" | 56 | | "todo/isFetching" | "/your-module/todo/isFetching" | 57 | 58 | ## The reduxSelect pipe 59 | 60 | You can select values direct out of your view. This is very comfortable to use, because you don't have 61 | to create a class property in your component class. To select the value, the pipe will use the 62 | [ReduxStateProvider](../api/redux-state-provider.md#select)'s `select` method. 63 | 64 | ```angular2html 65 |
    66 |
  • 67 | {{todo}} 68 |
  • 69 |
70 | ``` 71 | 72 | ## The @ReduxSelect decorator 73 | 74 | The `@ReduxSelect` decorator can be added to any class property. It will turn the property into an `ReduxSelector` 75 | as described above. Unfortunately, the `@ReduxSelect` decorator cannot automatically determine the correct 76 | [ReduxStateProvider](../api/redux-state-provider.md). Therefore, you have to specify it as second argument. 77 | 78 | To select the value, the decorator will use the [ReduxStateProvider](../api/redux-state-provider.md#select)'s 79 | `select` method. 80 | 81 | If you use an **absolute selector**, you can omit the second argument, which is ignored anyway. 82 | 83 | ```ts 84 | class SomeClass { 85 | 86 | @ReduxSelect('todo/items', YourModuleStateProvider) 87 | private todoItems: ReduxSelector; 88 | 89 | constructor() { 90 | this.todoItems.subscribe(todoItems => { 91 | console.log('todo items', todoItems); 92 | }); 93 | } 94 | 95 | } 96 | ``` 97 | 98 | ## The ReduxStateProvider 99 | 100 | Another simple way to select state values is the [ReduxStateProvider](../api/redux-state-provider.md). 101 | You can simply inject it via Angular's dependency injector. 102 | 103 | ```ts 104 | @Component({ 105 | templateUrl: './some.component.html', 106 | }) 107 | class SomeComponent { 108 | 109 | constructor(state: YourModuleStateProvider) { 110 | state.select('todo/items').subscribe(todoItems => { 111 | console.log('todo items', todoItems); 112 | }); 113 | } 114 | 115 | } 116 | ``` 117 | 118 | ## The ReduxSelector 119 | 120 | You can instantiate the `ReduxSelector` by yourself. As with the decorator, the [ReduxStateProvider](../api/redux-state-provider.md#select) 121 | cannot be determined automatically even with manual instantiation. Therefore, you have to specify it as second argument. 122 | If you instantiate the `ReduxSelector` by yourself, the [ReduxStateProvider](../api/redux-state-provider.md#select)'s 123 | `select` method is **not** used. 124 | 125 | If you use an **absolute selector**, you can omit the second argument, which is ignored anyway. 126 | 127 | ```ts 128 | class SomeClass { 129 | 130 | constructor(zone: NgZone) { 131 | new ReduxSelector(zone, 'todo/items', YourModuleStateProvider).subscribe(todoItems => { 132 | console.log('todo items', todoItems); 133 | }); 134 | } 135 | 136 | } 137 | ``` 138 | -------------------------------------------------------------------------------- /docs/articles/testing-guide.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [articles](./index.md) 2 | 3 | # Testing Guide 4 | 5 | ## Matcher 6 | 7 | There are some jasmine matchers provided by this package. This makes it easy to test whether a method triggers a redux action and 8 | if the reducer really listens to it. There is also a matcher available which will ensure that the reducer does not work on the state reference. 9 | 10 | ### Installation 11 | 12 | #### Add the framework and the plugin to your karma config 13 | 14 | ```js 15 | module.exports = function (config) { 16 | config.set({ 17 | // ... 18 | frameworks: ['jasmine', '@angular/cli', '@harmowatch/ngx-redux-core'], // (1) Enable the framework 19 | plugins: [ 20 | require('karma-jasmine'), 21 | require('karma-chrome-launcher'), 22 | require('karma-jasmine-html-reporter'), 23 | require('karma-coverage-istanbul-reporter'), 24 | require('@angular/cli/plugins/karma'), 25 | require('@harmowatch/ngx-redux-core/plugins/karma') // (2) Require the plugin 26 | ], 27 | // ... 28 | }); 29 | }; 30 | ``` 31 | 32 | ### Usage 33 | 34 | ##### toDispatchAction 35 | 36 | ```ts 37 | it('will dispatch a redux action', () => { 38 | expect(TodoListComponent.prototype.toggleListMode).toDispatchAction(); 39 | // or test for a specific action name 40 | expect(TodoListComponent.prototype.toggleListMode).toDispatchAction('toggleListMode'); 41 | }); 42 | ``` 43 | 44 | ##### toReduceOn 45 | 46 | ```ts 47 | it('listens to the correct actions', () => { 48 | expect(TodoReducer.prototype.add).toReduceOn(TodoListComponent.prototype.add); 49 | }); 50 | 51 | // or you can check for many actions 52 | it('listens to the correct actions', () => { 53 | expect(TodoReducer.prototype.add).toReduceOn( 54 | 'addTodo', 55 | TodoComponent.prototype.add, 56 | TodoListComponent.prototype.add, 57 | ); 58 | }); 59 | ``` 60 | 61 | ##### notToMutateTheGivenState 62 | 63 | ```ts 64 | it('does not mutate the given state', () => { 65 | expect(TodoReducer.prototype.add).notToMutateTheGivenState(state); 66 | }); 67 | ``` 68 | 69 | ## ReduxTestingStore 70 | 71 | The ReduxTestingStore provides a [Store](https://redux.js.org/docs/api/Store.html) implementation that can be used in 72 | your unit tests. It adds an additional method `setState()` which allows easy state manipulation. 73 | 74 | ### Example 75 | 76 | ```ts 77 | describe('TodoListComponent', () => { 78 | 79 | let fixture: ComponentFixture; 80 | let testingStore: ReduxTestingStore; 81 | 82 | beforeEach(async(() => { 83 | TestBed.configureTestingModule({ 84 | declarations: [ 85 | TodoListComponent 86 | ], 87 | imports: [ 88 | ReduxModule.forRoot({ 89 | storeFactory: ReduxTestingStore.factory, // (1) Provide the ReduxTestingStore.factory 90 | state: { 91 | provider: TodoStateProvider, 92 | } 93 | }) 94 | ], 95 | providers: [ 96 | TodoStateProvider, 97 | ], 98 | }).compileComponents(); 99 | })); 100 | 101 | beforeEach(async(() => { 102 | testingStore = TestBed.get(ReduxStore); 103 | fixture = TestBed.createComponent(TodoListComponent); 104 | fixture.detectChanges(); 105 | })); 106 | 107 | describe('todo rendering', () => { 108 | 109 | beforeEach(async(() => { 110 | 111 | /* 112 | * set the state in a async beforeEach block, so your "it" is called after the state change was applied 113 | */ 114 | testingStore.setState(TodoStateProvider, { 115 | ...TodoStateProvider.default, 116 | items: [{ 117 | uuid: '14cf5fdb-afcf-4297-9507-ff645e47d3af', 118 | label: 'Some todo', 119 | creationDate: '2018-03-30T14:44:01.452Z', 120 | }, { 121 | uuid: '38150de6-7ad6-4007-af71-409a668a449e', 122 | label: 'Some other todo', 123 | creationDate: '2018-03-30T14:44:26.796Z', 124 | }], 125 | }).then(() => fixture.detectChanges()); 126 | 127 | })); 128 | 129 | it('displays two todo items', () => { 130 | 131 | const todos = fixture.debugElement.queryAll(By.css('.todo-item > span')) 132 | .map(({nativeElement}) => nativeElement.innerText.trim()); 133 | 134 | expect(todos).toEqual([ 135 | 'Some todo', 136 | 'Some other todo', 137 | ]); 138 | 139 | }); 140 | 141 | }); 142 | 143 | }); 144 | ``` 145 | -------------------------------------------------------------------------------- /docs/decorators/index.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) 2 | 3 | # Decorators 4 | 5 | - [@ReduxAction](./redux-action.md) 6 | - [@ReduxActionContext](./redux-action-context.md) 7 | - [@ReduxReducer](./redux-reducer.md) 8 | - [@ReduxSelect](../articles/select-pattern.md#the-reduxselect-decorator) 9 | - [@ReduxState](./redux-state.md) 10 | -------------------------------------------------------------------------------- /docs/decorators/redux-action-context.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) / [decorators](./index.md) 2 | 3 | # @ReduxActionContext 4 | 5 | Marks a class as redux action context. This context is used by the [@ReduxAction](./redux-action.md) decorator to resolve the 6 | action type. 7 | 8 | ```ts 9 | @ReduxActionContext({ 10 | prefix: string; 11 | }) 12 | ``` 13 | 14 | ## Configuration Options 15 | 16 | ### ```prefix``` 17 | 18 | Sets the prefix that shall be used for all redux actions that will use this context. The prefix and the action type is 19 | concatenated to one string. 20 | 21 | | | | 22 | | ------------- | ------ | 23 | | type | string | 24 | | mandatory | yes | 25 | 26 | ```ts 27 | @ReduxActionContext({ 28 | prefix: 'some-prefix/' 29 | }) 30 | class SomeClass { // This will dispatch the following action: 31 | // 32 | @ReduxAction() // { 33 | public someMethod(): string { // "type": "some-prefix/someMethod", 34 | return 'some-return-value'; // "payload": "some-return-value" 35 | } // } 36 | 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/decorators/redux-action.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) / [decorators](./index.md) 2 | 3 | # @ReduxAction 4 | 5 | Marks a class method as redux action dispatcher. Every time the decorated method is called, it will dispatch a new 6 | [redux action](https://redux.js.org/docs/basics/Actions.html). The return value of the decorated method is used as payload. 7 | 8 | ```ts 9 | @ReduxAction({ 10 | type?: string; 11 | contextClass?: {}, 12 | onDispatchSuccess?: Function; 13 | }) 14 | ``` 15 | 16 | ## Configuration Options 17 | 18 | ### ```type``` 19 | 20 | Overwrites the type of the redux action. 21 | 22 | | | | 23 | | ------------- | -------------------------------- | 24 | | default value | The name of the decorated method | 25 | | type | string | 26 | | mandatory | no | 27 | 28 | ```ts 29 | class SomeClass { // This will dispatch the following action: 30 | // 31 | @ReduxAction({type: 'some-action-type'}) // { 32 | public someMethod(): string { // "type": "some-action-type", 33 | return 'some-return-value'; // "payload": "some-return-value" 34 | } // } 35 | 36 | } 37 | ``` 38 | 39 | ### ```contextClass``` 40 | 41 | Defines the context class which is used to resolve the [@ReduxActionContext](./redux-action-context.md). 42 | 43 | | | | 44 | | ------------- | ---------- | 45 | | default value | this class | 46 | | type | class | 47 | | mandatory | no | 48 | 49 | ```ts 50 | @ReduxActionContext({prefix: 'some-prefix/') 51 | class SomeContextClass { 52 | 53 | } 54 | 55 | class SomeClass { // This will dispatch the following action: 56 | // 57 | @ReduxAction({contextClass: SomeContextClass}) // { 58 | public someMethod(): string { // "type": "some-prefix/someMethod", 59 | return 'some-return-value'; // "payload": "some-return-value" 60 | } // } 61 | 62 | } 63 | ``` 64 | 65 | The example above will dispatch the following [redux action](https://redux.js.org/docs/basics/Actions.html): 66 | 67 | ```json 68 | { 69 | "type": "some-prefix/someMethod", 70 | "payload": "some-return-value" 71 | } 72 | ``` 73 | 74 | ### ```onDispatchSuccess``` 75 | 76 | Defines a callback function which is called as soon as the action is dispatched. The `this` context of the decorated method 77 | is bound to the callback function. That means that you can use `this` within the callback function. 78 | 79 | | | | 80 | | ------------- | ---------- | 81 | | default value | this class | 82 | | type | class | 83 | | mandatory | no | 84 | 85 | ```ts 86 | class SomeClass { 87 | 88 | protected someProperty: string; 89 | 90 | @ReduxAction({ 91 | onDispatchSuccess: SomeClass.prototype.someMethodDispatchSuccess 92 | }) 93 | public someMethod(): string { 94 | this.someProperty = '123'; 95 | return 'some-return-value'; 96 | } 97 | 98 | public someMethodDispatchSuccess() { 99 | console.log(this.someProperty); // will log 123 100 | } 101 | 102 | } 103 | ``` 104 | 105 | ## Special return values 106 | 107 | ### Promise 108 | 109 | If the method returns a `Promise`, the [redux action](https://redux.js.org/docs/basics/Actions.html) will not be fired 110 | until the `Promise` is resolved *successfully*. 111 | 112 | ```ts 113 | class SomeClass { // This will dispatch the following action: 114 | // 115 | @ReduxAction() // { 116 | public someMethod(): Promise { // "type": "someMethod", 117 | return Promise.resolve('some-return-value'); // "payload": "some-return-value" 118 | } // } 119 | 120 | } 121 | ``` 122 | 123 | ### Observable 124 | 125 | If the method returns a `Observable`, the [redux action](https://redux.js.org/docs/basics/Actions.html) will not be fired 126 | until the `Observable` is *completed*. The last value in the event stream is used as the payload. 127 | 128 | ```ts 129 | class SomeClass { // This will dispatch the following action: 130 | // 131 | @ReduxAction() // { 132 | public someMethod(): Observable { // "type": "someMethod", 133 | return Observable.from([ // "payload": "some-return-value" 134 | 'not-used-as-payload', // } 135 | 'some-return-value' 136 | ]); 137 | } 138 | 139 | } 140 | ``` 141 | -------------------------------------------------------------------------------- /docs/decorators/redux-reducer.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) / [decorators](./index.md) 2 | 3 | # @ReduxReducer 4 | 5 | Marks a class method as redux reducer and wires the reducer method against one or many redux actions. 6 | 7 | ```ts 8 | @ReduxAction(string|string[]|Function|Function[]) 9 | ``` 10 | 11 | ## Reference variants 12 | 13 | ### By prototype 14 | 15 | The recommended and simplest way is to wire the reducer directly via the prototype with the action method. This gives you the 16 | advantage that TypeScript can validate the return value of the ReduxAction against the expected payload of your Reducer 17 | function. This is also the only way to enable IDE refactoring. 18 | 19 | ![TypeScript support](../ts-support.gif "TypeScript support") 20 | 21 | ```ts 22 | class SomeReducerClass { 23 | 24 | @ReduxReducer(SomeActionClass.prototype.addItem) // addItem returns string 25 | public addItem(state: SomeState, action: ReduxActionWithPayload): SomeState { 26 | return { 27 | ...state, 28 | items: state.items.concat(action.payload), 29 | }; 30 | } 31 | 32 | } 33 | ``` 34 | 35 | The reducer can also listen to several actions at the same time: 36 | 37 | ```ts 38 | class SomeReducerClass { 39 | 40 | @ReduxReducer([ 41 | SomeActionClass.prototype.addItem, // addItem returns string 42 | SomeActionClass.prototype.addDefaultItem, // addDefaultItem returns string 43 | ]) 44 | public addItem(state: SomeState, action: ReduxActionWithPayload): SomeState { 45 | return { 46 | ...state, 47 | items: state.items.concat(action.payload), 48 | }; 49 | } 50 | 51 | } 52 | ``` 53 | 54 | Or you can decorate the reducer method multiple times: 55 | 56 | ```ts 57 | class SomeReducerClass { 58 | 59 | @ReduxReducer(SomeActionClass.prototype.addItem) // addItem returns string 60 | @ReduxReducer(SomeActionClass.prototype.addDefaultItem) // addDefaultItem returns string 61 | public addItem(state: SomeState, action: ReduxActionWithPayload): SomeState { 62 | return { 63 | ...state, 64 | items: state.items.concat(action.payload), 65 | }; 66 | } 67 | 68 | } 69 | ``` 70 | 71 | The array notation and the TypeScript type check currently only work well for actions which return the same type. 72 | So if you have something like this, TypeScript will throw a type error: 73 | 74 | ```ts 75 | class SomeReducerClass { 76 | 77 | @ReduxReducer([ 78 | SomeActionClass.prototype.addItem, // addItem returns string 79 | SomeActionClass.prototype.addItems // addItems returns string[] 80 | ]) 81 | public addItem(state: SomeState, action: ReduxActionWithPayload): SomeState { 82 | return { 83 | ...state, 84 | items: state.items.concat(action.payload), 85 | }; 86 | } 87 | 88 | } 89 | ``` 90 | 91 | But you can fall back to the following solution: 92 | 93 | ```ts 94 | class SomeReducerClass { 95 | 96 | @ReduxReducer(SomeActionClass.prototype.addItem) // addItem returns string 97 | @ReduxReducer(SomeActionClass.prototype.addItems) // addItems returns string[] 98 | public addItem(state: SomeState, action: ReduxActionWithPayload): SomeState { 99 | return { 100 | ...state, 101 | items: state.items.concat(action.payload), 102 | }; 103 | } 104 | 105 | } 106 | ``` 107 | 108 | ### By string 109 | 110 | It is also possible to refer directly to an action by string. However, this option should only be used if there 111 | is no other option. You can also specify this as an array or decorate the method multiple times. 112 | 113 | ```ts 114 | class SomeReducerClass { 115 | 116 | @ReduxReducer('some-action-type') 117 | public addItem(state: SomeState, action: ReduxActionWithPayload): SomeState { 118 | return { 119 | ...state, 120 | items: state.items.concat(action.payload), 121 | }; 122 | } 123 | 124 | } 125 | ``` 126 | 127 | ## Activation 128 | 129 | Don't forget to activate your reducer! You've to add the reducer class to the reducers array like this: 130 | 131 | ```ts 132 | @NgModule({ 133 | imports: [ 134 | ReduxModule.forRoot({ 135 | state: { 136 | provider: SomeStateProvider, 137 | reducers: [ SomeReducerClass ], 138 | } 139 | }), 140 | ], 141 | providers: [SomeStateProvider] 142 | }) 143 | ``` 144 | -------------------------------------------------------------------------------- /docs/decorators/redux-state.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) / [decorators](./index.md) 2 | 3 | # @ReduxState 4 | 5 | Marks a [ReduxStateProvider](../api/redux-state-provider.md) class as redux state. 6 | This is required to provide a unique name within the global application state. 7 | 8 | ```ts 9 | @ReduxState({ 10 | name: string; 11 | }) 12 | ``` 13 | 14 | ## Configuration Options 15 | 16 | ### ```name``` 17 | 18 | Defines the unique state name, that will be used as key in the global application state. 19 | 20 | | | | 21 | | ------------- | -------------------------------- | 22 | | type | string | 23 | | mandatory | yes | 24 | 25 | ```ts 26 | class SomeClass { 27 | 28 | @ReduxAction({name: 'my-unique-name'}) 29 | public someMethod(): string { 30 | return 'some-return-value'; 31 | } 32 | 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/how-to/create-an-actions-provider.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) / [how to](./index.md) 2 | 3 | # Create an actions provider 4 | 5 | Of course you can also create a provider to fire your Redux actions. Here's an example using 6 | [@ReduxActionContext](../decorators/redux-action-context.md) and [@ReduxAction](../decorators/redux-action.md): 7 | 8 | ## Example 9 | 10 | ```ts 11 | import { Injectable } from '@angular/core'; 12 | import { ReduxAction, ReduxActionContext } from '@harmowatch/ngx-redux-core'; 13 | 14 | @Injectable() 15 | @ReduxActionContext({prefix: 'TodoActions://'}) 16 | export class TodoActions { 17 | 18 | @ReduxAction() 19 | add(label: string): TodoListItem { 20 | return label; // your return value is the payload 21 | } 22 | 23 | } 24 | ``` 25 | 26 | Now `@harmowatch/ngx-redux-core` will dispatch the following action, every time the `add` method was called: 27 | 28 | ```json 29 | { 30 | "type": "TodoActions://add", 31 | "payload": "SampleTodo" 32 | } 33 | ``` 34 | 35 | -------------------------------------------------------------------------------- /docs/how-to/index.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) 2 | 3 | # How to 4 | 5 | - [Create an actions provider](./create-an-actions-provider.md) 6 | - [Provide your own store](./provide-your-own-store.md) 7 | - [Use lazy loading](./use-lazy-loading.md) 8 | -------------------------------------------------------------------------------- /docs/how-to/provide-your-own-store.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) / [how to](./index.md) 2 | 3 | # Provide your own store 4 | 5 | Sometimes you want to have your own redux store to register a custom middleware or enhancer. To make this possible, 6 | you can specify your own `storeFactory`, which will be used instead of the one that comes with @harmowatch/ngx-redux-core. 7 | The method `ReduxModule.forRoot` offers a configuration option `storeFactory` for this purpose. 8 | 9 | ## Example 10 | 11 | ```ts 12 | import { isDevMode, NgModule } from '@angular/core'; 13 | import { BrowserModule } from '@angular/platform-browser'; 14 | 15 | import { AppComponent } from './app.component'; 16 | import { createStore, Store } from 'redux'; 17 | import { ReduxModule, ReduxReducerProvider, ReduxRootState } from '@harmowatch/ngx-redux-core'; 18 | import { RouterModule } from '@angular/router'; 19 | 20 | // AOT compiler requires an exported function for factories 21 | export function storeFactory(reduxReducerProvider: ReduxReducerProvider): Store { 22 | return createStore( 23 | reduxReducerProvider.rootReducer, // the rootReducer to enable the @ReduxReducer decorated methods 24 | {}, // the initial state 25 | ReduxModule.defaultEnhancerFactory(isDevMode()), // in devMode this will enable the "Redux DevTools Extension" 26 | ); 27 | } 28 | 29 | @NgModule({ 30 | declarations: [ 31 | AppComponent 32 | ], 33 | imports: [ 34 | BrowserModule, 35 | ReduxModule.forRoot({ 36 | storeFactory, 37 | }), 38 | ], 39 | exports: [ 40 | AppComponent, 41 | ], 42 | bootstrap: [ AppComponent ] 43 | }) 44 | export class AppModule { 45 | } 46 | ``` 47 | 48 | For more information, how to create a store, please see the [redux docs](https://redux.js.org/api-reference/createstore). 49 | 50 | ---- 51 | 52 | To use this *new feature* you've to install the **next** version (>= 0.2.3-0). 53 | 54 | ``` 55 | $ npm install @harmowatch/ngx-redux-core@next 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/how-to/use-lazy-loading.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) / [how to](./index.md) 2 | 3 | # Use lazy loading 4 | 5 | As described in the [Angular - Router & Navigation](https://angular.io/guide/router#asynchronous-routing) docs, 6 | it's possible to load a [Angular Module](https://angular.io/guide/ngmodules) asynchronously. To use this together 7 | with `@harmowatch/ngx-redux-core` you just have to use the static `ReduxModule.forChild()` method in your lazy loaded module. 8 | 9 | Make sure you've initialized the `ReduxModule.forRoot()` module in your app module as well. 10 | 11 | ## Example 12 | 13 | *app.module.ts* (The root NgModule) 14 | 15 | ```ts 16 | import { NgModule } from '@angular/core'; 17 | import { BrowserModule } from '@angular/platform-browser'; 18 | import { RouterModule } from '@angular/router'; 19 | import { ReduxModule } from '@harmowatch/ngx-redux-core'; 20 | 21 | import { AppComponent } from './app.component'; 22 | 23 | @NgModule({ 24 | declarations: [ 25 | AppComponent 26 | ], 27 | imports: [ 28 | BrowserModule, 29 | ReduxModule.forRoot(), // import the ReduxModule.forRoot() 30 | RouterModule.forRoot([ 31 | {path: 'todo', loadChildren: './todo/todo.module#TodoModule'}, // lazy load you child module 32 | {path: '', redirectTo: 'todo', pathMatch: 'prefix'}, 33 | ], {useHash: true}) 34 | ], 35 | exports: [ 36 | AppComponent, 37 | ], 38 | bootstrap: [ AppComponent ] 39 | }) 40 | export class AppModule { 41 | } 42 | ``` 43 | 44 | *todo.module.ts* (The lazy loaded NgModule) 45 | 46 | ```ts 47 | import { CommonModule } from '@angular/common'; 48 | import { NgModule } from '@angular/core'; 49 | import { FormsModule } from '@angular/forms'; 50 | import { RouterModule } from '@angular/router'; 51 | import { ReduxModule } from '@harmowatch/ngx-redux-core'; 52 | 53 | import { TodoListComponent } from './list/todo-list.component'; 54 | import { TodoListReducer } from './list/todo-list.reducer'; 55 | import { TodoModuleStateProvider } from './todo.module.state.provider'; 56 | 57 | @NgModule({ 58 | imports: [ 59 | CommonModule, 60 | FormsModule, 61 | ReduxModule.forChild({ // use forChild instead of forRoot in lazy loaded modules 62 | state: { 63 | provider: TodoModuleStateProvider, 64 | reducers: [ TodoListReducer ], 65 | } 66 | }), 67 | RouterModule.forChild([ 68 | {path: '', component: TodoListComponent}, 69 | ]), 70 | ], 71 | declarations: [TodoListComponent], 72 | providers: [TodoModuleStateProvider] 73 | }) 74 | export class TodoModule { } 75 | ``` 76 | 77 | For more information, please see the [example app](https://github.com/HarmoWatch/ngx-redux-core/tree/master/src/example-app) 78 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../README.md) 2 | 3 | # Docs 4 | 5 | - [Articles](./articles/index.md) 6 | - [The Select Pattern](./articles/select-pattern.md) 7 | - [Testing Guide](./articles/testing-guide.md) 8 | - [Decorators](./decorators/index.md) 9 | - [@ReduxAction](./decorators/redux-action.md) 10 | - [@ReduxActionContext](./decorators/redux-action-context.md) 11 | - [@ReduxReducer](./decorators/redux-reducer.md) 12 | - [@ReduxSelect](./articles/select-pattern.md#the-reduxselect-decorator) 13 | - [@ReduxState](./decorators/redux-state.md) 14 | - [Pipes](./pipes/index.md) 15 | - [reduxSelect](./pipes/redux-select.md) 16 | - [API](./api/index.md) 17 | - [ReduxStateProvider](./api/redux-state-provider.md) 18 | - [How to](./how-to/index.md) 19 | - [Create an actions provider](./how-to/create-an-actions-provider.md) 20 | - [Provide your own store](./how-to/provide-your-own-store.md) 21 | - [Use lazy loading](./how-to/use-lazy-loading.md) 22 | -------------------------------------------------------------------------------- /docs/pipes/index.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) 2 | 3 | # Pipes 4 | 5 | - [reduxSelect](./redux-select.md) 6 | -------------------------------------------------------------------------------- /docs/pipes/redux-select.md: -------------------------------------------------------------------------------- 1 | ###### [@harmowatch/ngx-redux-core](../../README.md) / [docs](../index.md) / [pipes](./index.md) 2 | 3 | # reduxSelect 4 | 5 | Selects a value from the state directly out of your view. For the selection it will use your 6 | [ReduxStateProvider](../api/redux-state-provider.md#select)'s select method. 7 | 8 | ## Using a relative selector, second argument needed (recommended) 9 | 10 | ```angular2html 11 |
    12 |
  • 13 | {{todo}} 14 |
  • 15 |
16 | ``` 17 | 18 | ## Select the same value using a absolute selector, no second argument needed (avoid) 19 | 20 | ```angular2html 21 |
    22 |
  • 23 | {{todo}} 24 |
  • 25 |
26 | ``` 27 | -------------------------------------------------------------------------------- /docs/reducer-switch-case.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarmoWatch/ngx-redux-core/55f3473bb0a209fa4e42b93a54f65bce9b505bf3/docs/reducer-switch-case.gif -------------------------------------------------------------------------------- /docs/ts-support.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarmoWatch/ngx-redux-core/55f3473bb0a209fa4e42b93a54f65bce9b505bf3/docs/ts-support.gif -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('ngx-redux App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | module.exports = function (config) { 4 | config.set({ 5 | basePath: '', 6 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 7 | plugins: [ 8 | require('karma-jasmine'), 9 | require('karma-chrome-launcher'), 10 | require('karma-jasmine-html-reporter'), 11 | require('karma-coverage-istanbul-reporter'), 12 | require('@angular-devkit/build-angular/plugins/karma'), 13 | ], 14 | client:{ 15 | clearContext: false // leave Jasmine Spec Runner output visible in browser 16 | }, 17 | coverageIstanbulReporter: { 18 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 19 | fixWebpackSourcePaths: true 20 | }, 21 | 22 | customLaunchers: { 23 | Chrome_travis_ci: { 24 | base: 'Chrome', 25 | flags: ['--no-sandbox'] 26 | } 27 | }, 28 | reporters: ['progress', 'kjhtml'], 29 | port: 9876, 30 | colors: true, 31 | logLevel: config.LOG_INFO, 32 | autoWatch: true, 33 | browsers: ['Chrome'], 34 | singleRun: false 35 | }); 36 | 37 | if (process.env.TRAVIS) { 38 | config.browsers = ['Chrome_travis_ci']; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-packagr/package.schema.json", 3 | "name": "@harmowatch/ngx-redux-core", 4 | "version": "0.2.6", 5 | "license": "MIT", 6 | "scripts": { 7 | "lint": "ng lint", 8 | "test": "ng test --watch=false --code-coverage --source-map && npm run coveralls", 9 | "start": "ng serve", 10 | "coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls", 11 | "build:package": "ng-packagr -p package.json && npm run build:karma-plugin", 12 | "build:karma-plugin": "webpack --config src/plugins/karma/webpack.config.js && echo \"import './plugins/karma/index';\" >> ./dist/index.d.ts", 13 | "publish:package": "npm run build:package && npm publish ./dist/ --access=public --registry https://registry.npmjs.org/", 14 | "publish:package:next": "npm run build:package && npm publish ./dist/ --access=public --registry https://registry.npmjs.org/ --tag next" 15 | }, 16 | "ngPackage": { 17 | "whitelistedNonPeerDependencies": [ 18 | "@harmowatch/redux-decorators" 19 | ], 20 | "lib": { 21 | "entryFile": "src/harmowatch/ngx-redux-core/index.ts", 22 | "languageLevel": [ 23 | "dom", 24 | "es2017" 25 | ] 26 | } 27 | }, 28 | "private": false, 29 | "keywords": [ 30 | "angular", 31 | "angular 2", 32 | "angular 4", 33 | "angular2", 34 | "angular4", 35 | "redux", 36 | "ng-redux", 37 | "ng", 38 | "annotation", 39 | "decorator" 40 | ], 41 | "author": "Kay Schecker", 42 | "bugs": { 43 | "url": "https://github.com/HarmoWatch/ngx-redux-core/issues" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/HarmoWatch/ngx-redux-core.git" 48 | }, 49 | "homepage": "https://github.com/HarmoWatch/ngx-redux-core#readme", 50 | "peerDependencies": { 51 | "@angular/core": "6.x || 7.x", 52 | "redux": ">=3.0.0", 53 | "rxjs": "6.x" 54 | }, 55 | "devDependencies": { 56 | "@angular-devkit/build-angular": "0.13.8", 57 | "@angular/cli": "7.3.8", 58 | "@angular/common": "7.2.13", 59 | "@angular/compiler": "7.2.13", 60 | "@angular/compiler-cli": "7.2.13", 61 | "@angular/core": "7.2.13", 62 | "@angular/forms": "7.2.13", 63 | "@angular/http": "7.2.13", 64 | "@angular/language-service": "7.2.13", 65 | "@angular/platform-browser": "7.2.13", 66 | "@angular/platform-browser-dynamic": "7.2.13", 67 | "@angular/router": "7.2.13", 68 | "@types/jasmine": "2.8.16", 69 | "@types/jasminewd2": "2.0.6", 70 | "@types/node": "10.14.5", 71 | "@types/redux-logger": "3.0.7", 72 | "@types/uuid": "3.4.4", 73 | "awesome-typescript-loader": "5.2.1", 74 | "codelyzer": "4.5.0", 75 | "copy-webpack-plugin": "4.6.0", 76 | "core-js": "2.6.5", 77 | "coveralls": "3.0.3", 78 | "jasmine-core": "3.4.0", 79 | "jasmine-spec-reporter": "4.2.1", 80 | "karma": "3.1.4", 81 | "karma-chrome-launcher": "2.2.0", 82 | "karma-cli": "1.0.1", 83 | "karma-coverage-istanbul-reporter": "2.0.5", 84 | "karma-jasmine": "1.1.2", 85 | "karma-jasmine-html-reporter": "1.4.0", 86 | "ng-packagr": "4.7.1", 87 | "protractor": "5.4.2", 88 | "redux": "4.0.1", 89 | "redux-logger": "3.0.6", 90 | "rxjs": "6.5.1", 91 | "ts-node": "7.0.1", 92 | "tsickle": "0.34.3", 93 | "tslint": "5.16.0", 94 | "typescript": "3.2.4", 95 | "uuid": "3.3.2", 96 | "webpack-cli": "3.3.1", 97 | "zone.js": "0.9.0" 98 | }, 99 | "dependencies": { 100 | "@harmowatch/redux-decorators": "0.1.1" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "rebaseStalePrs": true, 6 | "automerge": true, 7 | "automergeType": "branch-push", 8 | "major": { 9 | "automerge": false 10 | }, 11 | "devDependencies": { 12 | "automerge": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/example-app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarmoWatch/ngx-redux-core/55f3473bb0a209fa4e42b93a54f65bce9b505bf3/src/example-app/app.component.css -------------------------------------------------------------------------------- /src/example-app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/example-app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: [ './app.component.css' ] 7 | }) 8 | export class AppComponent { 9 | title = 'app'; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/example-app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { ReduxModule } from '../harmowatch/ngx-redux-core/redux.module'; 7 | import { ReduxActionWithPayload } from '../harmowatch/ngx-redux-core'; 8 | 9 | const logger = store => next => (action: ReduxActionWithPayload) => { 10 | 11 | console.log('dispatching', action); 12 | const result = next(action); 13 | console.log('next state', store.getState()); 14 | 15 | return result; 16 | 17 | }; 18 | ​ 19 | @NgModule({ 20 | declarations: [ 21 | AppComponent 22 | ], 23 | imports: [ 24 | BrowserModule, 25 | ReduxModule.forRoot({ 26 | middlewareFunctions: [logger] 27 | }), 28 | RouterModule.forRoot([ 29 | {path: 'todo', loadChildren: './todo/todo.module#TodoModule'}, 30 | {path: '', redirectTo: 'todo', pathMatch: 'prefix'}, 31 | ], {useHash: true}) 32 | ], 33 | exports: [ 34 | AppComponent, 35 | ], 36 | bootstrap: [ AppComponent ] 37 | }) 38 | export class AppModule { 39 | } 40 | -------------------------------------------------------------------------------- /src/example-app/todo/list/todo-list-item.ts: -------------------------------------------------------------------------------- 1 | export interface TodoListItem { 2 | uuid: string; 3 | label: string; 4 | creationDate: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/example-app/todo/list/todo-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Redux State 6 |
7 |
8 |
9 |
{{ state | async | json }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
    17 |
  • 18 | 19 |
  • 20 |
  • 21 | {{todo.label}} 22 | 25 |
  • 26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /src/example-app/todo/list/todo-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | // 3 | // import { TodoListComponent } from './todo-list.component'; 4 | // import { ReduxModule } from '../../../harmowatch/ngx-redux-core/redux.module'; 5 | // import { ReduxTestingStore } from '../../../harmowatch/ngx-redux-core/testing/store'; 6 | // import { TodoModuleStateProvider } from '../todo.module.state.provider'; 7 | // 8 | // describe('TodoListComponent', () => { 9 | // 10 | // let fixture: ComponentFixture; 11 | // 12 | // beforeEach(async(() => { 13 | // TestBed.configureTestingModule({ 14 | // imports: [ 15 | // ReduxModule.forRoot({ 16 | // storeFactory: ReduxTestingStore.factory, 17 | // state: { 18 | // provider: TodoModuleStateProvider, 19 | // } 20 | // }) 21 | // ], 22 | // declarations: [ 23 | // TodoListComponent, 24 | // ], 25 | // providers: [ 26 | // TodoModuleStateProvider 27 | // ], 28 | // }); 29 | // 30 | // fixture = TestBed.createComponent(TodoListComponent); 31 | // })); 32 | // 33 | // 34 | // describe('add()', () => { 35 | // 36 | // it('dispatches a action', () => { 37 | // expect(fixture.componentInstance.add).toDispatchSomeAction(); 38 | // 39 | // // or you can test for a specific action type 40 | // expect(fixture.componentInstance.add).toDispatchAction('TodoListComponent://add'); 41 | // }); 42 | // 43 | // }); 44 | // 45 | // }); 46 | -------------------------------------------------------------------------------- /src/example-app/todo/list/todo-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { v4 } from 'uuid'; 4 | import { TodoModuleStateProvider } from '../todo.module.state.provider'; 5 | import { TodoListItem } from './todo-list-item'; 6 | import { ReduxAction, ReduxActionContext } from '../../../harmowatch/ngx-redux-core/decorators/index'; 7 | import { ReduxSelect } from '../../../harmowatch/ngx-redux-core/decorators/redux-select.decorator'; 8 | import { ReduxSelector } from '../../../harmowatch/ngx-redux-core/redux-selector'; 9 | 10 | @Component({ 11 | templateUrl: './todo-list.component.html', 12 | }) 13 | @ReduxActionContext({prefix: 'TodoListComponent://'}) 14 | export class TodoListComponent { 15 | 16 | @ReduxSelect('', TodoModuleStateProvider) 17 | public state: ReduxSelector<{}>; 18 | 19 | @ReduxAction() 20 | add(label: string): TodoListItem { 21 | return { 22 | uuid: v4(), 23 | label, 24 | creationDate: new Date().toISOString(), 25 | }; 26 | } 27 | 28 | @ReduxAction() 29 | remove(todo: TodoListItem) { 30 | return todo; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/example-app/todo/list/todo-list.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | // import { async } from '@angular/core/testing'; 2 | // 3 | // import { TodoListReducer } from './todo-list.reducer'; 4 | // import { TodoListItem } from './todo-list-item'; 5 | // import { TodoModuleStateProvider } from '../todo.module.state.provider'; 6 | // import { getActionType } from '../../../harmowatch/ngx-redux-core/index'; 7 | // import { ReduxActionWithPayload } from '../../../harmowatch/ngx-redux-core/interfaces/redux-action-with-payload.interface'; 8 | // import { TodoListComponent } from './todo-list.component'; 9 | // 10 | // describe('TodoListReducer', () => { 11 | // 12 | // let fixture: TodoListReducer; 13 | // 14 | // beforeEach(async(() => { 15 | // // TestBed.configureTestingModule({ 16 | // // providers: [ 17 | // // TodoListReducer, 18 | // // ] 19 | // // }); 20 | // // 21 | // // fixture = TestBed.get(TodoListReducer); 22 | // 23 | // fixture = new TodoListReducer(); 24 | // })); 25 | // 26 | // describe('addTodo()', () => { 27 | // 28 | // let todo: TodoListItem; 29 | // let action: ReduxActionWithPayload; 30 | // 31 | // beforeEach(() => { 32 | // todo = { 33 | // uuid: '', 34 | // label: 'some todo', 35 | // creationDate: '', 36 | // }; 37 | // 38 | // action = { 39 | // type: getActionType(TodoListComponent.prototype.add), 40 | // payload: todo, 41 | // }; 42 | // 43 | // }); 44 | // 45 | // it('listens to the correct actions', () => { 46 | // // expect(fixture.addTodo).toListenForActions([ 47 | // // TodoListComponent.prototype.add, 48 | // // TodoListComponent.prototype.remove, 49 | // // ]); 50 | // }); 51 | // 52 | // it('does not touch the given state', () => { 53 | // expect(fixture.addTodo).doesNotTouchTheGivenState({ 54 | // state: TodoModuleStateProvider.DEFAULT_STATE, 55 | // action, 56 | // }); 57 | // }); 58 | // 59 | // it('returns the new state', () => { 60 | // expect(fixture.addTodo(TodoModuleStateProvider.DEFAULT_STATE, action)).toEqual({ 61 | // items: [ todo ] 62 | // }); 63 | // }); 64 | // 65 | // }); 66 | // 67 | // describe('removeTodo()', () => { 68 | // 69 | // it('listens to the correct action', () => { 70 | // expect(fixture.removeTodo).toListenForAction(TodoListComponent.prototype.remove); 71 | // }); 72 | // 73 | // }); 74 | // 75 | // }); 76 | -------------------------------------------------------------------------------- /src/example-app/todo/list/todo-list.reducer.ts: -------------------------------------------------------------------------------- 1 | import { TodoListItem } from './todo-list-item'; 2 | import { TodoListComponent } from './todo-list.component'; 3 | import { TodoState } from '../todo.module.state'; 4 | import { ReduxReducer } from '../../../harmowatch/ngx-redux-core/decorators/index'; 5 | import { ReduxActionWithPayload } from '../../../harmowatch/ngx-redux-core/interfaces/redux-action-with-payload.interface'; 6 | 7 | export class TodoListReducer { 8 | 9 | @ReduxReducer(TodoListComponent.prototype.add) 10 | addTodo(state: TodoState, action: ReduxActionWithPayload): TodoState { 11 | return { 12 | ...state, 13 | items: state.items.concat(action.payload), 14 | }; 15 | } 16 | 17 | @ReduxReducer(TodoListComponent.prototype.remove) 18 | removeTodo(state: TodoState, action: ReduxActionWithPayload): TodoState { 19 | return { 20 | ...state, 21 | items: state.items.filter(todo => todo.uuid !== action.payload.uuid) 22 | }; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/example-app/todo/todo.module.state.provider.ts: -------------------------------------------------------------------------------- 1 | import { TodoState } from './todo.module.state'; 2 | import { ReduxState } from '../../harmowatch/ngx-redux-core/decorators/index'; 3 | import { ReduxStateProvider } from '../../harmowatch/ngx-redux-core/providers/redux-state.provider'; 4 | import { Injectable } from '@angular/core'; 5 | import { Observable } from 'rxjs'; 6 | 7 | @Injectable() 8 | @ReduxState({name: 'todo'}) 9 | export class TodoModuleStateProvider extends ReduxStateProvider { 10 | 11 | public static readonly DEFAULT_STATE: TodoState = { 12 | items: [] 13 | }; 14 | 15 | getInitialState(): Promise { 16 | return Promise.resolve(TodoModuleStateProvider.DEFAULT_STATE); 17 | } 18 | 19 | 20 | select(selector: string = ''): Observable { 21 | 22 | if (selector === 'todos') { 23 | selector = 'items'; 24 | } 25 | 26 | return super.select(selector); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/example-app/todo/todo.module.state.ts: -------------------------------------------------------------------------------- 1 | import { TodoListItem } from './list/todo-list-item'; 2 | 3 | export interface TodoState { 4 | items: TodoListItem[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/example-app/todo/todo.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | 6 | import { TodoListComponent } from './list/todo-list.component'; 7 | import { TodoListReducer } from './list/todo-list.reducer'; 8 | import { TodoModuleStateProvider } from './todo.module.state.provider'; 9 | import { ReduxModule } from '../../harmowatch/ngx-redux-core/redux.module'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | FormsModule, 15 | ReduxModule.forChild({ 16 | state: { 17 | provider: TodoModuleStateProvider, 18 | reducers: [ TodoListReducer ], 19 | } 20 | }), 21 | RouterModule.forChild([ 22 | {path: '', component: TodoListComponent}, 23 | ]), 24 | ], 25 | declarations: [TodoListComponent], 26 | providers: [TodoModuleStateProvider] 27 | }) 28 | export class TodoModule { } 29 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarmoWatch/ngx-redux-core/55f3473bb0a209fa4e42b93a54f65bce9b505bf3/src/favicon.ico -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ReduxActionContextDecoratorForClass as ReduxActionContext, 3 | ReduxActionDecoratorForMethod as ReduxAction, 4 | ReduxReducerDecoratorForMethod as ReduxReducer, 5 | ReduxStateDecoratorForClass as ReduxState, 6 | } from '@harmowatch/redux-decorators'; 7 | 8 | export * from './redux-select.decorator'; 9 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/decorators/redux-select.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { ReduxSelect } from './redux-select.decorator'; 6 | import { TestingStateProvider } from '../testing/state'; 7 | import { ReduxTestingStore } from '../testing/store'; 8 | import { ReduxModule } from '../redux.module'; 9 | import { ReduxStore } from '../tokens/redux-store.token'; 10 | import { selectorSuiteFactory } from '../testing/selector/suite.config'; 11 | 12 | @Component({ 13 | template: '', 14 | }) 15 | class TestComponent { 16 | 17 | @ReduxSelect('', TestingStateProvider) 18 | empty: Observable<{}>; 19 | 20 | @ReduxSelect('todo', TestingStateProvider) 21 | todo: Observable<{}>; 22 | 23 | @ReduxSelect('todo/items', TestingStateProvider) 24 | todoItems: Observable<{}>; 25 | 26 | @ReduxSelect('todo/items/', TestingStateProvider) 27 | todoItemsTrailingSlash: Observable<{}>; 28 | 29 | @ReduxSelect('/', TestingStateProvider) 30 | root: Observable<{}>; 31 | 32 | @ReduxSelect('unknown', TestingStateProvider) 33 | unknown: Observable<{}>; 34 | 35 | } 36 | 37 | describe('select/decorator', () => { 38 | 39 | let fixture: ComponentFixture; 40 | let store: ReduxTestingStore; 41 | let stateProvider: TestingStateProvider; 42 | 43 | beforeEach(async(() => { 44 | 45 | TestBed.configureTestingModule({ 46 | declarations: [ TestComponent ], 47 | imports: [ 48 | ReduxModule.forRoot({ 49 | storeFactory: ReduxTestingStore.factory, 50 | state: { 51 | provider: TestingStateProvider, 52 | reducers: [], 53 | }, 54 | }), 55 | ], 56 | providers: [ 57 | TestingStateProvider, 58 | ] 59 | }); 60 | 61 | stateProvider = TestBed.get(TestingStateProvider); 62 | TestBed.compileComponents(); 63 | 64 | store = TestBed.get(ReduxStore); 65 | store.setState(TestingStateProvider, TestingStateProvider.INITIAL_STATE); 66 | fixture = TestBed.createComponent(TestComponent); 67 | spyOn(stateProvider, 'select').and.callThrough(); 68 | })); 69 | 70 | beforeEach(async(() => { 71 | fixture.detectChanges(); 72 | })); 73 | 74 | selectorSuiteFactory().forEach((cfg) => { 75 | 76 | describe(`property "${cfg.given.name}"`, () => { 77 | 78 | it('injects an observable', () => { 79 | expect(fixture.componentInstance[ cfg.given.name ] instanceof Observable).toBeTruthy(); 80 | }); 81 | 82 | it('directly returns the initial state', ((done) => { 83 | fixture.componentInstance[ cfg.given.name ].subscribe((value) => { 84 | expect(value).toEqual(cfg.result.initialState); 85 | done(); 86 | }); 87 | 88 | })); 89 | 90 | it('uses the select method provided by the state provider', (done) => { 91 | fixture.componentInstance[ cfg.given.name ].subscribe((value) => { 92 | expect(stateProvider.select).toHaveBeenCalledTimes(1); 93 | done(); 94 | }); 95 | }); 96 | 97 | }); 98 | 99 | }); 100 | 101 | 102 | }); 103 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/decorators/redux-select.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | import { ReduxStateDecorator } from '@harmowatch/redux-decorators'; 3 | 4 | import { ReduxStateProvider } from '../providers/redux-state.provider'; 5 | 6 | export function ReduxSelect(expression: string, 7 | context?: Type): PropertyDecorator { 8 | 9 | return (target: {}, propertyKey: string) => { 10 | const stateName = ReduxStateDecorator.get(context).name; 11 | 12 | Object.defineProperty(target, propertyKey, { 13 | get: () => ReduxStateProvider.instancesByName[ stateName ].select(expression) 14 | }); 15 | 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces/redux-action-with-payload.interface'; 2 | 3 | export * from './decorators'; 4 | export * from './interfaces/redux-child-module-config.interface'; 5 | export * from './interfaces/redux-root-module-config.interface'; 6 | export * from './interfaces/redux-root-state.interface'; 7 | export * from './providers/redux-reducer.provider'; 8 | export * from './decorators/redux-select.decorator'; 9 | export * from './pipes/redux-select.pipe'; 10 | export * from './redux-selector'; 11 | export * from './tokens/redux-state-definition.token'; 12 | export * from './interfaces/redux-state-definition.interface'; 13 | export * from './providers/redux-state.provider'; 14 | export * from './tokens/redux-store.token'; 15 | export * from './testing/store'; 16 | export * from './testing/state'; 17 | export * from './providers/redux-registry'; 18 | 19 | export { ReduxModule } from './redux.module'; 20 | import { ReduxActionDispatcher } from '@harmowatch/redux-decorators'; 21 | 22 | export const getActionType = ReduxActionDispatcher.getType; 23 | export const dispatch = ReduxActionDispatcher.dispatch; 24 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/interfaces/redux-action-with-payload.interface.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | 3 | export interface ReduxActionWithPayload

extends Action { 4 | type: string; 5 | payload?: P; 6 | } 7 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/interfaces/redux-child-module-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { ReduxStateDefinition } from './redux-state-definition.interface'; 2 | 3 | export interface ReduxChildModuleConfig { 4 | state?: ReduxStateDefinition; 5 | } 6 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/interfaces/redux-root-module-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { Middleware, Store } from 'redux'; 2 | import { ReduxChildModuleConfig } from './redux-child-module-config.interface'; 3 | import { ReduxReducerProvider } from '../providers/redux-reducer.provider'; 4 | import { ReduxRootState } from './redux-root-state.interface'; 5 | 6 | export interface ReduxRootModuleConfig extends ReduxChildModuleConfig { 7 | middlewareFunctions?: Middleware[]; 8 | storeFactory?(reduxReducerProvider: ReduxReducerProvider): Store; 9 | } 10 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/interfaces/redux-root-state.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ReduxRootState { 2 | [key: string]: T; 3 | } 4 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/interfaces/redux-state-definition.interface.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | 3 | import { ReduxStateProvider } from '../providers/redux-state.provider'; 4 | 5 | export interface ReduxStateDefinition { 6 | provider: Type>; 7 | reducers?: Type<{}>[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/pipes/redux-select.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from '@angular/core'; 2 | import { async, TestBed } from '@angular/core/testing'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { selectorSuiteFactory } from '../testing/selector/suite.config'; 6 | import { TestingStateProvider } from '../testing/state'; 7 | import { ReduxTestingStore } from '../testing/store'; 8 | import { ReduxSelectPipe } from './redux-select.pipe'; 9 | import { ReduxModule } from '../redux.module'; 10 | import { ReduxStore } from '../tokens/redux-store.token'; 11 | 12 | describe('select/pipe', () => { 13 | 14 | let pipe: ReduxSelectPipe; 15 | let transformedValue: Observable<{}>; 16 | let store: ReduxTestingStore; 17 | 18 | beforeEach(async(() => { 19 | TestBed.configureTestingModule({ 20 | imports: [ 21 | ReduxModule.forRoot({ 22 | storeFactory: ReduxTestingStore.factory, 23 | state: { 24 | provider: TestingStateProvider, 25 | } 26 | }), 27 | ], 28 | providers: [ 29 | TestingStateProvider, 30 | ] 31 | }); 32 | 33 | store = TestBed.get(ReduxStore); 34 | store.setState(TestingStateProvider, TestingStateProvider.INITIAL_STATE); 35 | pipe = new ReduxSelectPipe( 36 | [ {provider: TestingStateProvider} ], 37 | TestBed.get(Injector), 38 | ); 39 | 40 | transformedValue = pipe.transform('todo/items'); 41 | 42 | })); 43 | 44 | it('returns an observable', () => { 45 | expect(transformedValue instanceof Observable).toBeTruthy(); 46 | }); 47 | 48 | selectorSuiteFactory().forEach((cfg) => { 49 | 50 | describe(`select "${cfg.given.path}"`, () => { 51 | 52 | it('directly returns the initial state', ((done) => { 53 | 54 | pipe.transform(cfg.given.path).subscribe((value) => { 55 | expect(value).toEqual(cfg.result.initialState); 56 | done(); 57 | }); 58 | 59 | })); 60 | 61 | it('returns the same selector instance ', () => { 62 | const firstInstance = pipe.transform(cfg.given.path); 63 | const secondInstance = pipe.transform(cfg.given.path); 64 | expect(firstInstance === secondInstance).toBeTruthy(); 65 | }); 66 | 67 | it('is distinct', () => { 68 | 69 | }); 70 | 71 | }); 72 | 73 | }); 74 | 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/pipes/redux-select.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { Inject, Injector, Pipe, PipeTransform } from '@angular/core'; 3 | 4 | import { ReduxStateProvider } from '../providers/redux-state.provider'; 5 | import { ReduxStateDefinitionToken } from '../tokens/redux-state-definition.token'; 6 | import { ReduxStateDefinition } from '../interfaces/redux-state-definition.interface'; 7 | 8 | @Pipe({name: 'reduxSelect'}) 9 | export class ReduxSelectPipe implements PipeTransform { 10 | 11 | private provider: ReduxStateProvider; 12 | 13 | constructor(@Inject(ReduxStateDefinitionToken) stateDefs: ReduxStateDefinition[] = [], injector: Injector) { 14 | this.provider = injector.get(stateDefs[ 0 ].provider) as ReduxStateProvider; 15 | } 16 | 17 | transform(selector: string): Observable<{}> { 18 | return this.provider.select(selector); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/providers/redux-reducer.provider.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { ReduxReducerProvider } from './redux-reducer.provider'; 3 | import { Injectable } from '@angular/core'; 4 | import { ReduxRegistry } from './redux-registry'; 5 | 6 | @Injectable() 7 | class StateA { 8 | public name = 'foo'; 9 | } 10 | 11 | @Injectable() 12 | class StateB { 13 | public name = 'foo'; 14 | } 15 | 16 | describe('ReduxReducerProvider', () => { 17 | 18 | let fixture: ReduxReducerProvider; 19 | 20 | beforeEach(async(() => { 21 | 22 | TestBed.configureTestingModule({ 23 | providers: [ 24 | ReduxReducerProvider, 25 | StateA, 26 | StateB, 27 | ] 28 | }); 29 | 30 | fixture = TestBed.get(ReduxReducerProvider); 31 | 32 | spyOn(ReduxRegistry, 'registerState'); 33 | })); 34 | 35 | describe('addStateProvider()', () => { 36 | 37 | it('registers the state t the registry', () => { 38 | const expectedProvider = TestBed.get(StateA); 39 | 40 | fixture.addStateProvider(expectedProvider); 41 | expect(ReduxRegistry.registerState).toHaveBeenCalledTimes(1); 42 | expect(ReduxRegistry.registerState).toHaveBeenCalledWith(expectedProvider); 43 | }); 44 | 45 | it('throws an exception if a state name is registered twice', () => { 46 | fixture.addStateProvider(TestBed.get(StateA)); 47 | 48 | expect(() => { 49 | fixture.addStateProvider(TestBed.get(StateB)); 50 | }).toThrowError('State "foo" is registered twice! Make sure your state name is unique!'); 51 | }); 52 | 53 | it('throws an exception if the provider is registered twice', () => { 54 | fixture.addStateProvider(TestBed.get(StateA)); 55 | 56 | expect(() => { 57 | fixture.addStateProvider(TestBed.get(StateA)); 58 | }).toThrowError('State "foo" is registered twice! Make sure your state name is unique!'); 59 | }); 60 | 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/providers/redux-reducer.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Reducer } from 'redux'; 4 | import { ReduxStateProvider } from './redux-state.provider'; 5 | import { IRegisterStatePayload, ReduxRegistry } from './redux-registry'; 6 | import { ReduxRootState } from '../interfaces/redux-root-state.interface'; 7 | import { ReduxActionWithPayload } from '../interfaces/redux-action-with-payload.interface'; 8 | 9 | @Injectable() 10 | export class ReduxReducerProvider { 11 | 12 | private stateProviders: { 13 | [name: string]: ReduxStateProvider, 14 | } = {}; 15 | 16 | public get rootReducer(): Reducer { 17 | return this.reduce.bind(this); 18 | } 19 | 20 | public addStateProvider(provider: ReduxStateProvider) { 21 | if (!this.stateProviders[ provider.name ]) { 22 | ReduxRegistry.registerState(provider); 23 | this.stateProviders[ provider.name ] = provider; 24 | } else { 25 | throw new Error(`State "${provider.name}" is registered twice! Make sure your state name is unique!`); 26 | } 27 | } 28 | 29 | public reduce(rootState: ReduxRootState, action: ReduxActionWithPayload): ReduxRootState { 30 | 31 | if (action.type === ReduxRegistry.ACTION_REGISTER_STATE) { 32 | const regAction = (action as {} as ReduxActionWithPayload); 33 | 34 | return { 35 | ...rootState, 36 | [ regAction.payload.name ]: regAction.payload.initialValue, 37 | }; 38 | } 39 | 40 | return Object.values(this.stateProviders).reduce((stateToReduce, provider) => { 41 | return Object.assign({}, stateToReduce, { 42 | [ provider.name ]: provider.reduce(stateToReduce[ provider.name ], action), 43 | }); 44 | }, rootState); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/providers/redux-registry.ts: -------------------------------------------------------------------------------- 1 | import { ReduxActionDispatcher, ReduxStateDecorator } from '@harmowatch/redux-decorators'; 2 | 3 | import { Store } from 'redux'; 4 | import { AsyncSubject, Observable } from 'rxjs'; 5 | 6 | import { Inject, Injectable } from '@angular/core'; 7 | import { ReduxStore } from '../tokens/redux-store.token'; 8 | import { ReduxActionWithPayload } from '../interfaces/redux-action-with-payload.interface'; 9 | import { ReduxStateProvider } from './redux-state.provider'; 10 | 11 | export interface IRegisterStatePayload { 12 | initialValue: {}; 13 | name: string; 14 | } 15 | 16 | @Injectable() 17 | export class ReduxRegistry { 18 | 19 | public static readonly ACTION_REGISTER_STATE = `@harmowatch/ngx-redux-core/registerState`; 20 | 21 | private static _store = new AsyncSubject>(); 22 | 23 | constructor(@Inject(ReduxStore) store: Store<{}> = null) { 24 | ReduxRegistry.reset(); 25 | ReduxRegistry.registerStore(store); 26 | } 27 | 28 | public static reset() { 29 | ReduxRegistry._store = new AsyncSubject>(); 30 | } 31 | 32 | public static registerStore(store: Store<{}>) { 33 | ReduxRegistry.reset(); 34 | ReduxRegistry._store.next(store); 35 | ReduxRegistry._store.complete(); 36 | 37 | ReduxActionDispatcher.dispatchedActions.subscribe(action => { 38 | 39 | const reduxAction: ReduxActionWithPayload = { 40 | type: action.type, 41 | payload: action.payload, 42 | }; 43 | 44 | store.dispatch(reduxAction); 45 | 46 | if (action.onDispatchSuccess) { 47 | action.onDispatchSuccess(); 48 | } 49 | 50 | }); 51 | } 52 | 53 | public static registerState(state: ReduxStateProvider) { 54 | ReduxRegistry.getStore().then((store) => { 55 | 56 | const stateConfig = ReduxStateDecorator.get(state.constructor); 57 | const initialState = state.getInitialState(); 58 | 59 | Promise 60 | .resolve(initialState instanceof Observable ? initialState.toPromise() : initialState) 61 | .then(initialValue => { 62 | store.dispatch>({ 63 | payload: { 64 | initialValue, 65 | name: stateConfig.name, 66 | }, 67 | type: ReduxRegistry.ACTION_REGISTER_STATE, 68 | }); 69 | }); 70 | 71 | }); 72 | } 73 | 74 | public static getStore(): Promise> { 75 | return new Promise(ReduxRegistry._store.subscribe.bind(ReduxRegistry._store)); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/providers/redux-state.provider.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgZone } from '@angular/core'; 2 | import { async, TestBed } from '@angular/core/testing'; 3 | import { ReduxStateDecorator } from '@harmowatch/redux-decorators'; 4 | import { take } from 'rxjs/operators'; 5 | import { ReduxTestingStore } from '../testing/store'; 6 | import { ReduxAction, ReduxReducer, ReduxState } from '../decorators/index'; 7 | import { ReduxActionWithPayload } from '../interfaces/redux-action-with-payload.interface'; 8 | import { ReduxModule } from '../redux.module'; 9 | import { ReduxStore } from '../tokens/redux-store.token'; 10 | import { ReduxStateProvider } from './redux-state.provider'; 11 | 12 | describe('ReduxStateProvider', () => { 13 | 14 | describe('exceptions', () => { 15 | 16 | describe('getInitialState() is NOT implemented', () => { 17 | 18 | it('throws an exception', () => { 19 | 20 | class TestSubject extends ReduxStateProvider { 21 | 22 | constructor() { 23 | spyOn(ReduxStateDecorator, 'get').and.returnValue({ 24 | name: 'some-name', 25 | }); 26 | super([{ 27 | provider: null, 28 | reducers: [], 29 | }], ({ 30 | run: jasmine.createSpy('Zone.run') 31 | }) as {} as NgZone); 32 | } 33 | 34 | } 35 | 36 | expect(new TestSubject().getInitialState).toThrowError('Method "getInitialState" not implemented.'); 37 | 38 | }); 39 | 40 | }); 41 | 42 | describe('the class is not registered to the redux module', () => { 43 | 44 | @ReduxState({name: 'registered-state'}) 45 | class RegisteredStateProvider extends ReduxStateProvider { 46 | getInitialState() { 47 | return {}; 48 | } 49 | } 50 | 51 | @ReduxState({name: 'unregistered-state'}) 52 | class UnregisteredStateProvider extends ReduxStateProvider { 53 | 54 | } 55 | 56 | beforeEach(async(() => { 57 | TestBed.configureTestingModule({ 58 | imports: [ 59 | ReduxModule.forRoot({ 60 | storeFactory: ReduxTestingStore.factory, 61 | state: { 62 | provider: RegisteredStateProvider, 63 | } 64 | }), 65 | ], 66 | providers: [ 67 | RegisteredStateProvider, 68 | UnregisteredStateProvider, 69 | ] 70 | }); 71 | })); 72 | 73 | it('throws an exception', () => { 74 | expect(() => TestBed.get(UnregisteredStateProvider)).toThrowError( 75 | 'Unable to resolve state definition! Make sure you\'ve registered the provider to ReduxModule!' 76 | ); 77 | }); 78 | 79 | }); 80 | 81 | describe('the class is not decorated by "@ReduxState"', () => { 82 | 83 | class TestSubject extends ReduxStateProvider { 84 | 85 | } 86 | 87 | beforeEach(async(() => { 88 | TestBed.configureTestingModule({ 89 | imports: [ 90 | ReduxModule.forRoot({ 91 | storeFactory: ReduxTestingStore.factory, 92 | state: { 93 | provider: TestSubject, 94 | } 95 | }), 96 | ], 97 | providers: [ 98 | TestSubject, 99 | ] 100 | }); 101 | })); 102 | 103 | it('throws an exception', () => { 104 | expect(() => TestBed.get(TestSubject)) 105 | .toThrowError('Unable to resolve state name! Make sure you\'ve decorated the provider by "@ReduxState"!'); 106 | }); 107 | 108 | }); 109 | 110 | }); 111 | 112 | describe('a well configured state provider ', () => { 113 | 114 | @ReduxState({name: 'test-state'}) 115 | class TestSubject extends ReduxStateProvider { 116 | 117 | getInitialState(): {} { 118 | return {}; 119 | } 120 | 121 | } 122 | 123 | class TestActions { 124 | 125 | @ReduxAction() 126 | public setFoo(foo: string): string { 127 | return foo; 128 | } 129 | 130 | } 131 | 132 | class TestReducer { 133 | 134 | public static spy = jasmine.createSpy('TestReducer.spy').and.callFake((_, s, action) => { 135 | return { 136 | ...s, 137 | foo: 'touched by TestReducer', 138 | }; 139 | }); 140 | 141 | constructor() { 142 | TestReducer.spy.calls.reset(); 143 | } 144 | 145 | @ReduxReducer('setFoo') 146 | public setFoo(state: {}, action: ReduxActionWithPayload): {} { 147 | // make sure "this" is binded 148 | expect(this instanceof TestReducer).toBeTruthy(); 149 | return TestReducer.spy('setFoo', state, action); 150 | } 151 | 152 | @ReduxReducer('clearFoo') 153 | public clearFoo(state: {}, action: ReduxActionWithPayload): {} { 154 | // make sure "this" is binded 155 | expect(this instanceof TestReducer).toBeTruthy(); 156 | return TestReducer.spy('clearFoo', state, action); 157 | } 158 | 159 | // it shall not throw an exception for the undecorated method 160 | private foo() { 161 | 162 | } 163 | 164 | } 165 | 166 | class SomeOtherReducer { 167 | 168 | public static spy = jasmine.createSpy('SomeOtherReducer.spy').and.callFake((_, s, action) => { 169 | return { 170 | ...s, 171 | foo: 'touched by SomeOtherReducer', 172 | }; 173 | }); 174 | 175 | constructor() { 176 | SomeOtherReducer.spy.calls.reset(); 177 | } 178 | 179 | @ReduxReducer([TestActions.prototype.setFoo, 'some-other-event']) 180 | public setFoo(state: {}, action: ReduxActionWithPayload): {} { 181 | return SomeOtherReducer.spy('setFoo', state, action); 182 | } 183 | 184 | // it shall not throw an exception for the undecorated method 185 | public foo() { 186 | 187 | } 188 | 189 | } 190 | 191 | class ThirdReducer { 192 | 193 | public static spy = jasmine.createSpy('ThirdReducer.spy').and.callFake((_, s, action) => { 194 | return { 195 | ...s, 196 | foo: 'touched by ThirdReducer', 197 | }; 198 | }); 199 | 200 | constructor() { 201 | ThirdReducer.spy.calls.reset(); 202 | } 203 | 204 | @ReduxReducer(TestActions.prototype.setFoo) 205 | public setFoo(state: {}, action: ReduxActionWithPayload): {} { 206 | return ThirdReducer.spy('setFoo', state, action); 207 | } 208 | 209 | } 210 | 211 | let fixture: TestSubject; 212 | 213 | beforeEach(async(() => { 214 | TestBed.configureTestingModule({ 215 | imports: [ 216 | ReduxModule.forRoot({ 217 | storeFactory: ReduxTestingStore.factory, 218 | state: { 219 | provider: TestSubject, 220 | reducers: [TestReducer, SomeOtherReducer, ThirdReducer], 221 | } 222 | }), 223 | ], 224 | providers: [ 225 | TestSubject, 226 | ] 227 | }); 228 | 229 | fixture = TestBed.get(TestSubject); 230 | TestBed.get(ReduxStore).setState(TestSubject, {foo: 'bar'}); 231 | })); 232 | 233 | describe('select()', () => { 234 | 235 | let fooValues: string[]; 236 | 237 | beforeEach(async(() => { 238 | fooValues = []; 239 | 240 | const selector = fixture.select('foo'); 241 | selector.subscribe(foo => fooValues.push(foo)); 242 | 243 | selector 244 | .pipe(take(1)) 245 | .toPromise() 246 | .then(() => TestBed.get(ReduxStore).setState(TestSubject, {foo: 'baz'})); 247 | 248 | })); 249 | 250 | it('selects the correct value', () => { 251 | expect(fooValues).toEqual(['bar', 'baz']); 252 | }); 253 | 254 | it('returns the same instance of ReduxSelector, if the same selector is given', () => { 255 | expect(fixture.select('foo')).toBe(fixture.select('foo')); 256 | }); 257 | 258 | it('returns a new instance of ReduxSelector, if another same selector is given', () => { 259 | expect(fixture.select('foo')).not.toBe(fixture.select('fuz')); 260 | }); 261 | 262 | }); 263 | 264 | }); 265 | 266 | }); 267 | 268 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/providers/redux-state.provider.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { distinctUntilChanged } from 'rxjs/operators'; 3 | import { Inject, NgZone, Type } from '@angular/core'; 4 | import { ReduxActionDispatcher, ReduxReducerDecorator, ReduxStateDecorator } from '@harmowatch/redux-decorators'; 5 | 6 | import { ReduxStateDefinition } from '../interfaces/redux-state-definition.interface'; 7 | import { ReduxActionWithPayload } from '../interfaces/redux-action-with-payload.interface'; 8 | import { ReduxStateDefinitionToken } from '../tokens/redux-state-definition.token'; 9 | 10 | import { ReduxSelector } from '../redux-selector'; 11 | import { ReduxRegistry } from './redux-registry'; 12 | 13 | export abstract class ReduxStateProvider { 14 | 15 | public static instancesByName: { [stateName: string]: ReduxStateProvider } = {}; 16 | 17 | public readonly name: string; 18 | public readonly stateDef: ReduxStateDefinition; 19 | 20 | protected selectorCache: { [selector: string]: Observable<{}> } = {}; 21 | protected reducerMethodsByType: { [actionType: string]: Function[] }; 22 | 23 | constructor(@Inject(ReduxStateDefinitionToken) stateDefs: ReduxStateDefinition[] = [], 24 | private zone: NgZone) { 25 | 26 | const {name = null} = ReduxStateDecorator.get(this.constructor) || {}; 27 | 28 | if (!name) { 29 | throw new Error( 30 | 'Unable to resolve state name! Make sure you\'ve decorated the provider by "@ReduxState"!' 31 | ); 32 | } 33 | 34 | this.name = ReduxStateDecorator.get(this.constructor).name; 35 | this.stateDef = stateDefs.find(def => ReduxStateDecorator.get(def.provider).name === name); 36 | 37 | if (!this.stateDef) { 38 | throw new Error( 39 | 'Unable to resolve state definition! Make sure you\'ve registered the provider to ReduxModule!' 40 | ); 41 | } 42 | 43 | this.reducerMethodsByType = (this.stateDef.reducers || []) 44 | .map(clazz => this.getReducerMethods(new clazz())) 45 | .reduce((all, curr) => [].concat(curr, all), []) // [].concat keeps the order, all.concat(curr) destroys the order 46 | // .reduce((all, curr) => [curr, ...all], []) // [].concat keeps the order, all.concat(curr) destroys the order 47 | .reduce((methodsByType, reducer) => { 48 | const type = ReduxActionDispatcher.getType(reducer.type); 49 | 50 | return { 51 | ...methodsByType, 52 | [ type ]: [ reducer.method ].concat(methodsByType[ type ] || []) 53 | }; 54 | }, {}); 55 | 56 | ReduxStateProvider.instancesByName[ this.name ] = this; 57 | } 58 | 59 | getInitialState(): S | Promise | Observable { 60 | throw new Error('Method "getInitialState" not implemented.'); 61 | } 62 | 63 | select(selector = ''): Observable { 64 | 65 | const stateType = this.constructor as Type>; 66 | selector = ReduxSelector.normalize(selector, stateType); 67 | 68 | if (!this.selectorCache[ selector ]) { 69 | this.selectorCache[ selector ] = new ReduxSelector(this.zone, selector, stateType).pipe(distinctUntilChanged()); 70 | } 71 | 72 | return this.selectorCache[ selector ] as ReduxSelector; 73 | } 74 | 75 | getState(): Promise { 76 | return ReduxRegistry.getStore().then(store => { 77 | return ReduxSelector.getValueByState(store.getState(), '/' + this.name); 78 | }); 79 | } 80 | 81 | reduce

(state: S, action: ReduxActionWithPayload

): S { 82 | const reducerMethods = this.reducerMethodsByType[ action.type ] || []; 83 | return reducerMethods.reduce((stateToReduce, method) => method(stateToReduce, action), state); 84 | } 85 | 86 | private getReducerMethods(reducerClassInstance: any) { 87 | return Object.values(Object.getPrototypeOf(reducerClassInstance)) 88 | .map(method => { 89 | return { 90 | method: (method as Function).bind(reducerClassInstance), 91 | type: ReduxReducerDecorator.get(method), 92 | }; 93 | }) 94 | .filter(reducer => reducer && reducer.type) 95 | // convert array of types to multiple method entries 96 | .reduce((all, curr) => all.concat([].concat(curr.type).map(type => ({...curr, type}))), []); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/redux-selector.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgZone } from '@angular/core'; 2 | import { async, TestBed } from '@angular/core/testing'; 3 | import { ReduxRegistry } from './providers/redux-registry'; 4 | import { Observable } from 'rxjs'; 5 | import { ReduxSelector } from './redux-selector'; 6 | import { TestingState, TestingStateProvider } from './testing/state'; 7 | import { ReduxTestingStore } from './testing/store'; 8 | import { ReduxModule } from './redux.module'; 9 | import { ReduxStore } from './tokens/redux-store.token'; 10 | import { ReduxRootState } from './interfaces/redux-root-state.interface'; 11 | 12 | describe('ReduxSelector', () => { 13 | 14 | let store: ReduxTestingStore; 15 | let zone: NgZone; 16 | 17 | beforeEach(async(() => { 18 | 19 | TestBed.configureTestingModule({ 20 | imports: [ 21 | ReduxModule.forRoot({ 22 | storeFactory: ReduxTestingStore.factory, 23 | }), 24 | ], 25 | }); 26 | 27 | store = TestBed.get(ReduxStore); 28 | zone = TestBed.get(NgZone); 29 | store.setState(TestingStateProvider, TestingStateProvider.INITIAL_STATE); 30 | })); 31 | 32 | it('can handle falsy values', done => { 33 | new ReduxSelector(zone, 'todo/isFetching', TestingStateProvider).subscribe(isFetching => { 34 | expect(isFetching).toBe(false); 35 | done(); 36 | }); 37 | }); 38 | 39 | it('can handle invalid selectors', done => { 40 | new ReduxSelector(zone, 'im/a/invalid/selector', TestingStateProvider).subscribe(value => { 41 | expect(value).toBe(null); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('runs in a zone', done => { 47 | const runSpy = jasmine.createSpy('Zone.run').and.callFake(fn => fn()); 48 | const mockZone = {run: runSpy} as {} as NgZone; 49 | 50 | new ReduxSelector(mockZone).subscribe(value => { 51 | expect(mockZone.run).toHaveBeenCalled(); 52 | done(); 53 | }); 54 | }); 55 | 56 | describe('instantiation with zone parameter only', () => { 57 | 58 | let selector: Observable; 59 | 60 | beforeEach(() => { 61 | selector = new ReduxSelector(zone); 62 | }); 63 | 64 | it('selects the root state', done => { 65 | selector.subscribe(selectedState => { 66 | const expectedState: ReduxRootState = { 67 | [TestingStateProvider.NAME]: TestingStateProvider.INITIAL_STATE, 68 | }; 69 | 70 | expect(selectedState).toEqual(expectedState); 71 | done(); 72 | }); 73 | }); 74 | 75 | }); 76 | 77 | describe('instantiation with a relative selector, but no state provider reference was given', () => { 78 | 79 | it('throws an exception', () => { 80 | expect(function () { 81 | new ReduxSelector(zone, '').subscribe(); 82 | }).toThrowError('You need to provide a state provider, if you use relative selectors'); 83 | }); 84 | 85 | }); 86 | 87 | describe('instantiation with a relative selector and a state provider reference', () => { 88 | 89 | let selector: Observable; 90 | 91 | beforeEach(() => { 92 | selector = new ReduxSelector(zone, 'todo/items', TestingStateProvider); 93 | }); 94 | 95 | it('selects the testing state', done => { 96 | selector.subscribe(todoItems => { 97 | expect(todoItems).toEqual(TestingStateProvider.INITIAL_STATE.todo.items); 98 | done(); 99 | }); 100 | }); 101 | 102 | it('is updated when the state changes', done => { 103 | 104 | const state1: TestingState = { 105 | ...TestingStateProvider.INITIAL_STATE, 106 | todo: { 107 | ...TestingStateProvider.INITIAL_STATE.todo, 108 | items: [] 109 | } 110 | }; 111 | 112 | const state2: TestingState = { 113 | ...TestingStateProvider.INITIAL_STATE, 114 | todo: { 115 | ...TestingStateProvider.INITIAL_STATE.todo, 116 | items: ['It works'] 117 | } 118 | }; 119 | 120 | const expectedSequence: string[][] = [ 121 | [], 122 | state1.todo.items, 123 | state2.todo.items, 124 | ]; 125 | 126 | const givenSequence: string[][] = []; 127 | selector.subscribe(todoItems => givenSequence.push(todoItems)); 128 | 129 | store.setState(TestingStateProvider, state1) 130 | .then(() => store.setState(TestingStateProvider, state2)) 131 | .then(() => { 132 | expect(givenSequence).toEqual(expectedSequence); 133 | done(); 134 | }); 135 | 136 | }); 137 | 138 | }); 139 | 140 | it('it will not create an observerable for each observer', () => { 141 | spyOn(ReduxRegistry, 'getStore').and.callThrough(); 142 | const selector = new ReduxSelector(zone, 'todo/items', TestingStateProvider); 143 | 144 | selector.subscribe(() => null); 145 | selector.subscribe(() => null); 146 | 147 | expect(ReduxRegistry.getStore).toHaveBeenCalledTimes(1); 148 | }); 149 | 150 | }); 151 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/redux-selector.ts: -------------------------------------------------------------------------------- 1 | import { NgZone, Type } from '@angular/core'; 2 | import { ReplaySubject } from 'rxjs'; 3 | import { ReduxStateDecorator } from '@harmowatch/redux-decorators'; 4 | 5 | import { ReduxRegistry } from './providers/redux-registry'; 6 | import { ReduxRootState } from './interfaces/redux-root-state.interface'; 7 | import { ReduxStateProvider } from './providers/redux-state.provider'; 8 | 9 | export class ReduxSelector extends ReplaySubject { 10 | 11 | private static readonly DELIMITER = '/'; 12 | 13 | constructor(zone: NgZone, 14 | selector = '/', 15 | stateProvider?: Type) { 16 | 17 | if (!selector.startsWith(ReduxSelector.DELIMITER) && !stateProvider) { 18 | throw new Error('You need to provide a state provider, if you use relative selectors'); 19 | } 20 | 21 | super(1); 22 | 23 | ReduxRegistry.getStore().then(store => { 24 | const next = () => { 25 | zone.run(() => { 26 | this.next(ReduxSelector.getValueByState(store.getState(), selector, stateProvider)); 27 | }); 28 | }; 29 | 30 | store.subscribe(() => next()); 31 | next(); // we need to trigger a initial value, otherwise we've to wait until the first state change 32 | }); 33 | 34 | } 35 | 36 | public static normalize(selector: string, stateProvider?: Type): string { 37 | if (!selector.startsWith(ReduxSelector.DELIMITER)) { 38 | const stateName = ReduxStateDecorator.get(stateProvider).name; 39 | return `${ReduxSelector.DELIMITER}${stateName}${ReduxSelector.DELIMITER}${selector}`; 40 | } 41 | 42 | return selector; 43 | } 44 | 45 | public static getValueByState(state: ReduxRootState, 46 | selector: string, 47 | stateProvider?: Type): S { 48 | 49 | /* save the return value in a constant to prevent 50 | * "Metadata collected contains an error that will be reported at runtime: Lambda not supported." 51 | * error 52 | */ 53 | const value: S = ReduxSelector.normalize(selector, stateProvider).split(ReduxSelector.DELIMITER) 54 | .filter(propertyKey => propertyKey !== '') 55 | .reduce((previousValue, propertyKey) => { 56 | if (!previousValue || !previousValue.hasOwnProperty(propertyKey)) { 57 | return null; 58 | } 59 | 60 | return previousValue[ propertyKey ]; 61 | }, state as {}); 62 | 63 | return value; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/redux.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { async } from '@angular/core/testing'; 2 | import { compose, Middleware } from 'redux'; 3 | import { ReduxActionWithPayload } from './index'; 4 | import { ReduxReducerProvider } from './providers/redux-reducer.provider'; 5 | import { ReduxModule } from './redux.module'; 6 | 7 | describe('ReduxModule', () => { 8 | 9 | let reduxReducerProviderMock: ReduxReducerProvider; 10 | 11 | describe('.defaultStoreFactory()', () => { 12 | 13 | beforeEach(async(() => { 14 | reduxReducerProviderMock = { 15 | rootReducer: s => s, 16 | } as {} as ReduxReducerProvider; 17 | 18 | window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] = jasmine.createSpy('__REDUX_DEVTOOLS_EXTENSION_COMPOSE__').and.callFake(compose); 19 | })); 20 | 21 | it('enables the dev tools only if we are in dev mode', () => { 22 | ReduxModule.defaultStoreFactory(reduxReducerProviderMock, [], true); 23 | expect(window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__']).toHaveBeenCalledTimes(1); 24 | window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'].calls.reset(); 25 | 26 | ReduxModule.defaultStoreFactory(reduxReducerProviderMock, [], false); 27 | expect(window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__']).not.toHaveBeenCalled(); 28 | }); 29 | 30 | 31 | describe('middleware registration', () => { 32 | 33 | let middlewareA: Middleware; 34 | let middlewareASpy: jasmine.Spy; 35 | 36 | let middlewareB: Middleware; 37 | let middlewareBSpy: jasmine.Spy; 38 | 39 | beforeEach(() => { 40 | middlewareASpy = jasmine.createSpy('middlewareASpy'); 41 | middlewareBSpy = jasmine.createSpy('middlewareBSpy'); 42 | 43 | middlewareA = _store => next => (action: ReduxActionWithPayload) => { 44 | middlewareASpy(action); 45 | return next(action); 46 | }; 47 | 48 | middlewareB = _store => next => (action: ReduxActionWithPayload) => { 49 | middlewareBSpy(action); 50 | return next(action); 51 | }; 52 | 53 | }); 54 | 55 | it('will register when devMode is true', () => { 56 | const store = ReduxModule.defaultStoreFactory( 57 | reduxReducerProviderMock, 58 | [middlewareA, middlewareB], 59 | true 60 | ); 61 | 62 | const expectedAction = {type: 'Foo'}; 63 | 64 | store.dispatch(expectedAction); 65 | 66 | expect(middlewareASpy).toHaveBeenCalledTimes(1); 67 | expect(middlewareASpy).toHaveBeenCalledWith(expectedAction); 68 | 69 | expect(middlewareBSpy).toHaveBeenCalledTimes(1); 70 | expect(middlewareBSpy).toHaveBeenCalledWith(expectedAction); 71 | }); 72 | 73 | it('will register when devMode is false', () => { 74 | const store = ReduxModule.defaultStoreFactory( 75 | reduxReducerProviderMock, 76 | [middlewareA, middlewareB], 77 | false 78 | ); 79 | 80 | const expectedAction = {type: 'Foo'}; 81 | 82 | store.dispatch(expectedAction); 83 | 84 | expect(middlewareASpy).toHaveBeenCalledTimes(1); 85 | expect(middlewareASpy).toHaveBeenCalledWith(expectedAction); 86 | 87 | expect(middlewareBSpy).toHaveBeenCalledTimes(1); 88 | expect(middlewareBSpy).toHaveBeenCalledWith(expectedAction); 89 | }); 90 | 91 | }); 92 | 93 | }); 94 | 95 | }); 96 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/redux.module.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore, Middleware, Store, StoreEnhancer } from 'redux'; 2 | 3 | import { CommonModule } from '@angular/common'; 4 | import { Inject, Injector, isDevMode, ModuleWithProviders, NgModule, Optional } from '@angular/core'; 5 | import { ReduxSelectPipe } from './pipes/redux-select.pipe'; 6 | import { ReduxReducerProvider } from './providers/redux-reducer.provider'; 7 | import { ReduxStateDefinitionToken } from './tokens/redux-state-definition.token'; 8 | import { ReduxStateDefinition } from './interfaces/redux-state-definition.interface'; 9 | import { ReduxRegistry } from './providers/redux-registry'; 10 | import { ReduxChildModuleConfig } from './interfaces/redux-child-module-config.interface'; 11 | import { ReduxRootModuleConfig } from './interfaces/redux-root-module-config.interface'; 12 | import { ReduxStore } from './tokens/redux-store.token'; 13 | import { ReduxMiddlewares } from './tokens/redux-middlewares.token'; 14 | 15 | @NgModule({ 16 | declarations: [ 17 | ReduxSelectPipe, 18 | ], 19 | exports: [ 20 | ReduxSelectPipe, 21 | ], 22 | imports: [ 23 | CommonModule, 24 | ], 25 | }) 26 | export class ReduxModule { 27 | 28 | constructor(injector: Injector, 29 | reducerProvider: ReduxReducerProvider, 30 | @Optional() @Inject(ReduxStateDefinitionToken) stateDefs: ReduxStateDefinition[] = []) { 31 | 32 | injector.get(ReduxRegistry); // just make sure the provider is instantiated 33 | 34 | if (Array.isArray(stateDefs)) { 35 | stateDefs 36 | .filter(def => def && def.provider) 37 | .map(def => injector.get(def.provider)) 38 | .forEach(provider => reducerProvider.addStateProvider(provider)); 39 | } 40 | 41 | } 42 | 43 | public static forChild(config: ReduxChildModuleConfig = {}): ModuleWithProviders { 44 | return { 45 | ngModule: ReduxModule, 46 | providers: [ 47 | {provide: ReduxStateDefinitionToken, useValue: config.state || null, multi: true}, 48 | ], 49 | }; 50 | } 51 | 52 | public static forRoot(config: ReduxRootModuleConfig = {}): ModuleWithProviders { 53 | return { 54 | ngModule: ReduxModule, 55 | providers: [ 56 | ReduxReducerProvider, 57 | ReduxRegistry, 58 | { 59 | provide: ReduxStore, 60 | useFactory: config.storeFactory || ReduxModule.defaultStoreFactory, 61 | deps: [ReduxReducerProvider, ReduxMiddlewares] 62 | }, 63 | {provide: ReduxStateDefinitionToken, useValue: config.state || null, multi: true}, 64 | {provide: ReduxMiddlewares, useValue: config.middlewareFunctions || [], multi: false}, 65 | ], 66 | }; 67 | } 68 | 69 | public static defaultStoreFactory(reduxReducerProvider: ReduxReducerProvider, 70 | middlewareFunctions: Middleware[], 71 | devMode = isDevMode()): Store<{}> { 72 | 73 | return createStore( 74 | reduxReducerProvider.rootReducer, 75 | {}, 76 | ReduxModule.defaultEnhancerFactory(middlewareFunctions, devMode), 77 | ); 78 | } 79 | 80 | public static defaultEnhancerFactory(middlewareFunctions: Middleware[], devMode: boolean): StoreEnhancer<{}> { 81 | 82 | let composeEnhancers = compose; 83 | 84 | if (devMode && window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__']) { 85 | composeEnhancers = window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__']; 86 | } 87 | 88 | return composeEnhancers(applyMiddleware(...middlewareFunctions)); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/testing/selector/suite.config.ts: -------------------------------------------------------------------------------- 1 | import { ReduxStateDecorator } from '@harmowatch/redux-decorators'; 2 | import { TestingStateProvider } from '../state'; 3 | 4 | export interface SelectorTestCase { 5 | given: { 6 | name: string; 7 | path: string; 8 | }; 9 | result: { 10 | initialState: {}; 11 | }; 12 | } 13 | 14 | export function selectorSuiteFactory(): SelectorTestCase[] { 15 | 16 | const state = TestingStateProvider.INITIAL_STATE; 17 | const rootState = { 18 | [ ReduxStateDecorator.get(TestingStateProvider).name ]: state, 19 | }; 20 | 21 | return [ { 22 | given: { 23 | name: 'unknown', 24 | path: 'unknown', 25 | }, 26 | result: { 27 | initialState: null, 28 | }, 29 | }, { 30 | given: { 31 | name: 'empty', 32 | path: '', 33 | }, 34 | result: { 35 | initialState: state, 36 | }, 37 | }, { 38 | given: { 39 | name: 'todo', 40 | path: 'todo', 41 | }, 42 | result: { 43 | initialState: state.todo, 44 | }, 45 | }, { 46 | given: { 47 | name: 'todoItems', 48 | path: 'todo/items', 49 | }, 50 | result: { 51 | initialState: state.todo.items, 52 | }, 53 | }, { 54 | given: { 55 | name: 'todoItemsTrailingSlash', 56 | path: 'todo/items/', 57 | }, 58 | result: { 59 | initialState: state.todo.items, 60 | }, 61 | }, { 62 | given: { 63 | name: 'root', 64 | path: '/', 65 | }, 66 | result: { 67 | initialState: rootState, 68 | }, 69 | } ]; 70 | } 71 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/testing/state.ts: -------------------------------------------------------------------------------- 1 | import { ReduxState } from '../decorators/index'; 2 | import { ReduxStateProvider } from '../providers/redux-state.provider'; 3 | 4 | export interface TestingState { 5 | todo: { 6 | isFetching: boolean; 7 | items: string[]; 8 | }; 9 | } 10 | 11 | @ReduxState({name: TestingStateProvider.NAME}) 12 | export class TestingStateProvider extends ReduxStateProvider { 13 | 14 | public static readonly NAME = 'testing-7c66b613-20bd-4d35-8611-5181ca4a0b72'; 15 | 16 | public static readonly INITIAL_STATE: TestingState = { 17 | todo: { 18 | isFetching: false, 19 | items: [ 'Item 1', 'Item 2' ], 20 | }, 21 | }; 22 | 23 | getInitialState(): TestingState { 24 | return TestingStateProvider.INITIAL_STATE; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/testing/store.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | 3 | import { ReduxTestingStore } from './store'; 4 | import { ReduxModule } from '../redux.module'; 5 | import { TestingStateProvider } from './state'; 6 | import { ReduxStore } from '../tokens/redux-store.token'; 7 | 8 | 9 | describe('ReduxTestingStore', () => { 10 | 11 | let store: ReduxTestingStore; 12 | 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [ 16 | ReduxModule.forRoot({ 17 | storeFactory: ReduxTestingStore.factory, 18 | state: { 19 | provider: TestingStateProvider, 20 | } 21 | }), 22 | ], 23 | providers: [ 24 | TestingStateProvider, 25 | ] 26 | }).compileComponents(); 27 | 28 | store = TestBed.get(ReduxStore); 29 | })); 30 | 31 | describe('setState', () => { 32 | 33 | it('returns a promise that is resolved after the state was set', (done) => { 34 | const expectedState = TestingStateProvider.INITIAL_STATE; 35 | store.setState(TestingStateProvider, expectedState).then((givenState) => { 36 | expect(givenState).toEqual({ 37 | 'testing-7c66b613-20bd-4d35-8611-5181ca4a0b72': expectedState, 38 | }); 39 | done(); 40 | }); 41 | 42 | }); 43 | 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/testing/store.ts: -------------------------------------------------------------------------------- 1 | import { Action, Store, Unsubscribe } from 'redux'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { takeWhile } from 'rxjs/operators'; 4 | import { Injectable, Type } from '@angular/core'; 5 | import { ReduxRootState } from '../interfaces/redux-root-state.interface'; 6 | import { ReduxStateDecorator } from '@harmowatch/redux-decorators'; 7 | import { ReduxStateProvider } from '../providers/redux-state.provider'; 8 | 9 | @Injectable() 10 | export class ReduxTestingStore implements Store<{}> { 11 | 12 | private state = new BehaviorSubject(null); 13 | 14 | public static factory(): ReduxTestingStore { 15 | return new ReduxTestingStore(); 16 | } 17 | 18 | public setState(state: Type, value: S): Promise { 19 | const {name} = ReduxStateDecorator.get(state); 20 | 21 | const nextState = Object.assign({}, this.state.getValue(), { 22 | [ name ]: value, 23 | }); 24 | 25 | this.state.next(nextState); 26 | 27 | return this.state 28 | .pipe(takeWhile(currentState => currentState !== nextState)) 29 | .toPromise().then(() => this.state.getValue()); 30 | } 31 | 32 | public getState(): {} { 33 | return this.state.getValue(); 34 | } 35 | 36 | public subscribe(listener: () => void): Unsubscribe { 37 | return this.state.subscribe.call(this.state, listener); 38 | } 39 | 40 | public replaceReducer() { 41 | } 42 | 43 | public dispatch(action: T): T { 44 | return action; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/tokens/redux-middlewares.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export class ReduxMiddlewares extends InjectionToken { 4 | 5 | constructor() { 6 | super('ReduxMiddlewares'); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/tokens/redux-state-definition.token.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'redux'; 2 | import { InjectionToken } from '@angular/core'; 3 | 4 | export class ReduxStateDefinitionToken extends InjectionToken> { 5 | 6 | constructor() { 7 | super('ReduxStateDefinitionToken'); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/harmowatch/ngx-redux-core/tokens/redux-store.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { Store } from 'redux'; 3 | 4 | export class ReduxStore extends InjectionToken> { 5 | 6 | constructor() { 7 | super('ReduxStore'); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgxRedux 6 | 7 | 9 | 10 | 11 | 12 | 13 |

14 |
15 |
16 | 17 |
18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './example-app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/plugins/karma/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module jasmine { 2 | 3 | interface Matchers { 4 | 5 | // reducer matchers 6 | toReduceOn: (...any) => void; 7 | notToMutateTheGivenState: (state: {}, action?: { type?: string, payload?: T }) => void; 8 | 9 | // action matchers 10 | toDispatchAction: (action?: string) => void; 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/plugins/karma/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var path = require("path"); 3 | 4 | function createPattern(pattern) { 5 | return { pattern: pattern, included: true, served: true, watched: false }; 6 | } 7 | 8 | function factory(files) { 9 | files.push(createPattern(path.join(__dirname, '/bundle.js'))); 10 | } 11 | 12 | factory['$inject'] = ['config.files']; 13 | module.exports = { 14 | 'framework:@harmowatch/ngx-redux-core': ['factory', factory] 15 | }; 16 | -------------------------------------------------------------------------------- /src/plugins/karma/matchers/index.spec.ts: -------------------------------------------------------------------------------- 1 | import './index'; 2 | import { toReduceOn } from './to-reduce-on'; 3 | import { toDispatchAction } from './to-dispatch-action'; 4 | import { notToMutateTheGivenState } from './not-to-mutate-the-given-state'; 5 | 6 | describe('plugins/karma/matchers', () => { 7 | 8 | describe('index.ts', () => { 9 | function itRegistersTheMatcher(expectedFunction) { 10 | 11 | it(`registers the matcher "${expectedFunction.name}"`, () => { 12 | expect(expect()[ expectedFunction.name ]).toBeDefined(); 13 | }); 14 | 15 | } 16 | 17 | itRegistersTheMatcher(toReduceOn); 18 | itRegistersTheMatcher(toDispatchAction); 19 | itRegistersTheMatcher(notToMutateTheGivenState); 20 | 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /src/plugins/karma/matchers/index.ts: -------------------------------------------------------------------------------- 1 | import { notToMutateTheGivenState } from './not-to-mutate-the-given-state'; 2 | import { toDispatchAction } from './to-dispatch-action'; 3 | import { toReduceOn } from './to-reduce-on'; 4 | 5 | beforeEach(function () { 6 | 7 | jasmine.addMatchers({ 8 | notToMutateTheGivenState, 9 | toDispatchAction, 10 | toReduceOn, 11 | }); 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /src/plugins/karma/matchers/not-to-mutate-the-given-state.spec.ts: -------------------------------------------------------------------------------- 1 | import { ReduxReducerDecoratorForMethod } from '@harmowatch/redux-decorators'; 2 | import { notToMutateTheGivenState } from './not-to-mutate-the-given-state'; 3 | import { ReduxActionWithPayload } from '../../../harmowatch/ngx-redux-core/interfaces/redux-action-with-payload.interface'; 4 | 5 | 6 | interface TestState { 7 | todo: { 8 | items: string[]; 9 | }; 10 | } 11 | 12 | class TestReducer { 13 | 14 | @ReduxReducerDecoratorForMethod('foo/123') 15 | doesMutate(state: TestState, action: ReduxActionWithPayload): TestState { 16 | state.todo.items.push(action.payload); 17 | return state; 18 | } 19 | 20 | @ReduxReducerDecoratorForMethod('bar/456') 21 | doesNotMutate(state: TestState, action: ReduxActionWithPayload): TestState { 22 | return { 23 | ...state, 24 | todo: { 25 | ...state.todo, 26 | items: state.todo.items.concat(action.payload) 27 | } 28 | }; 29 | } 30 | 31 | } 32 | 33 | describe('plugins/karma/matchers', () => { 34 | 35 | describe('notToMutateTheGivenState', () => { 36 | 37 | function testCompare({desc, reducerFunction, willPass, errorMessage = ''}) { 38 | 39 | describe(desc, () => { 40 | 41 | let result; 42 | 43 | beforeEach(() => { 44 | 45 | const state = { 46 | todo: { 47 | items: [], 48 | } 49 | }; 50 | 51 | const action = { 52 | type: null, 53 | payload: 'item 123', 54 | }; 55 | 56 | result = notToMutateTheGivenState().compare(reducerFunction, state, action); 57 | }); 58 | 59 | it(willPass ? 'will pass' : 'will fail', () => { 60 | expect(result.pass).toBe(willPass); 61 | }); 62 | 63 | if (errorMessage.length > 0) { 64 | it('displays the correct error message', () => { 65 | expect(result.message).toBe(errorMessage); 66 | }); 67 | } 68 | 69 | }); 70 | } 71 | 72 | testCompare({ 73 | desc: 'The reducer does not mutate the state', 74 | willPass: true, 75 | reducerFunction: TestReducer.prototype.doesNotMutate, 76 | }); 77 | 78 | testCompare({ 79 | desc: 'The reducer does mutate the state', 80 | willPass: false, 81 | reducerFunction: TestReducer.prototype.doesMutate, 82 | errorMessage: [ 83 | 'Expected the reducer not to mutate the state, but it mutates the following properties:', 84 | '- "todo.items"', 85 | ].join('\n'), 86 | }); 87 | 88 | }); 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /src/plugins/karma/matchers/not-to-mutate-the-given-state.ts: -------------------------------------------------------------------------------- 1 | import diff from 'deep-diff'; 2 | 3 | import { ReduxActionWithPayload } from '../../../harmowatch/ngx-redux-core'; 4 | import { ReduxReducerFunction } from '@harmowatch/redux-decorators/lib/reducer/function/redux-reducer-function.type'; 5 | 6 | export function compare(actual: ReduxReducerFunction, 7 | state: {}, 8 | action: ReduxActionWithPayload): jasmine.CustomMatcherResult { 9 | 10 | const stateJson = JSON.stringify(state); 11 | const givenState = JSON.parse(stateJson); 12 | const expectedState = JSON.parse(stateJson); 13 | 14 | actual(givenState, action || null); 15 | 16 | const differences = diff(expectedState, givenState) || []; 17 | const diffStr = differences 18 | .map(difference => `- "${difference.path.join('.')}"`) 19 | .join('\n'); 20 | 21 | return { 22 | pass: differences.length === 0, 23 | message: `Expected the reducer not to mutate the state, but it mutates the following properties:\n${diffStr}`, 24 | }; 25 | 26 | } 27 | 28 | export function notToMutateTheGivenState(): jasmine.CustomMatcher { 29 | return {compare}; 30 | } 31 | -------------------------------------------------------------------------------- /src/plugins/karma/matchers/to-dispatch-action.spec.ts: -------------------------------------------------------------------------------- 1 | import { ReduxActionDecoratorForMethod, ReduxReducerDecoratorForMethod } from '@harmowatch/redux-decorators'; 2 | import { toDispatchAction } from './to-dispatch-action'; 3 | 4 | class TestAction { 5 | 6 | @ReduxActionDecoratorForMethod() 7 | foo() { 8 | } 9 | 10 | bar() { 11 | 12 | } 13 | 14 | } 15 | 16 | describe('plugins/karma/matchers', () => { 17 | 18 | describe('toDispatchAction', () => { 19 | 20 | function testCompare({desc, actionFunction, expectedAction = '', willPass, errorMessage = ''}) { 21 | 22 | describe(desc, () => { 23 | 24 | let result; 25 | beforeEach(() => { 26 | if (expectedAction.length > 0) { 27 | result = toDispatchAction().compare(actionFunction, expectedAction); 28 | } else { 29 | result = toDispatchAction().compare(actionFunction); 30 | } 31 | }); 32 | 33 | it(willPass ? 'will pass' : 'will fail', () => { 34 | expect(result.pass).toBe(willPass); 35 | }); 36 | 37 | if (errorMessage.length > 0) { 38 | it('displays the correct error message', () => { 39 | expect(result.message).toBe(errorMessage); 40 | }); 41 | } 42 | 43 | }); 44 | } 45 | 46 | testCompare({ 47 | desc: 'Expect that the method dispatch the action "foo", and it does', 48 | willPass: true, 49 | actionFunction: TestAction.prototype.foo, 50 | expectedAction: 'foo' 51 | }); 52 | 53 | testCompare({ 54 | desc: 'Expect that the method dispatch some action, and it does', 55 | willPass: true, 56 | actionFunction: TestAction.prototype.foo, 57 | }); 58 | 59 | testCompare({ 60 | desc: 'Expect that the method dispatch some action, but it doesn\'t', 61 | willPass: false, 62 | actionFunction: TestAction.prototype.bar, 63 | errorMessage: 'Expected to dispatch some action, but the method will not dispatch any action!', 64 | }); 65 | 66 | testCompare({ 67 | desc: 'Expect that the method dispatch the action "foo", but it doesn\'t', 68 | willPass: false, 69 | actionFunction: TestAction.prototype.bar, 70 | expectedAction: 'foo', 71 | errorMessage: 'Expected to dispatch action "foo", but the method will not dispatch any action!', 72 | }); 73 | 74 | testCompare({ 75 | desc: 'Expect that the method dispatch the action "bar", but it dispatched action "foo"', 76 | willPass: false, 77 | actionFunction: TestAction.prototype.foo, 78 | expectedAction: 'bar', 79 | errorMessage: 'Expected to dispatch action "bar", but it dispatches the action "foo"!', 80 | }); 81 | 82 | }); 83 | 84 | }); 85 | 86 | -------------------------------------------------------------------------------- /src/plugins/karma/matchers/to-dispatch-action.ts: -------------------------------------------------------------------------------- 1 | import { ReduxActionDispatcher } from '@harmowatch/redux-decorators'; 2 | 3 | function compare(actual, expectedActionType): jasmine.CustomMatcherResult { 4 | 5 | const actionType = ReduxActionDispatcher.getType(actual); 6 | 7 | if (expectedActionType) { 8 | if (actionType) { 9 | return { 10 | pass: actionType === expectedActionType, 11 | message: `Expected to dispatch action "${expectedActionType}", but it dispatches the action "${actionType}"!`, 12 | }; 13 | } 14 | 15 | return { 16 | pass: false, 17 | message: `Expected to dispatch action "${expectedActionType}", but the method will not dispatch any action!`, 18 | }; 19 | } 20 | 21 | return { 22 | pass: !!actionType, 23 | message: `Expected to dispatch some action, but the method will not dispatch any action!`, 24 | }; 25 | 26 | } 27 | 28 | export function toDispatchAction(): jasmine.CustomMatcher { 29 | return {compare}; 30 | } 31 | -------------------------------------------------------------------------------- /src/plugins/karma/matchers/to-reduce-on.spec.ts: -------------------------------------------------------------------------------- 1 | import { ReduxActionDecoratorForMethod, ReduxReducerDecoratorForMethod } from '@harmowatch/redux-decorators'; 2 | import { toReduceOn } from './to-reduce-on'; 3 | 4 | class TestAction { 5 | 6 | @ReduxActionDecoratorForMethod() 7 | foo() { 8 | } 9 | 10 | } 11 | 12 | class TestReducer { 13 | 14 | @ReduxReducerDecoratorForMethod('foo/123') 15 | @ReduxReducerDecoratorForMethod(TestAction.prototype.foo) 16 | reduceFoo(state: T): T { 17 | return state; 18 | } 19 | 20 | } 21 | 22 | describe('plugins/karma/matchers', () => { 23 | 24 | describe('toReduceOn', () => { 25 | 26 | function testCompare({desc, reducerFunction, actions, willPass, errorMessage = ''}) { 27 | 28 | describe(desc, () => { 29 | 30 | let result; 31 | beforeEach(() => result = toReduceOn().compare.apply(null, [ reducerFunction ].concat(actions))); 32 | 33 | it(willPass ? 'will pass' : 'will fail', () => { 34 | expect(result.pass).toBe(willPass); 35 | }); 36 | 37 | if (errorMessage.length > 0) { 38 | it('displays the correct error message', () => { 39 | expect(result.message).toBe(errorMessage); 40 | }); 41 | } 42 | 43 | }); 44 | } 45 | 46 | testCompare({ 47 | desc: 'The reducer listens to all actions, in the specified order', 48 | willPass: true, 49 | reducerFunction: TestReducer.prototype.reduceFoo, 50 | actions: [ 51 | 'foo/123', 52 | TestAction.prototype.foo, 53 | ] 54 | }); 55 | 56 | testCompare({ 57 | desc: 'The reducer listens to all actions, in a different order', 58 | willPass: true, 59 | reducerFunction: TestReducer.prototype.reduceFoo, 60 | actions: [ 61 | TestAction.prototype.foo, 62 | 'foo/123', 63 | ] 64 | }); 65 | 66 | testCompare({ 67 | desc: 'The reducer listens to more actions than specified', 68 | willPass: true, 69 | reducerFunction: TestReducer.prototype.reduceFoo, 70 | actions: [ 71 | 'foo/123', 72 | ] 73 | }); 74 | 75 | testCompare({ 76 | desc: 'The reducer does not listen to the given action', 77 | errorMessage: 'Expected the reducer to listen for action "bar/456", but it doesn\'t!', 78 | willPass: false, 79 | reducerFunction: TestReducer.prototype.reduceFoo, 80 | actions: [ 81 | 'bar/456', 82 | ], 83 | }); 84 | 85 | testCompare({ 86 | desc: 'The reducer does not listen to one of the specified actions', 87 | willPass: false, 88 | reducerFunction: TestReducer.prototype.reduceFoo, 89 | actions: [ 'foo/123', 'bar/456' ], 90 | errorMessage: [ 91 | 'Expected the reducer to listen for the actions:', 92 | '- "foo/123"', 93 | '- "bar/456"', 94 | '', 95 | 'But it did not listen to:', 96 | '- "bar/456"', 97 | ].join('\n'), 98 | }); 99 | 100 | testCompare({ 101 | desc: 'The reducer does not listen to any of the specified actions', 102 | willPass: false, 103 | reducerFunction: TestReducer.prototype.reduceFoo, 104 | actions: [ 'bar/123', 'bar/456' ], 105 | errorMessage: [ 106 | 'Expected the reducer to listen for the actions:', 107 | '- "bar/123"', 108 | '- "bar/456"', 109 | '', 110 | 'But it did not listen to:', 111 | '- "bar/123"', 112 | '- "bar/456"', 113 | ].join('\n'), 114 | }); 115 | 116 | }); 117 | 118 | }); 119 | 120 | -------------------------------------------------------------------------------- /src/plugins/karma/matchers/to-reduce-on.ts: -------------------------------------------------------------------------------- 1 | import { ReduxActionDispatcher, ReduxActionFunction, ReduxReducerDecorator } from '@harmowatch/redux-decorators'; 2 | 3 | function printList(actions: string[]) { 4 | 5 | return actions 6 | .map(action => `- "${action}"`) 7 | .join('\n'); 8 | 9 | } 10 | 11 | export function compare(actual: Function, ...expected: ReduxActionFunction[]): jasmine.CustomMatcherResult { 12 | 13 | const expectedActionTypes = expected.map(ReduxActionDispatcher.getType); 14 | 15 | const givenActionTypes = [] 16 | .concat(ReduxReducerDecorator.get(actual)) 17 | .map(type => ReduxActionDispatcher.getType(type)); 18 | 19 | const actionsNotListeningTo = expectedActionTypes 20 | .filter(type => !givenActionTypes.includes(type)); 21 | 22 | const pass = actionsNotListeningTo.length === 0; 23 | 24 | if (expectedActionTypes.length === 1) { 25 | return { 26 | pass, 27 | message: `Expected the reducer to listen for action "${expectedActionTypes[ 0 ]}", but it doesn\'t!`, 28 | }; 29 | } else if (expectedActionTypes.length > 1) { 30 | return { 31 | pass, 32 | message: ` 33 | Expected the reducer to listen for the actions: 34 | ${printList(expectedActionTypes)} 35 | 36 | But it did not listen to: 37 | ${printList(actionsNotListeningTo)} 38 | `.trim(), 39 | }; 40 | } 41 | 42 | } 43 | 44 | export function toReduceOn(): jasmine.CustomMatcher { 45 | return {compare}; 46 | } 47 | -------------------------------------------------------------------------------- /src/plugins/karma/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var copyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | module.exports = { 6 | context: __dirname, 7 | resolve: { 8 | extensions: ['.ts', '.tsx', '.js', '.jsx'] 9 | }, 10 | // devtool: debug ? "inline-sourcemap" : null, 11 | entry: "./matchers/index.ts", 12 | output: { 13 | path: path.resolve('dist/plugins/karma'), 14 | filename: "bundle.js" 15 | }, 16 | mode: 'development', 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.ts?$/, 21 | use: 'awesome-typescript-loader' 22 | } 23 | ] 24 | }, 25 | plugins: [ 26 | new webpack.DefinePlugin({ 27 | 'process.env.NODE_ENV': JSON.stringify('production'), 28 | }), 29 | new webpack.LoaderOptionsPlugin({ 30 | minimize: true, 31 | debug: false 32 | }), 33 | new webpack.ContextReplacementPlugin( 34 | // The (\\|\/) piece accounts for path separators in *nix and Windows 35 | /angular(\\|\/)core(\\|\/)(@angular|esm5)/, 36 | path.resolve(__dirname, '../src') 37 | ), 38 | copyWebpackPlugin([ 39 | 'index.js', 40 | 'index.d.ts', 41 | ]) 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | import 'core-js/es6/symbol'; 23 | import 'core-js/es6/object'; 24 | import 'core-js/es7/object'; 25 | import 'core-js/es6/function'; 26 | import 'core-js/es6/parse-int'; 27 | import 'core-js/es6/parse-float'; 28 | import 'core-js/es6/number'; 29 | import 'core-js/es6/math'; 30 | import 'core-js/es6/string'; 31 | import 'core-js/es6/date'; 32 | import 'core-js/es6/array'; 33 | import 'core-js/es6/regexp'; 34 | import 'core-js/es6/map'; 35 | import 'core-js/es6/weak-map'; 36 | import 'core-js/es6/set'; 37 | 38 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 39 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 40 | 41 | /** IE10 and IE11 requires the following for the Reflect API. */ 42 | import 'core-js/es6/reflect'; 43 | 44 | 45 | /** Evergreen browsers require these. **/ 46 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 47 | import 'core-js/es7/reflect'; 48 | 49 | 50 | /** 51 | * Required to support Web Animations `@angular/platform-browser/animations`. 52 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | 57 | 58 | /*************************************************************************************************** 59 | * Zone JS is required by Angular itself. 60 | */ 61 | import 'zone.js/dist/zone'; // Included with Angular CLI. 62 | 63 | 64 | 65 | /*************************************************************************************************** 66 | * APPLICATION IMPORTS 67 | */ 68 | 69 | /** 70 | * Date, currency, decimal and percent pipes. 71 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 72 | */ 73 | // import 'intl'; // Run `npm install --save intl`. 74 | /** 75 | * Need to import at least one locale-data with intl. 76 | */ 77 | // import 'intl/locale-data/jsonp/en'; 78 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | .container { 3 | margin-top: 20px; 4 | } 5 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/test/test-bed.spec.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from '@angular/core'; 2 | import { async, TestBed } from '@angular/core/testing'; 3 | import { VehicleModule } from './vehicle/vehicle.module'; 4 | import { ReduxModule } from '../harmowatch/ngx-redux-core/redux.module'; 5 | 6 | export function initTestingModule() { 7 | 8 | const testBed = TestBed.configureTestingModule({ 9 | imports: [ 10 | ReduxModule.forRoot(), 11 | VehicleModule, 12 | ], 13 | }); 14 | 15 | 16 | testBed.get(Injector); 17 | 18 | } 19 | 20 | 21 | export function describeIntegrationTest(desc: string, suite: () => any) { 22 | 23 | describe('Integration Test', () => { 24 | 25 | beforeEach(async(() => initTestingModule())); 26 | describe(desc, suite); 27 | 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/test/vehicle/bike/bike.actions.provider.spec.ts: -------------------------------------------------------------------------------- 1 | import {take} from 'rxjs/operators'; 2 | 3 | import { describeIntegrationTest } from '../../test-bed.spec'; 4 | import { TestBed } from '@angular/core/testing'; 5 | import { BikeActionsProvider } from './bike.actions.provider'; 6 | import { ReduxStore } from '../../../harmowatch/ngx-redux-core/tokens/redux-store.token'; 7 | import { Store } from 'redux'; 8 | import { BikeState } from './bike.state'; 9 | import { VehicleStateProvider } from '../vehicle.state.provider'; 10 | 11 | describeIntegrationTest('BikeActions', () => { 12 | 13 | let bikeActions: BikeActionsProvider; 14 | let vehicleStateProvider: VehicleStateProvider; 15 | let store: Store<{ vehicle: { bike: BikeState } }>; 16 | 17 | beforeEach(() => { 18 | store = TestBed.get(ReduxStore); 19 | bikeActions = TestBed.get(BikeActionsProvider); 20 | vehicleStateProvider = TestBed.get(VehicleStateProvider); 21 | }); 22 | 23 | it('calls "addLicensePlateSuccess" after the state was reduced', done => { 24 | const licensePlate = 'IK-184n2-43'; 25 | 26 | BikeActionsProvider.addLicensePlateSuccessEmitter.pipe(take(1)).subscribe(() => { 27 | vehicleStateProvider.getState().then(state => { 28 | expect(state.bike.licensePlates).toEqual([ licensePlate ]); 29 | done(); 30 | }); 31 | }); 32 | 33 | vehicleStateProvider.getState().then(state => { 34 | bikeActions.addLicensePlate(licensePlate); 35 | expect(state.bike.licensePlates.length).toEqual(0); 36 | }); 37 | 38 | }); 39 | 40 | }); 41 | 42 | -------------------------------------------------------------------------------- /src/test/vehicle/bike/bike.actions.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ReduxAction } from '../../../harmowatch/ngx-redux-core/decorators/index'; 3 | import { Subject } from 'rxjs'; 4 | 5 | @Injectable() 6 | export class BikeActionsProvider { 7 | 8 | public static readonly addLicensePlateSuccessEmitter = new Subject(); 9 | 10 | 11 | @ReduxAction({ 12 | type: 'BikeActionsProvider://add-license-plate', 13 | onDispatchSuccess: BikeActionsProvider.prototype.addLicensePlateSuccess, 14 | }) 15 | public addLicensePlate(licensePlate: string) { 16 | return Promise.resolve(licensePlate); 17 | } 18 | 19 | public addLicensePlateSuccess() { 20 | BikeActionsProvider.addLicensePlateSuccessEmitter.next(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/vehicle/bike/bike.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { initTestingModule } from '../../test-bed.spec'; 3 | import { BikeState } from './bike.state'; 4 | import { VehicleStateProvider } from '../vehicle.state.provider'; 5 | import { VehicleActions } from '../vehicle.actions.provider'; 6 | import { BikeActionsProvider } from './bike.actions.provider'; 7 | 8 | describe('test/integration/bike/reducer', () => { 9 | 10 | beforeEach(async(() => initTestingModule())); 11 | 12 | let bikeState: BikeState; 13 | 14 | beforeEach(async(() => { 15 | TestBed.get(VehicleStateProvider).select('bike') 16 | .subscribe(v => bikeState = v); 17 | })); 18 | 19 | describe('setMaxVelocity()', () => { 20 | 21 | beforeEach(async(() => { 22 | TestBed.get(VehicleActions).setMaxVelocity(50); 23 | })); 24 | 25 | it('sets the velocity to 50', () => { 26 | expect(bikeState).toEqual({ 27 | ...VehicleStateProvider.initialState.bike, 28 | velocity: { 29 | ...VehicleStateProvider.initialState.bike.velocity, 30 | maximum: 50 31 | }, 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | describe('setDefaultVelocity()', () => { 38 | 39 | beforeEach(async(() => { 40 | TestBed.get(VehicleActions).setDefaultVelocity(); 41 | })); 42 | 43 | it('sets the default velocity', () => { 44 | expect(bikeState).toEqual({ 45 | ...VehicleStateProvider.initialState.bike, 46 | velocity: { 47 | ...VehicleStateProvider.initialState.bike.velocity, 48 | maximum: 25 49 | }, 50 | }); 51 | }); 52 | 53 | }); 54 | 55 | 56 | describe('addLicensePlate()', () => { 57 | 58 | beforeEach(async(() => { 59 | TestBed.get(BikeActionsProvider).addLicensePlate('SO-ME-4321'); 60 | })); 61 | 62 | it('sets the default velocity', () => { 63 | expect(bikeState).toEqual({ 64 | ...VehicleStateProvider.initialState.car, 65 | licensePlates: [ 'SO-ME-4321' ], 66 | }); 67 | }); 68 | 69 | }); 70 | 71 | }); 72 | 73 | -------------------------------------------------------------------------------- /src/test/vehicle/bike/bike.reducer.ts: -------------------------------------------------------------------------------- 1 | import { ReduxReducer } from '../../../harmowatch/ngx-redux-core/decorators/index'; 2 | import { VehicleActions } from '../vehicle.actions.provider'; 3 | import { VehicleState } from '../vehicle.state.provider'; 4 | import { ReduxActionWithPayload } from '../../../harmowatch/ngx-redux-core/interfaces/redux-action-with-payload.interface'; 5 | import { getActionType } from '../../../harmowatch/ngx-redux-core/index'; 6 | import { BikeActionsProvider } from './bike.actions.provider'; 7 | 8 | export class BikeReducer { 9 | 10 | @ReduxReducer(VehicleActions.prototype.setMaxVelocity) 11 | @ReduxReducer(VehicleActions.prototype.setDefaultVelocity) 12 | public setMaxVelocity(state: VehicleState, action: ReduxActionWithPayload): VehicleState { 13 | 14 | let maximum = 25; 15 | 16 | if (action.type === getActionType(VehicleActions.prototype.setMaxVelocity)) { 17 | maximum = action.payload as number; 18 | } 19 | 20 | return { 21 | ...state, 22 | bike: { 23 | ...state.bike, 24 | velocity: { 25 | ...state.bike.velocity, 26 | maximum, 27 | } 28 | } 29 | }; 30 | 31 | } 32 | 33 | 34 | @ReduxReducer(BikeActionsProvider.prototype.addLicensePlate) 35 | public addLicensePlate(state: VehicleState, action: ReduxActionWithPayload): VehicleState { 36 | 37 | return { 38 | ...state, 39 | bike: { 40 | ...state.bike, 41 | licensePlates: state.bike.licensePlates.concat(action.payload), 42 | } 43 | }; 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/vehicle/bike/bike.state.ts: -------------------------------------------------------------------------------- 1 | export interface BikeState { 2 | velocity: { 3 | maximum: number, 4 | }; 5 | licensePlates: string[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/test/vehicle/car/car.actions.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ReduxAction, ReduxActionContext } from '../../../harmowatch/ngx-redux-core/decorators/index'; 3 | import { from } from 'rxjs'; 4 | 5 | @Injectable() 6 | @ReduxActionContext({prefix: 'CarActionsProvider://'}) 7 | export class CarActionsProvider { 8 | 9 | @ReduxAction({type: 'add-license-plate'}) 10 | public addLicensePlate(licensePlate: string) { 11 | return from([ 'foo-123', licensePlate ]); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/vehicle/car/car.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { describeIntegrationTest } from '../../test-bed.spec'; 3 | import { CarState } from './car.state'; 4 | import { VehicleStateProvider } from '../vehicle.state.provider'; 5 | import { VehicleActions } from '../vehicle.actions.provider'; 6 | import { CarActionsProvider } from './car.actions.provider'; 7 | 8 | describeIntegrationTest('CarReducer', () => { 9 | 10 | let carState: CarState; 11 | 12 | beforeEach(async(() => { 13 | TestBed.get(VehicleStateProvider).select('car') 14 | .subscribe(v => carState = v); 15 | })); 16 | 17 | describe('setMaxVelocity()', () => { 18 | 19 | beforeEach(async(() => { 20 | TestBed.get(VehicleActions).setMaxVelocity(50); 21 | })); 22 | 23 | it('sets the velocity to 50', () => { 24 | expect(carState).toEqual({ 25 | ...VehicleStateProvider.initialState.car, 26 | velocity: { 27 | ...VehicleStateProvider.initialState.car.velocity, 28 | maximum: 50 29 | }, 30 | }); 31 | }); 32 | 33 | }); 34 | 35 | describe('setDefaultVelocity()', () => { 36 | 37 | beforeEach(async(() => { 38 | TestBed.get(VehicleActions).setDefaultVelocity(); 39 | })); 40 | 41 | it('sets the default velocity', () => { 42 | expect(carState).toEqual({ 43 | ...VehicleStateProvider.initialState.car, 44 | velocity: { 45 | ...VehicleStateProvider.initialState.car.velocity, 46 | maximum: 10 47 | }, 48 | }); 49 | }); 50 | 51 | }); 52 | 53 | describe('addLicensePlate()', () => { 54 | 55 | beforeEach(async(() => { 56 | TestBed.get(CarActionsProvider).addLicensePlate('SO-ME-1234'); 57 | })); 58 | 59 | it('sets the default velocity', () => { 60 | expect(carState).toEqual({ 61 | ...VehicleStateProvider.initialState.car, 62 | licensePlates: [ 'SO-ME-1234' ], 63 | }); 64 | }); 65 | 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /src/test/vehicle/car/car.reducer.ts: -------------------------------------------------------------------------------- 1 | import { ReduxReducer } from '../../../harmowatch/ngx-redux-core/decorators/index'; 2 | import { VehicleActions } from '../vehicle.actions.provider'; 3 | import { VehicleState } from '../vehicle.state.provider'; 4 | import { ReduxActionWithPayload } from '../../../harmowatch/ngx-redux-core/interfaces/redux-action-with-payload.interface'; 5 | import { getActionType } from '../../../harmowatch/ngx-redux-core/index'; 6 | 7 | export class CarReducer { 8 | 9 | @ReduxReducer([ 10 | 'VehicleActions://setDefaultVelocity', 11 | VehicleActions.prototype.setMaxVelocity 12 | ]) 13 | public setMaxVelocity(state: VehicleState, action: ReduxActionWithPayload): VehicleState { 14 | 15 | let maximum = 10; 16 | 17 | if (action.type === getActionType(VehicleActions.prototype.setMaxVelocity)) { 18 | maximum = action.payload as number; 19 | } 20 | 21 | return { 22 | ...state, 23 | car: { 24 | ...state.car, 25 | velocity: { 26 | ...state.car.velocity, 27 | maximum, 28 | } 29 | } 30 | }; 31 | } 32 | 33 | @ReduxReducer('CarActionsProvider://add-license-plate') 34 | public addLicensePlate(state: VehicleState, action: ReduxActionWithPayload): VehicleState { 35 | 36 | return { 37 | ...state, 38 | car: { 39 | ...state.car, 40 | licensePlates: state.car.licensePlates.concat(action.payload), 41 | } 42 | }; 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/vehicle/car/car.state.ts: -------------------------------------------------------------------------------- 1 | export interface CarState { 2 | velocity: { 3 | maximum: number, 4 | }; 5 | licensePlates: string[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/test/vehicle/vehicle.actions.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ReduxAction, ReduxActionContext } from '../../harmowatch/ngx-redux-core/decorators/index'; 3 | 4 | @Injectable() 5 | @ReduxActionContext({prefix: 'VehicleActions://'}) 6 | export class VehicleActions { 7 | 8 | @ReduxAction() 9 | public setMaxVelocity(max: number): number { 10 | return max; 11 | } 12 | 13 | @ReduxAction() 14 | public setDefaultVelocity(): void { 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/vehicle/vehicle.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ReduxModule } from '../../harmowatch/ngx-redux-core/redux.module'; 3 | import { VehicleStateProvider } from './vehicle.state.provider'; 4 | import { BikeReducer } from './bike/bike.reducer'; 5 | import { CarReducer } from './car/car.reducer'; 6 | import { VehicleActions } from './vehicle.actions.provider'; 7 | import { BikeActionsProvider } from './bike/bike.actions.provider'; 8 | import { CarActionsProvider } from './car/car.actions.provider'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | ReduxModule.forChild({ 13 | state: { 14 | provider: VehicleStateProvider, 15 | reducers: [ 16 | BikeReducer, 17 | CarReducer, 18 | ], 19 | } 20 | }), 21 | ], 22 | providers: [ 23 | VehicleActions, 24 | BikeActionsProvider, 25 | CarActionsProvider, 26 | VehicleStateProvider, 27 | ] 28 | }) 29 | export class VehicleModule { 30 | } 31 | -------------------------------------------------------------------------------- /src/test/vehicle/vehicle.spec.ts: -------------------------------------------------------------------------------- 1 | // import { async, TestBed } from '@angular/core/testing'; 2 | // import { configureTestingModule } from '../test-bed'; 3 | // import { BikeActionsProvider } from './bike/bike.actions.provider'; 4 | // import { BikeState } from './bike/bike.state'; 5 | // 6 | // describe('Integration Test', () => { 7 | // 8 | // let testBikeActions: BikeActionsProvider; 9 | // let testBikeStateProvider: TestBikeStateProvider; 10 | // let testBikeStateProviderRootSelectSpy: jasmine.Spy; 11 | // let testBikeStateProviderMenuOpenSelectSpy: jasmine.Spy; 12 | // 13 | // function resetSpies() { 14 | // testBikeStateProviderRootSelectSpy.calls.reset(); 15 | // testBikeStateProviderMenuOpenSelectSpy.calls.reset(); 16 | // } 17 | // 18 | // beforeEach(async(() => { 19 | // configureTestingModule(); 20 | // testBikeStateProviderRootSelectSpy = jasmine.createSpy('fooState/'); 21 | // testBikeStateProviderMenuOpenSelectSpy = jasmine.createSpy('fooState/menu/open'); 22 | // 23 | // testBikeActions = TestBed.get(BikeActions); 24 | // testBikeStateProvider = TestBed.get(TestBikeStateProvider); 25 | // })); 26 | // 27 | // beforeEach(async(() => { 28 | // testBikeStateProvider.select().subscribe(testBikeStateProviderRootSelectSpy); 29 | // testBikeStateProvider.select('menu/open').subscribe(testBikeStateProviderMenuOpenSelectSpy); 30 | // })); 31 | // 32 | // it('is initialized well', () => { 33 | // expect(testBikeStateProviderRootSelectSpy).toHaveBeenCalledTimes(1); 34 | // expect(testBikeStateProviderRootSelectSpy).toHaveBeenCalledWith(TestBikeStateProvider.initialState); 35 | // }); 36 | // 37 | // describe('todo add', () => { 38 | // 39 | // beforeEach(async(() => { 40 | // resetSpies(); 41 | // testBikeActions.addBike('new foo'); 42 | // })); 43 | // 44 | // it('will add a foo', () => { 45 | // 46 | // const expectedState: BikeState = { 47 | // ...TestBikeStateProvider.initialState, 48 | // list: { 49 | // ...TestBikeStateProvider.initialState.list, 50 | // items: TestBikeStateProvider.initialState.list.items.concat('new foo'), 51 | // } 52 | // }; 53 | // 54 | // expect(testBikeStateProviderMenuOpenSelectSpy).not.toHaveBeenCalled(); 55 | // 56 | // expect(testBikeStateProviderRootSelectSpy).toHaveBeenCalledTimes(1); 57 | // expect(testBikeStateProviderRootSelectSpy).toHaveBeenCalledWith(expectedState); 58 | // 59 | // }); 60 | // 61 | // }); 62 | // 63 | // }); 64 | -------------------------------------------------------------------------------- /src/test/vehicle/vehicle.state.provider.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { describeIntegrationTest } from '../test-bed.spec'; 3 | import { VehicleStateProvider } from './vehicle.state.provider'; 4 | 5 | describeIntegrationTest('BikeReducer', () => { 6 | 7 | describe('getState()', () => { 8 | 9 | it('returns the correct state', done => { 10 | TestBed.get(VehicleStateProvider).getState().then(state => { 11 | expect(state).toEqual(VehicleStateProvider.initialState); 12 | done(); 13 | }); 14 | }); 15 | 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /src/test/vehicle/vehicle.state.provider.ts: -------------------------------------------------------------------------------- 1 | import { BikeState } from './bike/bike.state'; 2 | import { CarState } from './car/car.state'; 3 | import { ReduxState } from '../../harmowatch/ngx-redux-core/decorators/index'; 4 | import { ReduxStateProvider } from '../../harmowatch/ngx-redux-core/providers/redux-state.provider'; 5 | 6 | export interface VehicleState { 7 | bike: BikeState; 8 | car: CarState; 9 | } 10 | 11 | @ReduxState({name: 'vehicle'}) 12 | export class VehicleStateProvider extends ReduxStateProvider { 13 | 14 | public static readonly initialState = { 15 | bike: { 16 | velocity: { 17 | maximum: -1 18 | }, 19 | licensePlates: [], 20 | }, 21 | car: { 22 | velocity: { 23 | maximum: -1 24 | }, 25 | licensePlates: [], 26 | }, 27 | }; 28 | 29 | getInitialState(): VehicleState { 30 | return VehicleStateProvider.initialState; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "plugins/karma/matchers", 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts", 15 | "polyfills.ts" 16 | ], 17 | "include": [ 18 | "**/*.spec.ts", 19 | "**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ], 18 | "module": "es2015", 19 | "baseUrl": "./" 20 | } 21 | } -------------------------------------------------------------------------------- /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 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs/Rx" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | { 35 | "order": [ 36 | "static-field", 37 | "instance-field", 38 | "static-method", 39 | "instance-method" 40 | ] 41 | } 42 | ], 43 | "no-arg": true, 44 | "no-bitwise": true, 45 | "no-console": [ 46 | true, 47 | "debug", 48 | "info", 49 | "time", 50 | "timeEnd", 51 | "trace" 52 | ], 53 | "no-construct": true, 54 | "no-debugger": true, 55 | "no-duplicate-super": true, 56 | "no-empty": false, 57 | "no-empty-interface": true, 58 | "no-eval": true, 59 | "no-inferrable-types": [ 60 | true, 61 | "ignore-params" 62 | ], 63 | "no-misused-new": true, 64 | "no-non-null-assertion": true, 65 | "no-shadowed-variable": true, 66 | "no-string-literal": false, 67 | "no-string-throw": true, 68 | "no-switch-case-fall-through": true, 69 | "no-trailing-whitespace": true, 70 | "no-unnecessary-initializer": true, 71 | "no-unused-expression": true, 72 | "no-use-before-declare": true, 73 | "no-var-keyword": true, 74 | "object-literal-sort-keys": false, 75 | "one-line": [ 76 | true, 77 | "check-open-brace", 78 | "check-catch", 79 | "check-else", 80 | "check-whitespace" 81 | ], 82 | "prefer-const": true, 83 | "quotemark": [ 84 | true, 85 | "single" 86 | ], 87 | "radix": true, 88 | "semicolon": [ 89 | true, 90 | "always" 91 | ], 92 | "triple-equals": [ 93 | true, 94 | "allow-null-check" 95 | ], 96 | "typedef-whitespace": [ 97 | true, 98 | { 99 | "call-signature": "nospace", 100 | "index-signature": "nospace", 101 | "parameter": "nospace", 102 | "property-declaration": "nospace", 103 | "variable-declaration": "nospace" 104 | } 105 | ], 106 | "unified-signatures": true, 107 | "variable-name": false, 108 | "whitespace": [ 109 | true, 110 | "check-branch", 111 | "check-decl", 112 | "check-operator", 113 | "check-separator", 114 | "check-type" 115 | ], 116 | "directive-selector": [ 117 | true, 118 | "attribute", 119 | "app", 120 | "camelCase" 121 | ], 122 | "component-selector": [ 123 | true, 124 | "element", 125 | "app", 126 | "kebab-case" 127 | ], 128 | "use-input-property-decorator": true, 129 | "use-output-property-decorator": true, 130 | "use-host-property-decorator": true, 131 | "no-input-rename": true, 132 | "no-output-rename": true, 133 | "use-life-cycle-interface": true, 134 | "use-pipe-transform-interface": true, 135 | "component-class-suffix": true, 136 | "directive-class-suffix": true 137 | } 138 | } 139 | --------------------------------------------------------------------------------