├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── .huskyrc.json ├── .prettierignore ├── .prettierrc ├── .yarnrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── docs ├── .nojekyll ├── README.md ├── _navbar.md ├── form │ └── README.md ├── index.html ├── router │ └── README.md ├── store │ ├── README.md │ └── articles │ │ ├── cookbooks.md │ │ ├── images │ │ ├── counter-hooked.png │ │ ├── counter-unhooked.png │ │ ├── devtools.png │ │ └── startup.png │ │ └── intro-tutorial.md └── vendor │ ├── docsify.js │ └── themes │ └── vue.css ├── jest.config.js ├── jest ├── file.mock.ts └── tests-setup.ts ├── lerna.json ├── package.json ├── packages ├── example-app │ ├── .editorconfig │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── angular.json │ ├── e2e │ │ └── tsconfig.e2e.json │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── animals │ │ │ │ ├── animal-list │ │ │ │ │ ├── component.css │ │ │ │ │ ├── component.html │ │ │ │ │ ├── component.spec.ts │ │ │ │ │ └── component.ts │ │ │ │ ├── animal │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── component.css │ │ │ │ │ ├── component.html │ │ │ │ │ ├── component.spec.ts │ │ │ │ │ ├── component.ts │ │ │ │ │ └── reducers.ts │ │ │ │ ├── api │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── epics.ts │ │ │ │ │ ├── reducer.ts │ │ │ │ │ └── service.ts │ │ │ │ ├── model.ts │ │ │ │ └── module.ts │ │ │ ├── component.css │ │ │ ├── component.html │ │ │ ├── component.spec.ts │ │ │ ├── component.ts │ │ │ ├── core │ │ │ │ ├── counter │ │ │ │ │ ├── component.html │ │ │ │ │ └── component.ts │ │ │ │ ├── error-well │ │ │ │ │ ├── component.css │ │ │ │ │ ├── component.html │ │ │ │ │ └── component.ts │ │ │ │ ├── module.ts │ │ │ │ └── spinner │ │ │ │ │ ├── component.css │ │ │ │ │ ├── component.html │ │ │ │ │ └── component.ts │ │ │ ├── elephants │ │ │ │ ├── module.ts │ │ │ │ ├── page.html │ │ │ │ ├── page.spec.ts │ │ │ │ └── page.ts │ │ │ ├── feedback │ │ │ │ ├── module.ts │ │ │ │ ├── page.css │ │ │ │ ├── page.html │ │ │ │ ├── page.spec.ts │ │ │ │ └── page.ts │ │ │ ├── lions │ │ │ │ ├── module.ts │ │ │ │ ├── page.html │ │ │ │ ├── page.spec.ts │ │ │ │ └── page.ts │ │ │ ├── module.ts │ │ │ ├── routes.ts │ │ │ └── store │ │ │ │ ├── epics.ts │ │ │ │ ├── model.ts │ │ │ │ ├── module.spec.ts │ │ │ │ ├── module.ts │ │ │ │ └── reducers.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.css │ │ └── tsconfig.app.json │ └── tsconfig.json ├── form │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── ng-package.json │ ├── package.json │ └── src │ │ ├── compose-reducers.spec.ts │ │ ├── compose-reducers.ts │ │ ├── configure.ts │ │ ├── connect-array │ │ ├── connect-array-template.ts │ │ ├── connect-array.directive.ts │ │ └── connect-array.module.ts │ │ ├── connect │ │ ├── connect-base.ts │ │ ├── connect-reactive.ts │ │ ├── connect.directive.ts │ │ ├── connect.module.ts │ │ └── connect.spec.ts │ │ ├── exports.spec.ts │ │ ├── form-exception.ts │ │ ├── form-reducer.ts │ │ ├── form-store.ts │ │ ├── index.ts │ │ ├── module.ts │ │ ├── shims.ts │ │ ├── state.ts │ │ └── tests.utilities.ts ├── router │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── ng-package.json │ ├── package.json │ └── src │ │ ├── actions.ts │ │ ├── exports.spec.ts │ │ ├── index.ts │ │ ├── module.ts │ │ ├── reducer.ts │ │ └── router.ts └── store │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── articles │ ├── action-creator-service.md │ ├── di-middleware.md │ ├── epics.md │ ├── fractal-store.md │ ├── images │ │ ├── counter-hooked.png │ │ ├── counter-unhooked.png │ │ ├── devtools.png │ │ └── startup.png │ ├── immutable-js.md │ ├── intro-tutorial.md │ ├── quickstart.md │ ├── redux-dev-tools.md │ ├── select-pattern.md │ └── strongly-typed-reducers.md │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── components │ │ ├── dev-tools.ts │ │ ├── fractal-reducer-map.ts │ │ ├── ng-redux.ts │ │ ├── observable-store.ts │ │ ├── root-store.spec.ts │ │ ├── root-store.ts │ │ ├── selectors.spec.ts │ │ ├── selectors.ts │ │ ├── sub-store.spec.ts │ │ └── sub-store.ts │ ├── decorators │ │ ├── dispatch.spec.ts │ │ ├── dispatch.ts │ │ ├── helpers.ts │ │ ├── select.spec.ts │ │ ├── select.ts │ │ ├── with-sub-store.spec.ts │ │ └── with-sub-store.ts │ ├── exports.spec.ts │ ├── index.ts │ ├── ng-redux.module.ts │ └── utils │ │ ├── assert.ts │ │ ├── get-in.spec.ts │ │ ├── get-in.ts │ │ ├── set-in.spec.ts │ │ └── set-in.ts │ └── testing │ ├── dev-tools.mock.ts │ ├── index.ts │ ├── ng-package.json │ ├── ng-redux-testing.module.ts │ ├── ng-redux.mock.spec.ts │ ├── ng-redux.mock.ts │ ├── observable-store.mock.ts │ └── package.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | docker: 3 | - image: 'circleci/node:8-browsers' 4 | working_directory: ~/repo 5 | 6 | repo_cache: &repo_cache 7 | key: v1-repo-{{ .Environment.CIRCLE_SHA1 }} 8 | 9 | deps_cache: &deps_cache 10 | key: v1-dependencies-{{ checksum "yarn.lock" }} 11 | 12 | build_cache: &build_cache 13 | key: v1-build-{{ .Environment.CIRCLE_SHA1 }} 14 | 15 | version: 2 16 | jobs: 17 | checkout_code: 18 | <<: *defaults 19 | steps: 20 | - checkout 21 | - save_cache: 22 | <<: *repo_cache 23 | paths: 24 | - ~/repo 25 | 26 | install_dependencies: 27 | <<: *defaults 28 | steps: 29 | - restore_cache: *repo_cache 30 | - restore_cache: 31 | keys: 32 | - v1-dependencies-{{ checksum "yarn.lock" }} 33 | # fallback to using the latest cache if no exact match is found 34 | - v1-dependencies- 35 | 36 | - run: yarn install --frozen-lockfile 37 | 38 | - save_cache: 39 | paths: 40 | - node_modules 41 | - packages/example-app/node_modules 42 | - packages/form/node_modules 43 | - packages/router/node_modules 44 | - packages/store/node_modules 45 | <<: *deps_cache 46 | 47 | lint: 48 | <<: *defaults 49 | steps: 50 | - restore_cache: *repo_cache 51 | - restore_cache: *deps_cache 52 | - run: yarn lint 53 | 54 | test: 55 | <<: *defaults 56 | steps: 57 | - restore_cache: *repo_cache 58 | - restore_cache: *deps_cache 59 | - run: yarn test:ci 60 | - store_test_results: 61 | path: ./coverage 62 | - store_artifacts: 63 | path: ./coverage 64 | 65 | build: 66 | <<: *defaults 67 | steps: 68 | - restore_cache: *repo_cache 69 | - restore_cache: *deps_cache 70 | - run: yarn build 71 | - save_cache: 72 | paths: 73 | - packages/example-app/dist 74 | - packages/form/dist 75 | - packages/router/dist 76 | - packages/store/dist 77 | <<: *build_cache 78 | 79 | release_canary: 80 | <<: *defaults 81 | steps: 82 | - restore_cache: *repo_cache 83 | - restore_cache: *deps_cache 84 | - restore_cache: *build_cache 85 | - run: 86 | name: Authenticate with registry 87 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc 88 | - run: npm run release:canary -- --yes 89 | 90 | release_stable: 91 | <<: *defaults 92 | steps: 93 | - restore_cache: *repo_cache 94 | - restore_cache: *deps_cache 95 | - restore_cache: *build_cache 96 | - run: 97 | name: Authenticate with registry 98 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc 99 | - run: yarn release:stable:ci --yes 100 | 101 | workflows: 102 | version: 2 103 | build_and_deploy: 104 | jobs: 105 | - checkout_code: 106 | filters: 107 | tags: 108 | only: /^v.*/ 109 | - install_dependencies: 110 | requires: 111 | - checkout_code 112 | filters: 113 | tags: 114 | only: /^v.*/ 115 | - lint: 116 | requires: 117 | - install_dependencies 118 | filters: 119 | tags: 120 | only: /^v.*/ 121 | - test: 122 | requires: 123 | - install_dependencies 124 | filters: 125 | tags: 126 | only: /^v.*/ 127 | - build: 128 | requires: 129 | - install_dependencies 130 | filters: 131 | tags: 132 | only: /^v.*/ 133 | - release_canary: 134 | requires: 135 | - lint 136 | - test 137 | - build 138 | filters: 139 | branches: 140 | only: master 141 | - release_stable: 142 | requires: 143 | - lint 144 | - test 145 | - build 146 | filters: 147 | tags: 148 | only: /^v.*/ 149 | branches: 150 | ignore: /.*/ 151 | -------------------------------------------------------------------------------- /.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 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | lerna-debug.log 5 | yarn-debug.log 6 | yarn-error.log 7 | .vscode 8 | .node-version 9 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --add.exact true 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Angular Redux 2 | 3 | ## Package Management 4 | 5 | This repo utilizes [Yarn Workspaces](https://yarnpkg.com/lang/en/docs/workspaces/) for package management. Please install and use [Yarn](https://yarnpkg.com/en/docs/getting-started) as your npm client for this project. The npm cli is not supported for package installation. 6 | 7 | ## Commit Message Guidelines 8 | 9 | We follow the [Conventional Commits](https://conventionalcommits.org/) guidelines. These are enforced through the use of [commitlint](http://marionebl.github.io/commitlint). If you would like a more interactive way of formatting your commit messages, run `yarn commit` once your changes are staged. 10 | 11 | ## Releases 12 | 13 | ## Canary Releases 14 | 15 | This repo is setup to automatically release canary builds for every commit that is pushed to master. In order to access those builds, run `npm install @angular-redux/store@canary` (or whichever package you are looking to use) 16 | 17 | ## Stable Releases 18 | 19 | For stable releases, the build and publishing is done automatically for CircleCI. If you have write access to the repo, run the following steps to automatically release a new version to `latest` 20 | 21 | - Pull down the latest version of master to your local machine 22 | - Run `yarn release:stable` 23 | 24 | The release commit will be automatically pushed to `master` where CircleCI will complete the remaining publishing steps. 25 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### This is a... 2 | 3 | - [ ] feature request 4 | - [ ] bug report 5 | - [ ] usage question 6 | 7 | ### What toolchain are you using for transpilation/bundling? 8 | 9 | - [ ] @angular/cli 10 | - [ ] Custom @ngTools/webpack 11 | - [ ] Raw `ngc` 12 | - [ ] SystemJS 13 | - [ ] Rollup 14 | - [ ] Other 15 | 16 | ### Environment 17 | 18 | NodeJS Version: 19 | Typescript Version: 20 | Angular Version: 21 | @angular-redux/store version: 22 | @angular/cli version: (if applicable) 23 | OS: 24 | 25 | ### Link to repo showing the issus 26 | 27 | (optional, but helps _a lot_) 28 | 29 | ### Expected Behaviour: 30 | 31 | ### Actual Behaviour: 32 | 33 | ### Stack Trace/Error Message: 34 | 35 | ### Additional Notes: 36 | 37 | (optional) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Angular Redux 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 | :warning: **This project is currently unmaintained** 2 | 3 | # Angular Redux 4 | 5 | [![CircleCI](https://circleci.com/gh/angular-redux/platform/tree/master.svg?style=svg)](https://circleci.com/gh/angular-redux/platform/tree/master) 6 | 7 | [Redux](https://redux.js.org/) bindings for [Angular](https://angular.io/) applications. 8 | 9 | ## Packages 10 | 11 | - [@angular-redux/store](packages/store) - Bindings between Redux and Angular 12 | - [@angular-redux/form](packages/form) - Bindings between Angular Forms and your Redux state 13 | - [@angular-redux/router](packages/router) - Bindings between Angular Router and your Redux state 14 | 15 | ## Examples 16 | 17 | - [Example Application](https://github.com/angular-redux/platform/blob/master/packages/example-app) 18 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/angular-redux/platform/tree/master.svg?style=svg)](https://circleci.com/gh/angular-redux/platform/tree/master) 2 | 3 | [Redux](https://redux.js.org/) bindings for [Angular](https://angular.io/) applications. 4 | 5 | # Packages 6 | 7 | - [@angular-redux/store](store/) - Bindings between Redux and Angular 8 | - [@angular-redux/form](form/) - Bindings between Angular Forms and your Redux state 9 | - [@angular-redux/router](router/) - Bindings between Angular Router and your Redux state 10 | 11 | # Examples 12 | 13 | - [Example Application](https://github.com/angular-redux/platform/blob/master/packages/example-app) 14 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | - [Store](store/) 2 | - [Form](form/) 3 | - [Router](router/) 4 | - [Changelog](changelog) 5 | - [Contributing](contributing) 6 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Redux 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/router/README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://img.shields.io/npm/v/@angular-redux/router.svg)](https://www.npmjs.com/package/@angular-redux/router) 2 | [![downloads per month](https://img.shields.io/npm/dm/@angular-redux/router.svg)](https://www.npmjs.com/package/@angular-redux/router) 3 | 4 | Bindings to connect @angular/router to @angular-redux/core 5 | 6 | # Setup 7 | 8 | 1. Use npm to install the bindings: 9 | 10 | ``` 11 | npm install @angular-redux/router --save 12 | ``` 13 | 14 | 2. Use the `routerReducer` when providing `Store`: 15 | 16 | ```ts 17 | import { combineReducers } from 'redux'; 18 | import { routerReducer } from '@angular-redux/router'; 19 | 20 | export default combineReducers({ 21 | // your reducers.. 22 | router: routerReducer, 23 | }); 24 | ``` 25 | 26 | 3. Add the bindings to your root module. 27 | 28 | ```ts 29 | import { NgModule } from '@angular/core'; 30 | import { NgReduxModule, NgRedux } from '@angular-redux/core'; 31 | import { NgReduxRouterModule, NgReduxRouter } from '@angular-redux/router'; 32 | import { RouterModule } from '@angular/router'; 33 | import { routes } from './routes'; 34 | 35 | @NgModule({ 36 | imports: [ 37 | RouterModule.forRoot(routes), 38 | NgReduxModule, 39 | NgReduxRouterModule.forRoot(), 40 | // ...your imports 41 | ], 42 | // Other stuff.. 43 | }) 44 | export class AppModule { 45 | constructor(ngRedux: NgRedux, ngReduxRouter: NgReduxRouter) { 46 | ngRedux.configureStore(/* args */); 47 | ngReduxRouter.initialize(/* args */); 48 | } 49 | } 50 | ``` 51 | 52 | # What if I use Immutable.js with my Redux store? 53 | 54 | When using a wrapper for your store's state, such as Immutable.js, you will need to change two things from the standard setup: 55 | 56 | 1. Provide your own reducer function that will receive actions of type `UPDATE_LOCATION` and return the payload merged into state. 57 | 2. Pass a selector to access the payload state and convert it to a JS object via the `selectLocationFromState` option on `NgReduxRouter`'s `initialize()`. 58 | 59 | These two hooks will allow you to store the state that this library uses in whatever format or wrapper you would like. 60 | 61 | # What if I have a different way of supplying the current URL of the page? 62 | 63 | Depending on your app's needs. It may need to supply the current URL of the page differently than directly 64 | through the router. This can be achieved by initializing the bindings with a second argument: `urlState$`. 65 | The `urlState$` argument lets you give `NgReduxRouter` an `Observable` of the current URL of the page. 66 | If this argument is not given to the bindings, it defaults to subscribing to the `@angular/router`'s events, and 67 | getting the URL from there. 68 | 69 | # Examples 70 | 71 | - [Example-app: An example of using @angular-redux/router along with the other companion packages.](https://github.com/angular-redux/platform/tree/master/packages/example-app) 72 | -------------------------------------------------------------------------------- /docs/store/articles/images/counter-hooked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/docs/store/articles/images/counter-hooked.png -------------------------------------------------------------------------------- /docs/store/articles/images/counter-unhooked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/docs/store/articles/images/counter-unhooked.png -------------------------------------------------------------------------------- /docs/store/articles/images/devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/docs/store/articles/images/devtools.png -------------------------------------------------------------------------------- /docs/store/articles/images/startup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/docs/store/articles/images/startup.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Jest JUnit Reporter config 4 | process.env.JEST_JUNIT_OUTPUT = './coverage/junit.xml'; 5 | 6 | module.exports = { 7 | globals: { 8 | __TRANSFORM_HTML__: true, 9 | }, 10 | transform: { 11 | '^.+\\.(ts|js|html)$': 12 | '/node_modules/jest-preset-angular/preprocessor.js', 13 | }, 14 | testMatch: ['**/packages/**/*.spec.{ts,js}'], 15 | moduleFileExtensions: ['ts', 'js', 'html', 'json'], 16 | setupTestFrameworkScriptFile: '/jest/tests-setup.ts', 17 | snapshotSerializers: [ 18 | '/node_modules/jest-preset-angular/AngularSnapshotSerializer.js', 19 | '/node_modules/jest-preset-angular/HTMLCommentSerializer.js', 20 | ], 21 | moduleNameMapper: { 22 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 23 | '/jest/file.mock.ts', 24 | }, 25 | modulePathIgnorePatterns: ['dist'], 26 | reporters: ['default', 'jest-junit'], 27 | collectCoverageFrom: [ 28 | '**/packages/**/*.{ts,js}', 29 | '!**/node_modules/**', 30 | '!**/dist/**', 31 | ], 32 | coverageReporters: ['lcov', 'text-summary'], 33 | }; 34 | -------------------------------------------------------------------------------- /jest/file.mock.ts: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /jest/tests-setup.ts: -------------------------------------------------------------------------------- 1 | // TODO: Should be able to remove this once dev dependencies are hoisted to the workspace 2 | // tslint:disable:no-implicit-dependencies 3 | import 'core-js/es6/reflect'; 4 | import 'core-js/es7/reflect'; 5 | import 'zone.js'; 6 | import 'zone.js/dist/async-test'; 7 | import 'zone.js/dist/fake-async-test'; 8 | import 'zone.js/dist/proxy'; 9 | import 'zone.js/dist/sync-test'; 10 | // This must be loaded in after ZoneJS 11 | // tslint:disable-next-line:ordered-imports 12 | import 'jest-zone-patch'; 13 | 14 | import { TestBed } from '@angular/core/testing'; 15 | import { 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting, 18 | } from '@angular/platform-browser-dynamic/testing'; 19 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 20 | 21 | TestBed.initTestEnvironment( 22 | [BrowserDynamicTestingModule, NoopAnimationsModule], 23 | platformBrowserDynamicTesting(), 24 | ); 25 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "10.0.0", 4 | "npmClient": "yarn", 5 | "useWorkspaces": true, 6 | "ignore": "example-app", 7 | "ignoreChanges": ["**/*.spec.ts", "**/*.md"], 8 | "command": { 9 | "init": { 10 | "exact": true 11 | }, 12 | "publish": { 13 | "npmClient": "yarn", 14 | "allowBranch": "master", 15 | "contents": "dist" 16 | }, 17 | "version": { 18 | "allowBranch": "master", 19 | "conventionalCommits": true, 20 | "exact": true, 21 | "githubRelease": true, 22 | "message": "chore(release): publish %s", 23 | "signGitCommit": true, 24 | "signGitTag": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "platform", 3 | "private": true, 4 | "workspaces": { 5 | "packages": [ 6 | "packages/*" 7 | ], 8 | "nohoist": [ 9 | "example-app/@angular-redux/store", 10 | "example-app/@angular-redux/form", 11 | "example-app/@angular-redux/router", 12 | "example-app/ng-packagr" 13 | ] 14 | }, 15 | "scripts": { 16 | "build": "npm-run-all build:*", 17 | "build:store": "yarn workspace @angular-redux/store build", 18 | "build:form": "yarn workspace @angular-redux/form build", 19 | "build:router": "yarn workspace @angular-redux/router build", 20 | "build:example-app": "yarn workspace example-app build", 21 | "release:canary": "lerna publish --canary --dist-tag canary --npm-client npm", 22 | "release:stable": "lerna version", 23 | "release:stable:ci": "lerna publish from-git", 24 | "clean": "npm-run-all -p clean:*", 25 | "clean:deps": "npm-run-all -p clean:deps:*", 26 | "clean:deps:workspace": "rimraf node_modules", 27 | "clean:deps:packages": "rimraf packages/*/node_modules", 28 | "clean:coverage": "rimraf coverage", 29 | "clean:dist": "rimraf packages/*/dist", 30 | "docs": "docsify serve ./docs", 31 | "lint": "npm-run-all -p lint:*", 32 | "lint:packages": "tslint -p tsconfig.json", 33 | "lint:prettier": "prettier -l \"**/*.*(ts|js|css|scss|json|md)\"", 34 | "test": "jest --coverage", 35 | "test:ci": "jest --coverage --ci --maxWorkers=2", 36 | "test:watch": "jest --watch", 37 | "commit": "commit", 38 | "prettier": "prettier --write \"**/*.*(ts|js|css|scss|json|md)\"", 39 | "run:app": "yarn workspace example-app bootstrap && yarn workspace example-app start" 40 | }, 41 | "lint-staged": { 42 | "*.{ts,js,css,scss,json,md}": [ 43 | "prettier --write", 44 | "git add" 45 | ] 46 | }, 47 | "engines": { 48 | "node": ">=8" 49 | }, 50 | "devDependencies": { 51 | "@angular/animations": "7.2.1", 52 | "@angular/common": "7.2.1", 53 | "@angular/compiler": "7.2.1", 54 | "@angular/compiler-cli": "7.2.1", 55 | "@angular/core": "7.2.1", 56 | "@angular/forms": "7.2.1", 57 | "@angular/http": "7.2.1", 58 | "@angular/platform-browser": "7.2.1", 59 | "@angular/platform-browser-dynamic": "7.2.1", 60 | "@angular/router": "7.2.1", 61 | "@babel/core": "7.2.2", 62 | "@babel/types": "7.3.0", 63 | "@commitlint/cli": "7.3.2", 64 | "@commitlint/config-conventional": "7.3.1", 65 | "@commitlint/prompt-cli": "7.3.1", 66 | "@types/jest": "23.3.13", 67 | "babel-jest": "23.6.0", 68 | "codelyzer": "4.5.0", 69 | "docsify-cli": "4.3.0", 70 | "husky": "1.3.1", 71 | "jest": "23.6.0", 72 | "jest-junit": "6.0.1", 73 | "jest-preset-angular": "6.0.2", 74 | "jest-zone-patch": "0.0.10", 75 | "lerna": "3.13.4", 76 | "lint-staged": "8.1.0", 77 | "ng-packagr": "4.6.0", 78 | "npm-run-all": "4.1.5", 79 | "prettier": "1.15.3", 80 | "redux": "4.0.1", 81 | "rimraf": "2.6.3", 82 | "rxjs": "6.3.3", 83 | "tsickle": "0.34.0", 84 | "tslib": "1.9.3", 85 | "tslint": "5.12.1", 86 | "typescript": "3.2.4", 87 | "zone.js": "0.8.28" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/example-app/.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 | -------------------------------------------------------------------------------- /packages/example-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | *.sublime-workspace 18 | 19 | # IDE - VSCode 20 | .vscode/* 21 | !.vscode/settings.json 22 | !.vscode/tasks.json 23 | !.vscode/launch.json 24 | !.vscode/extensions.json 25 | 26 | # misc 27 | /.sass-cache 28 | /connect.lock 29 | /coverage/* 30 | /libpeerconnection.log 31 | npm-debug.log 32 | testem.log 33 | /typings 34 | 35 | # e2e 36 | /e2e/*.js 37 | /e2e/*.map 38 | 39 | #System Files 40 | .DS_Store 41 | Thumbs.db 42 | -------------------------------------------------------------------------------- /packages/example-app/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [10.0.0](https://github.com/angular-redux/platform/compare/v9.0.1...v10.0.0) (2019-05-04) 7 | 8 | ### chore 9 | 10 | - **linting:** add global tslint rules ([#35](https://github.com/angular-redux/platform/issues/35)) ([336cc60](https://github.com/angular-redux/platform/commit/336cc60)), closes [#4](https://github.com/angular-redux/platform/issues/4) 11 | 12 | ### Features 13 | 14 | - upgrade to angular 7 ([#72](https://github.com/angular-redux/platform/issues/72)) ([18d9245](https://github.com/angular-redux/platform/commit/18d9245)), closes [#65](https://github.com/angular-redux/platform/issues/65) [#66](https://github.com/angular-redux/platform/issues/66) [#67](https://github.com/angular-redux/platform/issues/67) [#68](https://github.com/angular-redux/platform/issues/68) [#69](https://github.com/angular-redux/platform/issues/69) [#70](https://github.com/angular-redux/platform/issues/70) [#71](https://github.com/angular-redux/platform/issues/71) [#74](https://github.com/angular-redux/platform/issues/74) [#79](https://github.com/angular-redux/platform/issues/79) 15 | 16 | ### BREAKING CHANGES 17 | 18 | - Upgrades Angular dependencies to v7 19 | - **linting:** - ConnectArray has been renamed to ConnectArrayDirective 20 | 21 | * ReactiveConnect has been renamed to ReactiveConnectDirective 22 | * Connect has been renamed to ConnectDirective 23 | * interfaces with an "I" prefix have had that prefix removed (e.g "IAppStore" -> "AppStore") 24 | -------------------------------------------------------------------------------- /packages/example-app/README.md: -------------------------------------------------------------------------------- 1 | # Example App: Zoo Animals 2 | 3 | This is a sample project showing how the following packages work together to make a simple 4 | application. 5 | 6 | - [redux](https://github.com/reactjs/redux) Predictable state container for Javascript. 7 | - [redux-observable](https://github.com/redux-observable/redux-observable) Side-effect handling with Observables 8 | - [@angular-redux/store](/packages/store) Redux + Angular bindings 9 | - [@angular-redux/router](/packages/router) Time travel with the Angular router 10 | - [@angular-redux/form](/packages/form) Time travel with Angular forms 11 | - [Redux DevTools Chrome Extension](https://github.com/zalmoxisus/redux-devtools-extension) 12 | 13 | ## Bootstrapping 14 | 15 | Before being able to run the app, you will need to bootstrap the workspace dependencies linked by yarn. This can be done using the command `yarn bootstrap`. 16 | 17 | ## Development server 18 | 19 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 20 | 21 | ## Code scaffolding 22 | 23 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`. 24 | 25 | ## Build 26 | 27 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 28 | 29 | ## Running unit tests 30 | 31 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 32 | 33 | ## Running end-to-end tests 34 | 35 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 36 | Before running the tests make sure you are serving the app via `ng serve`. 37 | 38 | ## Using the folder as a standalone 39 | 40 | The versions included in `package.json` for the `@angular-redux` dependencies are linked using **yarn workspaces** and will need to be changed to a _latest_ or _next_ version if you want to detach the app as a standalone. 41 | 42 | ## Further help 43 | 44 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 45 | -------------------------------------------------------------------------------- /packages/example-app/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "example-app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "zoo", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/example-app", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": ["src/favicon.ico", "src/assets"], 22 | "styles": ["src/styles.css"], 23 | "scripts": [] 24 | }, 25 | "configurations": { 26 | "production": { 27 | "fileReplacements": [ 28 | { 29 | "replace": "src/environments/environment.ts", 30 | "with": "src/environments/environment.prod.ts" 31 | } 32 | ], 33 | "optimization": true, 34 | "outputHashing": "all", 35 | "sourceMap": false, 36 | "extractCss": true, 37 | "namedChunks": false, 38 | "aot": true, 39 | "extractLicenses": true, 40 | "vendorChunk": false, 41 | "buildOptimizer": true, 42 | "budgets": [ 43 | { 44 | "type": "initial", 45 | "maximumWarning": "2mb", 46 | "maximumError": "5mb" 47 | } 48 | ] 49 | } 50 | } 51 | }, 52 | "serve": { 53 | "builder": "@angular-devkit/build-angular:dev-server", 54 | "options": { 55 | "browserTarget": "example-app:build" 56 | }, 57 | "configurations": { 58 | "production": { 59 | "browserTarget": "example-app:build:production" 60 | } 61 | } 62 | }, 63 | "extract-i18n": { 64 | "builder": "@angular-devkit/build-angular:extract-i18n", 65 | "options": { 66 | "browserTarget": "example-app:build" 67 | } 68 | }, 69 | "test": { 70 | "builder": "@angular-devkit/build-angular:karma", 71 | "options": { 72 | "main": "src/test.ts", 73 | "polyfills": "src/polyfills.ts", 74 | "tsConfig": "src/tsconfig.spec.json", 75 | "karmaConfig": "src/karma.conf.js", 76 | "styles": ["src/styles.css"], 77 | "scripts": [], 78 | "assets": ["src/favicon.ico", "src/assets"] 79 | } 80 | }, 81 | "lint": { 82 | "builder": "@angular-devkit/build-angular:tslint", 83 | "options": { 84 | "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"], 85 | "exclude": ["**/node_modules/**"] 86 | } 87 | } 88 | } 89 | }, 90 | "example-app-e2e": { 91 | "root": "e2e/", 92 | "projectType": "application", 93 | "prefix": "", 94 | "architect": { 95 | "e2e": { 96 | "builder": "@angular-devkit/build-angular:protractor", 97 | "options": { 98 | "protractorConfig": "e2e/protractor.conf.js", 99 | "devServerTarget": "example-app:serve" 100 | }, 101 | "configurations": { 102 | "production": { 103 | "devServerTarget": "example-app:serve:production" 104 | } 105 | } 106 | }, 107 | "lint": { 108 | "builder": "@angular-devkit/build-angular:tslint", 109 | "options": { 110 | "tsConfig": "e2e/tsconfig.e2e.json", 111 | "exclude": ["**/node_modules/**"] 112 | } 113 | } 114 | } 115 | } 116 | }, 117 | "defaultProject": "example-app" 118 | } 119 | -------------------------------------------------------------------------------- /packages/example-app/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": ["jasmine", "jasminewd2", "node"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/example-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-app", 3 | "version": "10.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build --prod", 10 | "e2e": "ng e2e", 11 | "bootstrap": "npm-run-all bootstrap:*", 12 | "bootstrap:store": "npm explore @angular-redux/store -- npm run build", 13 | "bootstrap:form": "npm explore @angular-redux/form -- npm run build", 14 | "bootstrap:router": "npm explore @angular-redux/router -- npm run build" 15 | }, 16 | "engines": { 17 | "node": ">=8" 18 | }, 19 | "dependencies": { 20 | "@angular-redux/form": "10.0.0", 21 | "@angular-redux/router": "10.0.0", 22 | "@angular-redux/store": "10.0.0", 23 | "core-js": "2.6.2", 24 | "flux-standard-action": "2.0.3", 25 | "ramda": "0.23.0", 26 | "redux-logger": "3.0.6", 27 | "redux-observable": "1.0.0" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "0.12.1", 31 | "@angular/cli": "7.2.2", 32 | "@angular/language-service": "7.2.0", 33 | "@types/ramda": "0.24.18", 34 | "@types/redux-logger": "3.0.6", 35 | "ts-node": "7.0.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/animal-list/component.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | } 4 | .header-cell { 5 | flex-basis: 25%; 6 | font-weight: bold; 7 | } 8 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/animal-list/component.html: -------------------------------------------------------------------------------- 1 |

We have {{ animalsName }}!

2 | 3 | 4 | 5 | 6 |
7 |
Name
8 |
Ticket Price
9 |
Tickets
10 |
Subtotal
11 |
12 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/animal-list/component.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { async, TestBed } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | import { CoreModule } from '../../core/module'; 5 | import { AnimalType } from '../model'; 6 | import { AnimalListComponent } from './component'; 7 | 8 | @Component({ selector: 'zoo-animal', template: '' }) 9 | class MockAnimalComponent { 10 | @Input() key!: string; 11 | @Input() animalType!: AnimalType; 12 | } 13 | 14 | describe('AnimalListComponent', () => { 15 | beforeEach(async(() => { 16 | TestBed.configureTestingModule({ 17 | declarations: [AnimalListComponent, MockAnimalComponent], 18 | imports: [CoreModule], 19 | }).compileComponents(); 20 | })); 21 | 22 | it("should have as title 'Welcome to the Zoo'", async(() => { 23 | const fixture = TestBed.createComponent(AnimalListComponent); 24 | const animalList = fixture.componentInstance; 25 | 26 | animalList.animalsName = 'Wallabies'; 27 | animalList.animalType = 'WALLABIES'; 28 | fixture.detectChanges(); 29 | 30 | const titleElement = fixture.debugElement.query(By.css('h2')); 31 | expect(titleElement.nativeElement.textContent).toContain( 32 | 'We have Wallabies', 33 | ); 34 | })); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/animal-list/component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Animal } from '../model'; 4 | 5 | @Component({ 6 | selector: 'zoo-animal-list', 7 | templateUrl: './component.html', 8 | styleUrls: ['./component.css'], 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | }) 11 | export class AnimalListComponent { 12 | @Input() animalsName!: string; 13 | @Input() animalType!: string; 14 | @Input() animals!: Observable; 15 | @Input() loading!: Observable; 16 | @Input() error!: Observable; 17 | 18 | // Since we're observing an array of items, we need to set up a 'trackBy' 19 | // parameter so Angular doesn't tear down and rebuild the list's DOM every 20 | // time there's an update. 21 | getKey(_: unknown, animal: Animal) { 22 | return animal.id; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/animal/actions.ts: -------------------------------------------------------------------------------- 1 | export const ADD_TICKET = 'ADD_TICKET'; 2 | export const REMOVE_TICKET = 'REMOVE_TICKET'; 3 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/animal/component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | } 4 | div { 5 | flex-basis: 25%; 6 | } 7 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/animal/component.html: -------------------------------------------------------------------------------- 1 |
{{ name | async }}
2 |
{{ ticketPrice | async }}
3 |
4 | 8 | 9 |
10 |
${{ subTotal | async }} 11 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/animal/component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MockNgRedux, 3 | NgReduxTestingModule, 4 | } from '@angular-redux/store/testing'; 5 | import { async, TestBed } from '@angular/core/testing'; 6 | import { toArray } from 'rxjs/operators'; 7 | import { CoreModule } from '../../core/module'; 8 | import { AnimalComponent } from './component'; 9 | 10 | describe('AnimalComponent', () => { 11 | let fixture; 12 | let animalComponent: AnimalComponent; 13 | let spyConfigureSubStore: jasmine.Spy; 14 | 15 | beforeEach(async(() => { 16 | spyConfigureSubStore = spyOn( 17 | MockNgRedux.getInstance(), 18 | 'configureSubStore', 19 | ).and.callThrough(); 20 | 21 | MockNgRedux.reset(); 22 | TestBed.configureTestingModule({ 23 | declarations: [AnimalComponent], 24 | imports: [CoreModule, NgReduxTestingModule], 25 | }).compileComponents(); 26 | 27 | fixture = TestBed.createComponent(AnimalComponent); 28 | animalComponent = fixture.componentInstance; 29 | 30 | animalComponent.key = 'id1'; 31 | animalComponent.animalType = 'WALLABIES'; 32 | 33 | fixture.detectChanges(); 34 | })); 35 | 36 | it('should use the key to create a subStore', () => 37 | expect(spyConfigureSubStore).toHaveBeenCalledWith( 38 | ['WALLABIES', 'items', 'id1'], 39 | jasmine.any(Function), 40 | )); 41 | 42 | it('select name data from the substore', async(() => { 43 | const mockSubStore = MockNgRedux.getSubStore(['WALLABIES', 'items', 'id1']); 44 | 45 | const selectorStub = mockSubStore.getSelectorStub('name'); 46 | selectorStub.next('Wilbert'); 47 | selectorStub.complete(); 48 | 49 | animalComponent.name.subscribe(name => expect(name).toEqual('Wilbert')); 50 | })); 51 | 52 | it('select ticket price data from the substore', async(() => { 53 | const mockSubStore = MockNgRedux.getSubStore(['WALLABIES', 'items', 'id1']); 54 | 55 | const selectorStub = mockSubStore.getSelectorStub('ticketPrice'); 56 | selectorStub.next(2); 57 | selectorStub.complete(); 58 | 59 | animalComponent.ticketPrice.subscribe(ticketPrice => 60 | expect(ticketPrice).toEqual(2), 61 | ); 62 | })); 63 | 64 | it('select ticket quantity data from the substore', async(() => { 65 | const mockSubStore = MockNgRedux.getSubStore(['WALLABIES', 'items', 'id1']); 66 | 67 | const selectorStub = mockSubStore.getSelectorStub('tickets'); 68 | selectorStub.next(4); 69 | selectorStub.complete(); 70 | 71 | animalComponent.numTickets.subscribe(numTickets => 72 | expect(numTickets).toEqual(4), 73 | ); 74 | })); 75 | 76 | it('should use reasonable defaults if ticket price is missing', async(() => { 77 | animalComponent.ticketPrice.subscribe(ticketPrice => 78 | expect(ticketPrice).toEqual(0), 79 | ); 80 | })); 81 | 82 | it('should use reasonable defaults if ticket quantity is missing', async(() => { 83 | animalComponent.numTickets.subscribe(numTickets => 84 | expect(numTickets).toEqual(0), 85 | ); 86 | })); 87 | 88 | it('should compute the subtotal as the ticket quantity changes', async(() => { 89 | const mockSubStore = MockNgRedux.getSubStore(['WALLABIES', 'items', 'id1']); 90 | 91 | const priceStub = mockSubStore.getSelectorStub('ticketPrice'); 92 | priceStub.next(1); 93 | priceStub.next(2); 94 | priceStub.next(3); 95 | priceStub.complete(); 96 | 97 | const quantityStub = mockSubStore.getSelectorStub('tickets'); 98 | quantityStub.next(5); 99 | quantityStub.complete(); 100 | 101 | animalComponent.subTotal 102 | .pipe(toArray()) 103 | .subscribe(subTotals => expect(subTotals).toEqual([5, 10, 15])); 104 | })); 105 | }); 106 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/animal/component.ts: -------------------------------------------------------------------------------- 1 | import { dispatch, select, select$, WithSubStore } from '@angular-redux/store'; 2 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | import { Animal } from '../model'; 7 | import { animalComponentReducer } from './reducers'; 8 | 9 | export function toSubTotal(obs: Observable): Observable { 10 | return obs.pipe(map(s => s.ticketPrice * s.tickets)); 11 | } 12 | 13 | /** 14 | * Fractal component example. 15 | */ 16 | @WithSubStore({ 17 | basePathMethodName: 'getBasePath', 18 | localReducer: animalComponentReducer, 19 | }) 20 | @Component({ 21 | selector: 'zoo-animal', 22 | templateUrl: './component.html', 23 | styleUrls: ['./component.css'], 24 | changeDetection: ChangeDetectionStrategy.OnPush, 25 | }) 26 | export class AnimalComponent { 27 | @Input() key!: string; 28 | @Input() animalType!: string; 29 | 30 | @select() readonly name!: Observable; 31 | @select('tickets') readonly numTickets!: Observable; 32 | @select('ticketPrice') readonly ticketPrice!: Observable; 33 | @select$('', toSubTotal) 34 | readonly subTotal!: Observable; 35 | 36 | getBasePath = () => (this.key ? [this.animalType, 'items', this.key] : null); 37 | 38 | @dispatch() addTicket = () => ({ type: 'ADD_TICKET' }); 39 | @dispatch() removeTicket = () => ({ type: 'REMOVE_TICKET' }); 40 | } 41 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/animal/reducers.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | 3 | import { Animal } from '../model'; 4 | import { ADD_TICKET, REMOVE_TICKET } from './actions'; 5 | 6 | export const ticketsReducer: Reducer = ( 7 | state = 0, 8 | action: Action, 9 | ): number => { 10 | switch (action.type) { 11 | case ADD_TICKET: 12 | return state + 1; 13 | case REMOVE_TICKET: 14 | return Math.max(0, state - 1); 15 | } 16 | return state; 17 | }; 18 | 19 | // Basic reducer logic. 20 | export const animalComponentReducer = ( 21 | state: Animal | undefined, 22 | action: Action, 23 | ) => 24 | state 25 | ? { 26 | ...state, 27 | tickets: ticketsReducer(state.tickets, action), 28 | } 29 | : {}; 30 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/api/actions.ts: -------------------------------------------------------------------------------- 1 | import { dispatch } from '@angular-redux/store'; 2 | import { Injectable } from '@angular/core'; 3 | import { FluxStandardAction } from 'flux-standard-action'; 4 | 5 | import { Animal, AnimalError, AnimalType } from '../model'; 6 | 7 | // Flux-standard-action gives us stronger typing of our actions. 8 | export type Payload = Animal[] | AnimalError; 9 | 10 | export interface MetaData { 11 | animalType: AnimalType; 12 | } 13 | 14 | export type AnimalAPIAction = FluxStandardAction< 15 | T, 16 | MetaData 17 | >; 18 | 19 | @Injectable() 20 | export class AnimalAPIActions { 21 | static readonly LOAD_ANIMALS = 'LOAD_ANIMALS'; 22 | static readonly LOAD_STARTED = 'LOAD_STARTED'; 23 | static readonly LOAD_SUCCEEDED = 'LOAD_SUCCEEDED'; 24 | static readonly LOAD_FAILED = 'LOAD_FAILED'; 25 | 26 | @dispatch() 27 | loadAnimals = (animalType: AnimalType): AnimalAPIAction => ({ 28 | type: AnimalAPIActions.LOAD_ANIMALS, 29 | meta: { animalType }, 30 | }); 31 | 32 | loadStarted = (animalType: AnimalType): AnimalAPIAction => ({ 33 | type: AnimalAPIActions.LOAD_STARTED, 34 | meta: { animalType }, 35 | }); 36 | 37 | loadSucceeded = ( 38 | animalType: AnimalType, 39 | payload: Animal[], 40 | ): AnimalAPIAction => ({ 41 | type: AnimalAPIActions.LOAD_SUCCEEDED, 42 | meta: { animalType }, 43 | payload, 44 | }); 45 | 46 | loadFailed = ( 47 | animalType: AnimalType, 48 | error: AnimalError, 49 | ): AnimalAPIAction => ({ 50 | type: AnimalAPIActions.LOAD_FAILED, 51 | meta: { animalType }, 52 | payload: error, 53 | error: true, 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/api/epics.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Epic } from 'redux-observable'; 3 | 4 | import { of } from 'rxjs'; 5 | import { catchError, filter, map, startWith, switchMap } from 'rxjs/operators'; 6 | 7 | import { AppState } from '../../store/model'; 8 | import { Animal, AnimalError, AnimalType } from '../model'; 9 | import { AnimalAPIAction, AnimalAPIActions } from './actions'; 10 | import { AnimalAPIService } from './service'; 11 | 12 | const animalsNotAlreadyFetched = ( 13 | animalType: AnimalType, 14 | state: AppState, 15 | ): boolean => 16 | !( 17 | state[animalType] && 18 | state[animalType].items && 19 | Object.keys(state[animalType].items).length 20 | ); 21 | 22 | const actionIsForCorrectAnimalType = (animalType: AnimalType) => ( 23 | action: AnimalAPIAction, 24 | ): boolean => action.meta!.animalType === animalType; 25 | 26 | @Injectable() 27 | export class AnimalAPIEpics { 28 | constructor( 29 | private service: AnimalAPIService, 30 | private actions: AnimalAPIActions, 31 | ) {} 32 | 33 | createEpic(animalType: AnimalType) { 34 | return this.createLoadAnimalEpic(animalType); 35 | } 36 | 37 | private createLoadAnimalEpic( 38 | animalType: AnimalType, 39 | ): Epic< 40 | AnimalAPIAction, 41 | AnimalAPIAction, 42 | AppState 43 | > { 44 | return (action$, state$) => 45 | action$.ofType(AnimalAPIActions.LOAD_ANIMALS).pipe( 46 | filter(action => 47 | actionIsForCorrectAnimalType(animalType)(action as AnimalAPIAction), 48 | ), 49 | filter(() => animalsNotAlreadyFetched(animalType, state$.value)), 50 | switchMap(() => 51 | this.service.getAll(animalType).pipe( 52 | map(data => this.actions.loadSucceeded(animalType, data)), 53 | catchError(response => 54 | of( 55 | this.actions.loadFailed(animalType, { 56 | status: '' + response.status, 57 | }), 58 | ), 59 | ), 60 | startWith(this.actions.loadStarted(animalType)), 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/api/reducer.ts: -------------------------------------------------------------------------------- 1 | import { indexBy, prop } from 'ramda'; 2 | import { Action } from 'redux'; 3 | 4 | import { Animal, AnimalList, AnimalType } from '../model'; 5 | import { AnimalAPIAction, AnimalAPIActions } from './actions'; 6 | 7 | const INITIAL_STATE: AnimalList = { 8 | items: {}, 9 | loading: false, 10 | error: undefined, 11 | }; 12 | 13 | // A higher-order reducer: accepts an animal type and returns a reducer 14 | // that only responds to actions for that particular animal type. 15 | export function createAnimalAPIReducer(animalType: AnimalType) { 16 | return function animalReducer( 17 | state: AnimalList = INITIAL_STATE, 18 | a: Action, 19 | ): AnimalList { 20 | const action = a as AnimalAPIAction; 21 | if (!action.meta || action.meta.animalType !== animalType) { 22 | return state; 23 | } 24 | 25 | switch (action.type) { 26 | case AnimalAPIActions.LOAD_STARTED: 27 | return { 28 | ...state, 29 | items: {}, 30 | loading: true, 31 | error: undefined, 32 | }; 33 | case AnimalAPIActions.LOAD_SUCCEEDED: 34 | return { 35 | ...state, 36 | items: indexBy(prop('id'), action.payload as Animal[]), 37 | loading: false, 38 | error: undefined, 39 | }; 40 | case AnimalAPIActions.LOAD_FAILED: 41 | return { 42 | ...state, 43 | items: {}, 44 | loading: false, 45 | error: action.error, 46 | }; 47 | } 48 | 49 | return state; 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/api/service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http } from '@angular/http'; 3 | 4 | import { Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | import { Animal, ANIMAL_TYPES, AnimalType, fromServer } from '../model'; 8 | 9 | // A fake API on the internets. 10 | const URLS = { 11 | [ANIMAL_TYPES.ELEPHANT]: 'http://www.mocky.io/v2/59200c34110000ce1a07b598', 12 | [ANIMAL_TYPES.LION]: 'http://www.mocky.io/v2/5920141a25000023015998f2', 13 | }; 14 | 15 | @Injectable() 16 | export class AnimalAPIService { 17 | constructor(private http: Http) {} 18 | 19 | getAll = (animalType: AnimalType): Observable => 20 | this.http.get(URLS[animalType]).pipe( 21 | map(resp => resp.json()), 22 | map(records => records.map(fromServer)), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/model.ts: -------------------------------------------------------------------------------- 1 | export const ANIMAL_TYPES: { [key: string]: AnimalType } = { 2 | LION: 'lion', 3 | ELEPHANT: 'elephant', 4 | }; 5 | 6 | // TODO: is there a way to improve this? 7 | export type AnimalType = 'lion' | 'elephant'; 8 | export interface Animal { 9 | id: string; 10 | animalType: AnimalType; 11 | name: string; 12 | ticketPrice: number; 13 | tickets: number; 14 | } 15 | 16 | export interface AnimalResponse { 17 | name: string; 18 | type: AnimalType; 19 | ticketPrice: number; 20 | } 21 | 22 | export interface AnimalList { 23 | items: {}; 24 | loading: boolean; 25 | error: boolean | undefined; 26 | } 27 | 28 | export interface AnimalError { 29 | status: string; 30 | } 31 | 32 | export function initialAnimalList(): AnimalList { 33 | return { 34 | items: {}, 35 | loading: false, 36 | error: undefined, 37 | }; 38 | } 39 | 40 | export const fromServer = (record: AnimalResponse): Animal => ({ 41 | id: record.name.toLowerCase(), 42 | animalType: record.type, 43 | name: record.name, 44 | ticketPrice: record.ticketPrice || 0, 45 | tickets: 0, 46 | }); 47 | -------------------------------------------------------------------------------- /packages/example-app/src/app/animals/module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { CoreModule } from '../core/module'; 5 | import { StoreModule } from '../store/module'; 6 | import { AnimalListComponent } from './animal-list/component'; 7 | import { AnimalAPIActions } from './api/actions'; 8 | import { AnimalAPIEpics } from './api/epics'; 9 | import { AnimalAPIService } from './api/service'; 10 | 11 | import { AnimalComponent } from './animal/component'; 12 | 13 | @NgModule({ 14 | declarations: [AnimalListComponent, AnimalComponent], 15 | exports: [AnimalListComponent], 16 | imports: [CoreModule, StoreModule, CommonModule], 17 | providers: [AnimalAPIActions, AnimalAPIEpics, AnimalAPIService], 18 | }) 19 | export class AnimalModule {} 20 | -------------------------------------------------------------------------------- /packages/example-app/src/app/component.css: -------------------------------------------------------------------------------- 1 | .active { 2 | background: #eee; 3 | border-radius: 3px; 4 | padding: 5px; 5 | } 6 | 7 | content { 8 | display: block; 9 | padding: 10px; 10 | border: solid gray 1px; 11 | border-radius: 5px; 12 | margin-top: 1rem; 13 | } 14 | -------------------------------------------------------------------------------- /packages/example-app/src/app/component.html: -------------------------------------------------------------------------------- 1 |

2 | {{ title }} 3 |

4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/example-app/src/app/component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [AppComponent], 9 | imports: [RouterTestingModule], 10 | }).compileComponents(); 11 | })); 12 | 13 | it("should have as title 'Welcome to the Zoo'", async(() => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.componentInstance; 16 | expect(app.title).toEqual('Welcome to the Zoo'); 17 | })); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/example-app/src/app/component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'zoo-root', 5 | templateUrl: './component.html', 6 | styleUrls: ['./component.css'], 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | }) 9 | export class AppComponent { 10 | title = 'Welcome to the Zoo'; 11 | } 12 | -------------------------------------------------------------------------------- /packages/example-app/src/app/core/counter/component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ count }} 3 | 4 | -------------------------------------------------------------------------------- /packages/example-app/src/app/core/counter/component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | Output, 7 | } from '@angular/core'; 8 | 9 | @Component({ 10 | selector: 'zoo-counter', 11 | templateUrl: './component.html', 12 | changeDetection: ChangeDetectionStrategy.OnPush, 13 | }) 14 | export class CounterComponent { 15 | @Input() count!: number; 16 | @Output() increment = new EventEmitter(); 17 | @Output() decrement = new EventEmitter(); 18 | } 19 | -------------------------------------------------------------------------------- /packages/example-app/src/app/core/error-well/component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | background: #fdd; 3 | border: solid maroon 1px; 4 | border-radius: 3px; 5 | color: maroon; 6 | display: block; 7 | padding: 3px; 8 | width: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /packages/example-app/src/app/core/error-well/component.html: -------------------------------------------------------------------------------- 1 | Error status: {{ statusCode | async }} 2 | -------------------------------------------------------------------------------- /packages/example-app/src/app/core/error-well/component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | @Component({ 5 | selector: 'zoo-error-well', 6 | templateUrl: './component.html', 7 | styleUrls: ['./component.css'], 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | }) 10 | export class ErrorWellComponent { 11 | @Input() statusCode!: Observable; 12 | } 13 | -------------------------------------------------------------------------------- /packages/example-app/src/app/core/module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { CounterComponent } from './counter/component'; 5 | import { ErrorWellComponent } from './error-well/component'; 6 | import { SpinnerComponent } from './spinner/component'; 7 | 8 | @NgModule({ 9 | declarations: [SpinnerComponent, ErrorWellComponent, CounterComponent], 10 | imports: [CommonModule], 11 | exports: [SpinnerComponent, ErrorWellComponent, CounterComponent], 12 | }) 13 | export class CoreModule {} 14 | -------------------------------------------------------------------------------- /packages/example-app/src/app/core/spinner/component.css: -------------------------------------------------------------------------------- 1 | /* Taken from https://projects.lukehaas.me/css-loaders/ */ 2 | :host, 3 | :host:before, 4 | :host:after { 5 | border-radius: 50%; 6 | } 7 | :host { 8 | color: #000000; 9 | display: block; 10 | font-size: 11px; 11 | text-indent: -99999em; 12 | margin: 55px auto; 13 | position: relative; 14 | width: 10em; 15 | height: 10em; 16 | box-shadow: inset 0 0 0 1em; 17 | -webkit-transform: translateZ(0); 18 | -ms-transform: translateZ(0); 19 | transform: translateZ(0); 20 | } 21 | :host:before, 22 | :host:after { 23 | position: absolute; 24 | content: ''; 25 | } 26 | :host:before { 27 | width: 5.2em; 28 | height: 10.2em; 29 | background: #fff; 30 | border-radius: 10.2em 0 0 10.2em; 31 | top: -0.1em; 32 | left: -0.1em; 33 | -webkit-transform-origin: 5.2em 5.1em; 34 | transform-origin: 5.2em 5.1em; 35 | -webkit-animation: load2 2s infinite ease 1.5s; 36 | animation: load2 2s infinite ease 1.5s; 37 | } 38 | :host:after { 39 | width: 5.2em; 40 | height: 10.2em; 41 | background: #fff; 42 | border-radius: 0 10.2em 10.2em 0; 43 | top: -0.1em; 44 | left: 5.1em; 45 | -webkit-transform-origin: 0px 5.1em; 46 | transform-origin: 0px 5.1em; 47 | -webkit-animation: load2 2s infinite ease; 48 | animation: load2 2s infinite ease; 49 | } 50 | @-webkit-keyframes load2 { 51 | 0% { 52 | -webkit-transform: rotate(0deg); 53 | transform: rotate(0deg); 54 | } 55 | 100% { 56 | -webkit-transform: rotate(360deg); 57 | transform: rotate(360deg); 58 | } 59 | } 60 | @keyframes load2 { 61 | 0% { 62 | -webkit-transform: rotate(0deg); 63 | transform: rotate(0deg); 64 | } 65 | 100% { 66 | -webkit-transform: rotate(360deg); 67 | transform: rotate(360deg); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/example-app/src/app/core/spinner/component.html: -------------------------------------------------------------------------------- 1 | Loading... -------------------------------------------------------------------------------- /packages/example-app/src/app/core/spinner/component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'zoo-spinner', 5 | templateUrl: './component.html', 6 | styleUrls: ['./component.css'], 7 | }) 8 | export class SpinnerComponent {} 9 | -------------------------------------------------------------------------------- /packages/example-app/src/app/elephants/module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AnimalModule } from '../animals/module'; 5 | import { CoreModule } from '../core/module'; 6 | import { StoreModule } from '../store/module'; 7 | import { ElephantPageComponent } from './page'; 8 | 9 | @NgModule({ 10 | declarations: [ElephantPageComponent], 11 | exports: [ElephantPageComponent], 12 | imports: [AnimalModule, CoreModule, StoreModule, CommonModule], 13 | }) 14 | export class ElephantModule {} 15 | -------------------------------------------------------------------------------- /packages/example-app/src/app/elephants/page.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /packages/example-app/src/app/elephants/page.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MockNgRedux, 3 | NgReduxTestingModule, 4 | } from '@angular-redux/store/testing'; 5 | import { TestBed } from '@angular/core/testing'; 6 | 7 | import { Component, Input } from '@angular/core'; 8 | 9 | import { Observable } from 'rxjs'; 10 | import { toArray } from 'rxjs/operators'; 11 | 12 | import { AnimalAPIActions } from '../animals/api/actions'; 13 | import { Animal, ANIMAL_TYPES } from '../animals/model'; 14 | import { ElephantPageComponent } from './page'; 15 | 16 | @Component({ 17 | selector: 'zoo-animal-list', 18 | template: 'Mock Animal List', 19 | }) 20 | class MockAnimalListComponent { 21 | @Input() animalsName!: string; 22 | @Input() animals!: Observable; 23 | @Input() loading!: Observable; 24 | @Input() error!: Observable; 25 | } 26 | 27 | describe('Elephant Page Container', () => { 28 | beforeEach(() => { 29 | TestBed.configureTestingModule({ 30 | declarations: [ElephantPageComponent, MockAnimalListComponent], 31 | imports: [NgReduxTestingModule], 32 | providers: [AnimalAPIActions], 33 | }).compileComponents(); 34 | 35 | MockNgRedux.reset(); 36 | }); 37 | 38 | it('should select some elephants from the Redux store', done => { 39 | const fixture = TestBed.createComponent(ElephantPageComponent); 40 | const elephantPage = fixture.componentInstance; 41 | const mockStoreSequence = [ 42 | { elephant1: { name: 'I am an Elephant!', id: 'elephant1' } }, 43 | { 44 | elephant1: { name: 'I am an Elephant!', id: 'elephant1' }, 45 | elephant2: { name: 'I am a second Elephant!', id: 'elephant2' }, 46 | }, 47 | ]; 48 | 49 | const expectedSequence = [ 50 | [{ name: 'I am an Elephant!', id: 'elephant1' }], 51 | [ 52 | // Alphanumeric sort by name. 53 | { name: 'I am a second Elephant!', id: 'elephant2' }, 54 | { name: 'I am an Elephant!', id: 'elephant1' }, 55 | ], 56 | ]; 57 | 58 | const elephantItemStub = MockNgRedux.getSelectorStub(['elephant', 'items']); 59 | mockStoreSequence.forEach(value => elephantItemStub.next(value)); 60 | elephantItemStub.complete(); 61 | 62 | elephantPage.animals 63 | .pipe(toArray()) 64 | .subscribe( 65 | actualSequence => 66 | expect(actualSequence).toEqual(expectedSequence as any), 67 | undefined, 68 | done, 69 | ); 70 | }); 71 | 72 | it('should know when the animals are loading', done => { 73 | const fixture = TestBed.createComponent(ElephantPageComponent); 74 | const elephantPage = fixture.componentInstance; 75 | 76 | const stub = MockNgRedux.getSelectorStub(['elephant', 'loading']); 77 | stub.next(false); 78 | stub.next(true); 79 | stub.complete(); 80 | 81 | elephantPage.loading 82 | .pipe(toArray()) 83 | .subscribe( 84 | actualSequence => expect(actualSequence).toEqual([false, true]), 85 | undefined, 86 | done, 87 | ); 88 | }); 89 | 90 | it("should know when there's an error", done => { 91 | const fixture = TestBed.createComponent(ElephantPageComponent); 92 | const elephantPage = fixture.componentInstance; 93 | 94 | const stub = MockNgRedux.getSelectorStub(['elephant', 'error']); 95 | stub.next(false); 96 | stub.next(true); 97 | stub.complete(); 98 | 99 | elephantPage.error 100 | .pipe(toArray()) 101 | .subscribe( 102 | actualSequence => expect(actualSequence).toEqual([false, true]), 103 | undefined, 104 | done, 105 | ); 106 | }); 107 | 108 | it('should load elephants on creation', () => { 109 | const spy = spyOn(MockNgRedux.getInstance(), 'dispatch'); 110 | TestBed.createComponent(ElephantPageComponent); 111 | 112 | expect(spy).toHaveBeenCalledWith({ 113 | type: AnimalAPIActions.LOAD_ANIMALS, 114 | meta: { animalType: ANIMAL_TYPES.ELEPHANT }, 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /packages/example-app/src/app/elephants/page.ts: -------------------------------------------------------------------------------- 1 | import { select, select$ } from '@angular-redux/store'; 2 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 3 | import { pipe, prop, sortBy, values } from 'ramda'; 4 | 5 | import { Observable } from 'rxjs'; 6 | import { map } from 'rxjs/operators'; 7 | 8 | import { AnimalAPIActions } from '../animals/api/actions'; 9 | import { Animal, ANIMAL_TYPES } from '../animals/model'; 10 | 11 | export function sortAnimals(animalDictionary$: Observable<{}>) { 12 | return animalDictionary$.pipe( 13 | map( 14 | pipe( 15 | values, 16 | sortBy(prop('name')), 17 | ), 18 | ), 19 | ); 20 | } 21 | 22 | @Component({ 23 | templateUrl: './page.html', 24 | changeDetection: ChangeDetectionStrategy.OnPush, 25 | }) 26 | export class ElephantPageComponent { 27 | // Get elephant-related data out of the Redux store as observables. 28 | @select$(['elephant', 'items'], sortAnimals) 29 | readonly animals!: Observable; 30 | 31 | @select(['elephant', 'loading']) 32 | readonly loading!: Observable; 33 | 34 | @select(['elephant', 'error']) 35 | readonly error!: Observable; 36 | 37 | constructor(actions: AnimalAPIActions) { 38 | actions.loadAnimals(ANIMAL_TYPES.ELEPHANT); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/example-app/src/app/feedback/module.ts: -------------------------------------------------------------------------------- 1 | import { NgReduxFormModule } from '@angular-redux/form'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | import { StoreModule } from '../store/module'; 6 | import { FeedbackFormComponent } from './page'; 7 | 8 | @NgModule({ 9 | declarations: [FeedbackFormComponent], 10 | providers: [], 11 | imports: [ 12 | CommonModule, 13 | FormsModule, 14 | ReactiveFormsModule, 15 | NgReduxFormModule, 16 | StoreModule, 17 | ], 18 | exports: [FeedbackFormComponent], 19 | }) 20 | export class FeedbackModule {} 21 | -------------------------------------------------------------------------------- /packages/example-app/src/app/feedback/page.css: -------------------------------------------------------------------------------- 1 | label { 2 | display: block; 3 | width: 100%; 4 | margin-bottom: 1rem; 5 | } 6 | 7 | input, 8 | textarea { 9 | display: block; 10 | width: 95%; 11 | padding: 5px; 12 | border: solid gray 1px; 13 | border-radius: 5px; 14 | } 15 | 16 | textarea { 17 | height: 250px; 18 | } 19 | 20 | .footnote { 21 | font-style: italic; 22 | } 23 | -------------------------------------------------------------------------------- /packages/example-app/src/app/feedback/page.html: -------------------------------------------------------------------------------- 1 |
2 |

Feedback Form

3 | 4 |

5 | Did you enjoy your time at the zoo? Let us know using 6 | the form below. 7 |

8 | 9 | 18 | 19 | 27 | 28 |

{{ charsLeft | async }} characters remaining. 35 | 36 | 37 | 38 | 39 |

40 | (*doesn't really send anything - this is just a demo after all.) 41 |

42 |
43 | -------------------------------------------------------------------------------- /packages/example-app/src/app/feedback/page.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MockNgRedux, 3 | NgReduxTestingModule, 4 | } from '@angular-redux/store/testing'; 5 | import { TestBed } from '@angular/core/testing'; 6 | import { toArray } from 'rxjs/operators'; 7 | 8 | import { FeedbackFormComponent } from './page'; 9 | 10 | describe('Feedback Form Component', () => { 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [FeedbackFormComponent], 14 | imports: [NgReduxTestingModule], 15 | }).compileComponents(); 16 | 17 | MockNgRedux.reset(); 18 | }); 19 | 20 | it('should keep track of the number of remaining characters left', done => { 21 | const fixture = TestBed.createComponent(FeedbackFormComponent); 22 | const form = fixture.componentInstance; 23 | 24 | const expectedCharsLeftSequence = [ 25 | form.getMaxCommentChars() - 1, 26 | form.getMaxCommentChars() - 2, 27 | form.getMaxCommentChars() - 3, 28 | form.getMaxCommentChars() - 4, 29 | form.getMaxCommentChars() - 5, 30 | ]; 31 | 32 | const feedbackCommentsStub = MockNgRedux.getSelectorStub([ 33 | 'feedback', 34 | 'comments', 35 | ]); 36 | feedbackCommentsStub.next('h'); 37 | feedbackCommentsStub.next('he'); 38 | feedbackCommentsStub.next('hel'); 39 | feedbackCommentsStub.next('hell'); 40 | feedbackCommentsStub.next('hello'); 41 | feedbackCommentsStub.complete(); 42 | 43 | form.charsLeft 44 | .pipe(toArray()) 45 | .subscribe( 46 | actualSequence => 47 | expect(actualSequence).toEqual(expectedCharsLeftSequence), 48 | undefined, 49 | done, 50 | ); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/example-app/src/app/feedback/page.ts: -------------------------------------------------------------------------------- 1 | import { select$ } from '@angular-redux/store'; 2 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | const MAX_COMMENT_CHARS = 300; 7 | 8 | export const charsLeft = (obs: Observable): Observable => 9 | obs.pipe( 10 | map(comments => comments || ''), 11 | map(comments => MAX_COMMENT_CHARS - comments.length), 12 | ); 13 | 14 | @Component({ 15 | selector: 'zoo-feedback-form', 16 | templateUrl: './page.html', 17 | styleUrls: ['./page.css'], 18 | changeDetection: ChangeDetectionStrategy.OnPush, 19 | }) 20 | export class FeedbackFormComponent { 21 | @select$(['feedback', 'comments'], charsLeft) 22 | readonly charsLeft!: Observable; 23 | 24 | getMaxCommentChars = () => MAX_COMMENT_CHARS; 25 | } 26 | -------------------------------------------------------------------------------- /packages/example-app/src/app/lions/module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AnimalModule } from '../animals/module'; 5 | import { CoreModule } from '../core/module'; 6 | import { StoreModule } from '../store/module'; 7 | import { LionPageComponent } from './page'; 8 | 9 | @NgModule({ 10 | declarations: [LionPageComponent], 11 | exports: [LionPageComponent], 12 | imports: [AnimalModule, CoreModule, StoreModule, CommonModule], 13 | }) 14 | export class LionModule {} 15 | -------------------------------------------------------------------------------- /packages/example-app/src/app/lions/page.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /packages/example-app/src/app/lions/page.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MockNgRedux, 3 | NgReduxTestingModule, 4 | } from '@angular-redux/store/testing'; 5 | import { TestBed } from '@angular/core/testing'; 6 | 7 | import { Component, Input } from '@angular/core'; 8 | 9 | import { Observable } from 'rxjs'; 10 | import { toArray } from 'rxjs/operators'; 11 | 12 | import { AnimalAPIActions } from '../animals/api/actions'; 13 | import { Animal, ANIMAL_TYPES } from '../animals/model'; 14 | import { LionPageComponent } from './page'; 15 | 16 | @Component({ 17 | selector: 'zoo-animal-list', 18 | template: 'Mock Animal List', 19 | }) 20 | class MockAnimalListComponent { 21 | @Input() animalsName!: string; 22 | @Input() animals!: Observable; 23 | @Input() loading!: Observable; 24 | @Input() error!: Observable; 25 | } 26 | 27 | describe('Lion Page Container', () => { 28 | beforeEach(() => { 29 | TestBed.configureTestingModule({ 30 | declarations: [LionPageComponent, MockAnimalListComponent], 31 | imports: [NgReduxTestingModule], 32 | providers: [AnimalAPIActions], 33 | }).compileComponents(); 34 | 35 | MockNgRedux.reset(); 36 | }); 37 | 38 | // TO DO: debug later 39 | xit('should select some lions from the Redux store', done => { 40 | const fixture = TestBed.createComponent(LionPageComponent); 41 | const lionPage = fixture.componentInstance; 42 | const mockStoreSequence = [ 43 | { lion1: { name: 'I am a Lion!', id: 'lion1' } }, 44 | { 45 | lion1: { name: 'I am a Lion!', id: 'lion1' }, 46 | lion2: { name: 'I am a second Lion!', id: 'lion2' }, 47 | }, 48 | ]; 49 | 50 | const expectedSequence = [ 51 | [{ name: 'I am a Lion!', id: 'lion1' }], 52 | [ 53 | // Alphanumeric sort by name. 54 | { name: 'I am a Lion!', id: 'lion1' }, 55 | { name: 'I am a second Lion!', id: 'lion2' }, 56 | ], 57 | ]; 58 | 59 | const itemStub = MockNgRedux.getSelectorStub(['lion', 'items']); 60 | mockStoreSequence.forEach(value => itemStub.next(value)); 61 | itemStub.complete(); 62 | 63 | lionPage.animals 64 | .pipe(toArray()) 65 | .subscribe( 66 | actualSequence => 67 | expect(actualSequence).toEqual(expectedSequence as any), 68 | undefined, 69 | done, 70 | ); 71 | }); 72 | 73 | it('should know when the animals are loading', done => { 74 | const fixture = TestBed.createComponent(LionPageComponent); 75 | const lionPage = fixture.componentInstance; 76 | 77 | const lionsLoadingStub = MockNgRedux.getSelectorStub(['lion', 'loading']); 78 | lionsLoadingStub.next(false); 79 | lionsLoadingStub.next(true); 80 | lionsLoadingStub.complete(); 81 | 82 | lionPage.loading 83 | .pipe(toArray()) 84 | .subscribe( 85 | actualSequence => expect(actualSequence).toEqual([false, true]), 86 | undefined, 87 | done, 88 | ); 89 | }); 90 | 91 | it("should know when there's an error", done => { 92 | const fixture = TestBed.createComponent(LionPageComponent); 93 | const lionPage = fixture.componentInstance; 94 | 95 | const lionsErrorStub = MockNgRedux.getSelectorStub(['lion', 'error']); 96 | lionsErrorStub.next(false); 97 | lionsErrorStub.next(true); 98 | lionsErrorStub.complete(); 99 | 100 | lionPage.error 101 | .pipe(toArray()) 102 | .subscribe( 103 | actualSequence => expect(actualSequence).toEqual([false, true]), 104 | undefined, 105 | done, 106 | ); 107 | }); 108 | 109 | it('should load lions on creation', () => { 110 | const spy = spyOn(MockNgRedux.getInstance(), 'dispatch'); 111 | TestBed.createComponent(LionPageComponent); 112 | 113 | expect(spy).toHaveBeenCalledWith({ 114 | type: AnimalAPIActions.LOAD_ANIMALS, 115 | meta: { animalType: ANIMAL_TYPES.LION }, 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /packages/example-app/src/app/lions/page.ts: -------------------------------------------------------------------------------- 1 | import { select, select$ } from '@angular-redux/store'; 2 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 3 | import { pipe, prop, sortBy, values } from 'ramda'; 4 | import { Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | import { AnimalAPIActions } from '../animals/api/actions'; 8 | import { Animal, ANIMAL_TYPES } from '../animals/model'; 9 | 10 | export function sortAnimals(animalDictionary: Observable<{}>) { 11 | return animalDictionary.pipe( 12 | map(() => 13 | pipe( 14 | values, 15 | sortBy(prop('name')), 16 | ), 17 | ), 18 | ); 19 | } 20 | 21 | @Component({ 22 | templateUrl: './page.html', 23 | changeDetection: ChangeDetectionStrategy.OnPush, 24 | }) 25 | export class LionPageComponent { 26 | // Get lion-related data out of the Redux store as observables. 27 | @select$(['lion', 'items'], sortAnimals) 28 | readonly animals!: Observable; 29 | 30 | @select(['lion', 'loading']) 31 | readonly loading!: Observable; 32 | 33 | @select(['lion', 'error']) 34 | readonly error!: Observable; 35 | 36 | constructor(actions: AnimalAPIActions) { 37 | actions.loadAnimals(ANIMAL_TYPES.LION); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/example-app/src/app/module.ts: -------------------------------------------------------------------------------- 1 | import { NgReduxRouterModule } from '@angular-redux/router'; 2 | import { NgReduxModule } from '@angular-redux/store'; 3 | import { NgModule } from '@angular/core'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { HttpModule } from '@angular/http'; 6 | import { BrowserModule } from '@angular/platform-browser'; 7 | import { RouterModule } from '@angular/router'; 8 | 9 | // This app's ngModules 10 | import { AnimalModule } from './animals/module'; 11 | import { ElephantModule } from './elephants/module'; 12 | import { FeedbackModule } from './feedback/module'; 13 | import { LionModule } from './lions/module'; 14 | import { StoreModule } from './store/module'; 15 | 16 | // Top-level app component constructs. 17 | import { AppComponent } from './component'; 18 | import { appRoutes } from './routes'; 19 | 20 | @NgModule({ 21 | declarations: [AppComponent], 22 | imports: [ 23 | RouterModule.forRoot(appRoutes), 24 | BrowserModule, 25 | FormsModule, 26 | HttpModule, 27 | NgReduxModule, 28 | NgReduxRouterModule.forRoot(), 29 | AnimalModule, 30 | ElephantModule, 31 | LionModule, 32 | FeedbackModule, 33 | StoreModule, 34 | ], 35 | bootstrap: [AppComponent], 36 | }) 37 | export class AppModule {} 38 | -------------------------------------------------------------------------------- /packages/example-app/src/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { ElephantPageComponent } from './elephants/page'; 2 | import { FeedbackFormComponent } from './feedback/page'; 3 | import { LionPageComponent } from './lions/page'; 4 | 5 | export const appRoutes = [ 6 | { path: '', redirectTo: '/elephants', pathMatch: 'full' }, 7 | { path: 'elephants', component: ElephantPageComponent }, 8 | { path: 'lions', component: LionPageComponent }, 9 | { path: 'feedback', component: FeedbackFormComponent }, 10 | ]; 11 | -------------------------------------------------------------------------------- /packages/example-app/src/app/store/epics.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { combineEpics } from 'redux-observable'; 4 | 5 | import { AnimalAPIEpics } from '../animals/api/epics'; 6 | import { ANIMAL_TYPES } from '../animals/model'; 7 | 8 | @Injectable() 9 | export class RootEpics { 10 | constructor(private animalEpics: AnimalAPIEpics) {} 11 | 12 | createEpics() { 13 | return combineEpics( 14 | this.animalEpics.createEpic(ANIMAL_TYPES.ELEPHANT), 15 | this.animalEpics.createEpic(ANIMAL_TYPES.LION), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/example-app/src/app/store/model.ts: -------------------------------------------------------------------------------- 1 | import { AnimalList, AnimalType, initialAnimalList } from '../animals/model'; 2 | 3 | export type AppState = { [key in AnimalType]: AnimalList } & 4 | Partial<{ 5 | routes: string; 6 | feedback: unknown; 7 | }>; 8 | 9 | export function initialAppState() { 10 | return { 11 | lion: initialAnimalList(), 12 | elephant: initialAnimalList(), 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/example-app/src/app/store/module.spec.ts: -------------------------------------------------------------------------------- 1 | import { DevToolsExtension, NgRedux } from '@angular-redux/store'; 2 | import { 3 | MockNgRedux, 4 | NgReduxTestingModule, 5 | } from '@angular-redux/store/testing'; 6 | import { async, getTestBed, TestBed } from '@angular/core/testing'; 7 | import { RootEpics } from './epics'; 8 | import { AppState } from './model'; 9 | import { StoreModule } from './module'; 10 | 11 | describe('Store Module', () => { 12 | let mockNgRedux: NgRedux; 13 | let devTools: DevToolsExtension; 14 | let mockEpics: Partial; 15 | 16 | beforeEach(async(() => { 17 | TestBed.configureTestingModule({ 18 | imports: [NgReduxTestingModule], 19 | }) 20 | .compileComponents() 21 | .then(() => { 22 | const testbed = getTestBed(); 23 | 24 | mockEpics = { 25 | createEpics() { 26 | return [] as any; 27 | }, 28 | }; 29 | 30 | devTools = testbed.get(DevToolsExtension); 31 | mockNgRedux = MockNgRedux.getInstance(); 32 | }); 33 | })); 34 | 35 | it('should configure the store when the module is loaded', async(() => { 36 | const configureSpy = spyOn(MockNgRedux.getInstance(), 'configureStore'); 37 | new StoreModule(mockNgRedux, devTools, null as any, mockEpics as any); 38 | 39 | expect(configureSpy).toHaveBeenCalled(); 40 | })); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/example-app/src/app/store/module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | // Angular-redux ecosystem stuff. 4 | // @angular-redux/form and @angular-redux/router are optional 5 | // extensions that sync form and route location state between 6 | // our store and Angular. 7 | import { provideReduxForms } from '@angular-redux/form'; 8 | import { NgReduxRouter, NgReduxRouterModule } from '@angular-redux/router'; 9 | import { 10 | DevToolsExtension, 11 | NgRedux, 12 | NgReduxModule, 13 | } from '@angular-redux/store'; 14 | 15 | // Redux ecosystem stuff. 16 | import { FluxStandardAction } from 'flux-standard-action'; 17 | import { createLogger } from 'redux-logger'; 18 | import { createEpicMiddleware } from 'redux-observable'; 19 | 20 | // The top-level reducers and epics that make up our app's logic. 21 | import { RootEpics } from './epics'; 22 | import { AppState, initialAppState } from './model'; 23 | import { rootReducer } from './reducers'; 24 | 25 | @NgModule({ 26 | imports: [NgReduxModule, NgReduxRouterModule.forRoot()], 27 | providers: [RootEpics], 28 | }) 29 | export class StoreModule { 30 | constructor( 31 | public store: NgRedux, 32 | devTools: DevToolsExtension, 33 | ngReduxRouter: NgReduxRouter, 34 | rootEpics: RootEpics, 35 | ) { 36 | // Tell Redux about our reducers and epics. If the Redux DevTools 37 | // chrome extension is available in the browser, tell Redux about 38 | // it too. 39 | const epicMiddleware = createEpicMiddleware< 40 | FluxStandardAction, 41 | FluxStandardAction, 42 | AppState 43 | >(); 44 | 45 | store.configureStore( 46 | rootReducer, 47 | initialAppState(), 48 | [createLogger(), epicMiddleware], 49 | // configure store typings conflict with devTools typings 50 | (devTools.isEnabled() ? [devTools.enhancer()] : []) as any, 51 | ); 52 | 53 | epicMiddleware.run(rootEpics.createEpics()); 54 | 55 | // Enable syncing of Angular router state with our Redux store. 56 | if (ngReduxRouter) { 57 | ngReduxRouter.initialize(); 58 | } 59 | 60 | // Enable syncing of Angular form state with our Redux store. 61 | provideReduxForms(store); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/example-app/src/app/store/reducers.ts: -------------------------------------------------------------------------------- 1 | import { composeReducers, defaultFormReducer } from '@angular-redux/form'; 2 | import { routerReducer } from '@angular-redux/router'; 3 | import { combineReducers } from 'redux'; 4 | 5 | import { createAnimalAPIReducer } from '../animals/api/reducer'; 6 | import { ANIMAL_TYPES } from '../animals/model'; 7 | 8 | // Define the global store shape by combining our application's 9 | // reducers together into a given structure. 10 | export const rootReducer = composeReducers( 11 | defaultFormReducer(), 12 | combineReducers({ 13 | elephant: createAnimalAPIReducer(ANIMAL_TYPES.ELEPHANT), 14 | lion: createAnimalAPIReducer(ANIMAL_TYPES.LION), 15 | router: routerReducer, 16 | }), 17 | ); 18 | -------------------------------------------------------------------------------- /packages/example-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/example-app/src/assets/.gitkeep -------------------------------------------------------------------------------- /packages/example-app/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/example-app/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 | -------------------------------------------------------------------------------- /packages/example-app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/example-app/src/favicon.ico -------------------------------------------------------------------------------- /packages/example-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ExampleApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/example-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /packages/example-app/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 | /** 22 | * IE9, IE10 and IE11 requires all of the following polyfills. 23 | */ 24 | // import 'core-js/es6/symbol'; 25 | // import 'core-js/es6/object'; 26 | // import 'core-js/es6/function'; 27 | // import 'core-js/es6/parse-int'; 28 | // import 'core-js/es6/parse-float'; 29 | // import 'core-js/es6/number'; 30 | // import 'core-js/es6/math'; 31 | // import 'core-js/es6/string'; 32 | // import 'core-js/es6/date'; 33 | // import 'core-js/es6/array'; 34 | // import 'core-js/es6/regexp'; 35 | // import 'core-js/es6/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 to support `@angular/animation`. */ 42 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 43 | 44 | /** 45 | * Evergreen browsers require these. 46 | */ 47 | import 'core-js/es6/reflect'; 48 | import 'core-js/es7/reflect'; 49 | 50 | /** 51 | * ALL Firefox browsers require the following to support `@angular/animation`. 52 | */ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | -------------------------------------------------------------------------------- /packages/example-app/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /packages/example-app/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": ["test.ts", "**/*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/example-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "lib": ["es2018", "dom"], 15 | "paths": { 16 | "@angular-redux/*": ["node_modules/@angular-redux/*/dist"] 17 | }, 18 | 19 | // Causes problems for @Outputs with AoT. 20 | // See https://github.com/angular/angular/issues/17131. 21 | // "noUnusedParameters": true, 22 | // "noUnusedLocals": true, 23 | 24 | "forceConsistentCasingInFileNames": true, 25 | "pretty": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/form/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [10.0.0](https://github.com/angular-redux/platform/compare/v9.0.1...v10.0.0) (2019-05-04) 7 | 8 | ### chore 9 | 10 | - **build:** use ng-packagr ([#37](https://github.com/angular-redux/platform/issues/37)) ([dffe23a](https://github.com/angular-redux/platform/commit/dffe23a)), closes [#9](https://github.com/angular-redux/platform/issues/9) 11 | - **linting:** add global tslint rules ([#35](https://github.com/angular-redux/platform/issues/35)) ([336cc60](https://github.com/angular-redux/platform/commit/336cc60)), closes [#4](https://github.com/angular-redux/platform/issues/4) 12 | 13 | ### Features 14 | 15 | - upgrade to angular 7 ([#72](https://github.com/angular-redux/platform/issues/72)) ([18d9245](https://github.com/angular-redux/platform/commit/18d9245)), closes [#65](https://github.com/angular-redux/platform/issues/65) [#66](https://github.com/angular-redux/platform/issues/66) [#67](https://github.com/angular-redux/platform/issues/67) [#68](https://github.com/angular-redux/platform/issues/68) [#69](https://github.com/angular-redux/platform/issues/69) [#70](https://github.com/angular-redux/platform/issues/70) [#71](https://github.com/angular-redux/platform/issues/71) [#74](https://github.com/angular-redux/platform/issues/74) [#79](https://github.com/angular-redux/platform/issues/79) 16 | 17 | ### BREAKING CHANGES 18 | 19 | - Upgrades Angular dependencies to v7 20 | - **build:** - changes the output to conform to the Angular Package Format. This may cause subtle differences in consumption behaviour 21 | 22 | * peer dependencies have been corrected to actual dependencies 23 | 24 | - **linting:** - ConnectArray has been renamed to ConnectArrayDirective 25 | 26 | * ReactiveConnect has been renamed to ReactiveConnectDirective 27 | * Connect has been renamed to ConnectDirective 28 | * interfaces with an "I" prefix have had that prefix removed (e.g "IAppStore" -> "AppStore") 29 | 30 | # NOTE: For changelog information for v6.5.3 and above, please see the GitHub release notes. 31 | 32 | # 6.5.1 - Support typescript unused checks 33 | 34 | - https://github.com/angular-redux/form/pull/32 35 | - Minor README updates. 36 | 37 | # 6.5.0 - Added support for non-template forms. 38 | 39 | # 6.3.0 - Version bump to match Store@6.3.0 40 | 41 | https://github.com/angular-redux/store/blob/master/CHANGELOG.md 42 | 43 | # 6.2.0 - Version bump to match Store@6.2.0 44 | 45 | https://github.com/angular-redux/store/blob/master/CHANGELOG.md 46 | 47 | # 6.1.1 - Correct Peer Dependency 48 | 49 | # 6.1.0 - Angular 4 Support, Toolchain Fixes 50 | 51 | We now support versions 2 and 4 of Angular. However Angular 2 support is 52 | deprecated and will be removed in a future major version. 53 | 54 | Also updated the `npm` toolchain to build outputs on `npm publish` instead of 55 | on `npm install`. This fixes a number of toolchain/installation bugs people 56 | have reported. 57 | 58 | # 6.0.0 - The big-rename. 59 | 60 | Due to the impending release of Angular4, the name 'ng2-redux' no longer makes 61 | a ton of sense. The Angular folks have moved to a model where all versions are 62 | just called 'Angular', and we should match that. 63 | 64 | After discussion with the other maintainers, we decided that since we have to 65 | rename things anyway, this is a good opportunity to collect ng2-redux and its 66 | related libraries into a set of scoped packages. This will allow us to grow 67 | the feature set in a coherent but decoupled way. 68 | 69 | As of v6, the following packages are deprecated: 70 | 71 | - ng2-redux 72 | - ng2-redux-router 73 | - ng2-redux-form 74 | 75 | Those packages will still be available on npm for as long as they are being used. 76 | 77 | However we have published the same code under a new package naming scheme: 78 | 79 | - @angular-redux/store (formerly ng2-redux) 80 | - @angular-redux/router (formerly ng2-redux-router) 81 | - @angular-redux/form (formerly ng2-redux-form). 82 | 83 | We have also decided that it's easier to reason about things if these packages 84 | align at least on major versions. So everything has at this point been bumped 85 | to 6.0.0. 86 | 87 | # Breaking changes 88 | 89 | Apart from the rename, the following API changes are noted: 90 | 91 | - @angular-redux/store: none. 92 | - @angular-redux/router: none. 93 | - @angular-redux/form: `NgReduxForms` renamed to `NgReduxFormModule` for consistency. 94 | -------------------------------------------------------------------------------- /packages/form/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Chris Bond 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 | -------------------------------------------------------------------------------- /packages/form/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "src/index.ts", 5 | "languageLevel": ["esnext", "dom", "dom.iterable"], 6 | "umdModuleIds": { 7 | "immutable": "immutable", 8 | "@angular-redux/store": "angularReduxStore" 9 | } 10 | }, 11 | "whitelistedNonPeerDependencies": ["tslib", "immutable"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/form/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angular-redux/form", 3 | "version": "10.0.0", 4 | "description": "Build Angular 2+ forms with Redux", 5 | "author": "Chris Bond", 6 | "license": "MIT", 7 | "homepage": "https://github.com/angular-redux/platform", 8 | "main": "src/index.ts", 9 | "scripts": { 10 | "build": "ng-packagr -p ." 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/angular-redux/platform.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/angular-redux/platform/issues" 18 | }, 19 | "keywords": [ 20 | "angular", 21 | "redux", 22 | "form", 23 | "forms" 24 | ], 25 | "publishConfig": { 26 | "access": "public" 27 | }, 28 | "engines": { 29 | "node": ">=8" 30 | }, 31 | "peerDependencies": { 32 | "@angular-redux/store": "^10.0.0", 33 | "@angular/core": "^7.0.0", 34 | "@angular/forms": "^7.0.0", 35 | "redux": "^4.0.0", 36 | "rxjs": "^6.0.0" 37 | }, 38 | "dependencies": { 39 | "immutable": "^4.0.0-rc.12" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/form/src/compose-reducers.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromJS, List, Map, Set } from 'immutable'; 2 | 3 | import { composeReducers } from './compose-reducers'; 4 | 5 | xdescribe('composeReducers', () => { 6 | const compose = (s1: any, s2: any, s3: any) => { 7 | const r1 = (state = s1) => state; 8 | const r2 = (state = s2) => state; 9 | const r3 = (state = s3) => state; 10 | 11 | const reducer = composeReducers(r1, r2, r3); 12 | 13 | return reducer(undefined, { type: '' }); 14 | }; 15 | 16 | it('can compose plain-object initial states', () => { 17 | const state = compose( 18 | { a: 1 }, 19 | { b: 1 }, 20 | { c: 1 }, 21 | ); 22 | expect(state).toBeDefined(); 23 | expect(state).toEqual({ a: 1, b: 1, c: 1 }); 24 | }); 25 | 26 | it('can compose array states', () => { 27 | const state = compose( 28 | [1], 29 | [2], 30 | [3], 31 | ); 32 | expect(state).toBeDefined(); 33 | expect(state).toEqual([1, 2, 3]); 34 | }); 35 | 36 | it('can compose Immutable::Map initial states', () => { 37 | const state = compose( 38 | fromJS({ a: 1 }), 39 | fromJS({ b: 1 }), 40 | fromJS({ c: 1 }), 41 | ); 42 | expect(Map.isMap(state)).toEqual(true); 43 | 44 | const plain = state.toJS(); 45 | expect(plain).not.toBeNull(); 46 | expect(plain).toEqual({ a: 1, b: 1, c: 1 }); 47 | }); 48 | 49 | it('can compose Immutable::Set initial states', () => { 50 | const state = compose( 51 | Set.of(1, 2, 3), 52 | Set.of(4, 5, 6), 53 | Set.of(), 54 | ); 55 | expect(Set.isSet(state)).toEqual(true); 56 | 57 | const plain = state.toJS(); 58 | expect(plain).not.toBeNull(); 59 | expect(plain).toEqual([1, 2, 3, 4, 5, 6]); 60 | }); 61 | 62 | it('can compose Immutable::OrderedSet initial states', () => { 63 | const state = compose( 64 | Set.of(3, 2, 1), 65 | Set.of(4, 6, 5), 66 | Set.of(), 67 | ); 68 | expect(Set.isSet(state)).toEqual(true); 69 | 70 | const plain = state.toJS(); 71 | expect(plain).not.toBeNull(); 72 | expect(plain).toEqual([3, 2, 1, 4, 6, 5]); 73 | }); 74 | 75 | it('can compose Immutable::List initial states', () => { 76 | const state = compose( 77 | List.of('a', 'b'), 78 | List.of('c', 'd'), 79 | List.of(), 80 | ); 81 | expect(List.isList(state)).toEqual(true); 82 | 83 | const plain = state.toJS(); 84 | expect(plain).not.toBeNull(); 85 | expect(plain).toEqual(['a', 'b', 'c', 'd']); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/form/src/compose-reducers.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Reducer } from 'redux'; 2 | 3 | export const composeReducers = ( 4 | ...reducers: Reducer[] 5 | ): Reducer => (s: any, action: AnyAction) => 6 | reducers.reduce((st, reducer) => reducer(st, action), s); 7 | -------------------------------------------------------------------------------- /packages/form/src/configure.ts: -------------------------------------------------------------------------------- 1 | import { Action, Store } from 'redux'; 2 | 3 | import { AbstractStore, FormStore } from './form-store'; 4 | 5 | /// Use this function in your providers list if you are not using @angular-redux/core. 6 | /// This will allow you to provide a preexisting store that you have already 7 | /// configured, rather than letting @angular-redux/core create one for you. 8 | export const provideReduxForms = (store: Store | any) => { 9 | const abstractStore = wrap(store); 10 | 11 | return [ 12 | { provide: FormStore, useValue: new FormStore(abstractStore as any) }, 13 | ]; 14 | }; 15 | 16 | const wrap = (store: Store | any): AbstractStore => { 17 | const dispatch = (action: Action) => store.dispatch(action); 18 | 19 | const getState = () => store.getState() as T; 20 | 21 | const subscribe = (fn: (state: T) => void) => 22 | store.subscribe(() => fn(store.getState())); 23 | 24 | return { dispatch, getState, subscribe }; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/form/src/connect-array/connect-array-template.ts: -------------------------------------------------------------------------------- 1 | export class ConnectArrayTemplate { 2 | constructor(public $implicit: any, public index: number, public item: any) {} 3 | } 4 | -------------------------------------------------------------------------------- /packages/form/src/connect-array/connect-array.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { ConnectArrayDirective } from './connect-array.directive'; 4 | 5 | const declarations = [ConnectArrayDirective]; 6 | 7 | @NgModule({ 8 | declarations: [...declarations], 9 | exports: [...declarations], 10 | }) 11 | export class NgReduxFormConnectArrayModule {} 12 | -------------------------------------------------------------------------------- /packages/form/src/connect/connect-base.ts: -------------------------------------------------------------------------------- 1 | import { AfterContentInit, Input, OnDestroy } from '@angular/core'; 2 | 3 | import { 4 | AbstractControl, 5 | FormArray, 6 | FormControl, 7 | FormGroup, 8 | NgControl, 9 | } from '@angular/forms'; 10 | 11 | import { Subscription } from 'rxjs'; 12 | 13 | import { Unsubscribe } from 'redux'; 14 | 15 | import { debounceTime } from 'rxjs/operators'; 16 | 17 | import { FormStore } from '../form-store'; 18 | import { State } from '../state'; 19 | 20 | export interface ControlPair { 21 | path: string[]; 22 | control: AbstractControl; 23 | } 24 | 25 | export class ConnectBase implements OnDestroy, AfterContentInit { 26 | get path(): string[] { 27 | const path = 28 | typeof this.connect === 'function' ? this.connect() : this.connect; 29 | 30 | switch (typeof path) { 31 | case 'object': 32 | if (State.empty(path)) { 33 | return []; 34 | } 35 | if (Array.isArray(path)) { 36 | return path as string[]; 37 | } 38 | case 'string': 39 | return (path as string).split(/\./g); 40 | default: 41 | // fallthrough above (no break) 42 | throw new Error( 43 | `Cannot determine path to object: ${JSON.stringify(path)}`, 44 | ); 45 | } 46 | } 47 | @Input() connect?: () => (string | number) | (string | number)[]; 48 | protected store?: FormStore; 49 | protected formGroup: any; 50 | private stateSubscription?: Unsubscribe; 51 | 52 | private formSubscription?: Subscription; 53 | 54 | ngOnDestroy() { 55 | if (this.formSubscription) { 56 | this.formSubscription.unsubscribe(); 57 | } 58 | 59 | if (typeof this.stateSubscription === 'function') { 60 | this.stateSubscription(); // unsubscribe 61 | } 62 | } 63 | 64 | ngAfterContentInit() { 65 | Promise.resolve().then(() => { 66 | this.resetState(); 67 | 68 | if (this.store) { 69 | this.stateSubscription = this.store.subscribe(() => this.resetState()); 70 | } 71 | 72 | Promise.resolve().then(() => { 73 | this.formSubscription = (this.formGroup.valueChanges as any) 74 | .pipe(debounceTime(0)) 75 | .subscribe((values: any) => this.publish(values)); 76 | }); 77 | }); 78 | } 79 | 80 | private descendants(path: string[], formElement: any): ControlPair[] { 81 | const pairs = new Array(); 82 | 83 | if (formElement instanceof FormArray) { 84 | formElement.controls.forEach((c, index) => { 85 | for (const d of this.descendants((path as any).concat([index]), c)) { 86 | pairs.push(d); 87 | } 88 | }); 89 | } else if (formElement instanceof FormGroup) { 90 | for (const k of Object.keys(formElement.controls)) { 91 | pairs.push({ 92 | path: path.concat([k]), 93 | control: formElement.controls[k], 94 | }); 95 | } 96 | } else if ( 97 | formElement instanceof NgControl || 98 | formElement instanceof FormControl 99 | ) { 100 | return [{ path, control: formElement as any }]; 101 | } else { 102 | throw new Error( 103 | `Unknown type of form element: ${formElement.constructor.name}`, 104 | ); 105 | } 106 | 107 | return pairs.filter(p => { 108 | const parent = (p.control as any)._parent; 109 | return parent === this.formGroup.control || parent === this.formGroup; 110 | }); 111 | } 112 | 113 | private resetState() { 114 | const formElement = 115 | this.formGroup.control === undefined 116 | ? this.formGroup 117 | : this.formGroup.control; 118 | 119 | const children = this.descendants([], formElement); 120 | 121 | children.forEach(c => { 122 | const { path, control } = c; 123 | 124 | const value = State.get(this.getState(), this.path.concat(path)); 125 | 126 | if (control.value !== value) { 127 | control.setValue(value); 128 | } 129 | }); 130 | } 131 | 132 | private publish(value: any) { 133 | if (this.store) { 134 | this.store.valueChanged(this.path, this.formGroup, value); 135 | } 136 | } 137 | 138 | private getState() { 139 | if (this.store) { 140 | return this.store.getState(); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /packages/form/src/connect/connect-reactive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input } from '@angular/core'; 2 | 3 | import { FormStore } from '../form-store'; 4 | 5 | import { ConnectBase } from './connect-base'; 6 | 7 | // For reactive forms (without implicit NgForm) 8 | @Directive({ selector: 'form[connect][formGroup]' }) 9 | export class ReactiveConnectDirective extends ConnectBase { 10 | @Input() formGroup: any; 11 | 12 | constructor(protected store: FormStore) { 13 | super(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/form/src/connect/connect.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | 3 | import { NgForm } from '@angular/forms'; 4 | 5 | import { FormStore } from '../form-store'; 6 | import { ConnectBase } from './connect-base'; 7 | 8 | // For template forms (with implicit NgForm) 9 | @Directive({ selector: 'form[connect]:not([formGroup])' }) 10 | export class ConnectDirective extends ConnectBase { 11 | constructor(protected store: FormStore, protected formGroup: NgForm) { 12 | super(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/form/src/connect/connect.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { ReactiveConnectDirective } from './connect-reactive'; 4 | import { ConnectDirective } from './connect.directive'; 5 | 6 | const declarations = [ConnectDirective, ReactiveConnectDirective]; 7 | 8 | @NgModule({ 9 | declarations: [...declarations], 10 | exports: [...declarations], 11 | }) 12 | export class NgReduxFormConnectModule {} 13 | -------------------------------------------------------------------------------- /packages/form/src/exports.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | composeReducers, 3 | ConnectArrayDirective, 4 | ConnectArrayTemplate, 5 | ConnectBase, 6 | ConnectDirective, 7 | defaultFormReducer, 8 | FORM_CHANGED, 9 | FormException, 10 | FormStore, 11 | formStoreFactory, 12 | NgReduxFormConnectArrayModule, 13 | NgReduxFormConnectModule, 14 | NgReduxFormModule, 15 | provideReduxForms, 16 | ReactiveConnectDirective, 17 | } from './index'; 18 | 19 | describe('The @angular-redux/form package exports', () => { 20 | it('should contain the composeReducers function', () => { 21 | expect(composeReducers).toBeDefined(); 22 | }); 23 | 24 | it('should contain the ConnectArrayDirective class', () => { 25 | expect(ConnectArrayDirective).toBeDefined(); 26 | }); 27 | 28 | it('should contain the ConnectArrayTemplate class', () => { 29 | expect(ConnectArrayTemplate).toBeDefined(); 30 | }); 31 | 32 | it('should contain the ConnectBase class', () => { 33 | expect(ConnectBase).toBeDefined(); 34 | }); 35 | 36 | it('should contain the ConnectDirective class', () => { 37 | expect(ConnectDirective).toBeDefined(); 38 | }); 39 | 40 | it('should contain the defaultFormReducer function', () => { 41 | expect(defaultFormReducer).toBeDefined(); 42 | }); 43 | 44 | it('should contain the FORM_CHANGED const', () => { 45 | expect(FORM_CHANGED).toBeDefined(); 46 | }); 47 | 48 | it('should contain the FormException class', () => { 49 | expect(FormException).toBeDefined(); 50 | }); 51 | 52 | it('should contain the FormStore class', () => { 53 | expect(FormStore).toBeDefined(); 54 | }); 55 | 56 | it('should contain the formStoreFactory function', () => { 57 | expect(formStoreFactory).toBeDefined(); 58 | }); 59 | 60 | it('should contain the NgReduxFormConnectArrayModule class', () => { 61 | expect(NgReduxFormConnectArrayModule).toBeDefined(); 62 | }); 63 | 64 | it('should contain the NgReduxFormConnectModule class', () => { 65 | expect(NgReduxFormConnectModule).toBeDefined(); 66 | }); 67 | 68 | it('should contain the NgReduxFormModule class', () => { 69 | expect(NgReduxFormModule).toBeDefined(); 70 | }); 71 | 72 | it('should contain the provideReduxForms function', () => { 73 | expect(provideReduxForms).toBeDefined(); 74 | }); 75 | 76 | it('should contain the ReactiveConnectDirective class', () => { 77 | expect(ReactiveConnectDirective).toBeDefined(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /packages/form/src/form-exception.ts: -------------------------------------------------------------------------------- 1 | export class FormException extends Error { 2 | constructor(msg: string) { 3 | super(msg); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/form/src/form-reducer.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from 'immutable'; 2 | 3 | import { Action } from 'redux'; 4 | 5 | import { FORM_CHANGED } from './form-store'; 6 | 7 | import { State } from './state'; 8 | 9 | export const defaultFormReducer = ( 10 | initialState?: RootState | Collection.Keyed, 11 | ) => { 12 | const reducer = ( 13 | state: RootState | Collection.Keyed | undefined = initialState, 14 | action: Action & { payload?: any }, 15 | ) => { 16 | switch (action.type) { 17 | case FORM_CHANGED: 18 | return State.assign(state, action.payload.path, action.payload.value); 19 | default: 20 | return state; 21 | } 22 | }; 23 | 24 | return reducer; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/form/src/form-store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { NgForm } from '@angular/forms'; 4 | 5 | import { NgRedux } from '@angular-redux/store'; 6 | 7 | import { Action, Unsubscribe } from 'redux'; 8 | 9 | export interface AbstractStore { 10 | /// Dispatch an action 11 | dispatch(action: Action & { payload: any }): void; 12 | 13 | /// Retrieve the current application state 14 | getState(): RootState; 15 | 16 | /// Subscribe to changes in the store 17 | subscribe(fn: (state: RootState) => void): Unsubscribe; 18 | } 19 | 20 | export const FORM_CHANGED = '@@angular-redux/form/FORM_CHANGED'; 21 | 22 | @Injectable() 23 | export class FormStore { 24 | /// NOTE(cbond): The declaration of store is misleading. This class is 25 | /// actually capable of taking a plain Redux store or an NgRedux instance. 26 | /// But in order to make the ng dependency injector work properly, we 27 | /// declare it as an NgRedux type, since the @angular-redux/store use case involves 28 | /// calling the constructor of this class manually (from configure.ts), 29 | /// where a plain store can be cast to an NgRedux. (For our purposes, they 30 | /// have almost identical shapes.) 31 | constructor(private store: NgRedux) {} 32 | 33 | getState() { 34 | return this.store.getState(); 35 | } 36 | 37 | subscribe(fn: (state: any) => void): Unsubscribe { 38 | return this.store.subscribe(() => fn(this.getState())); 39 | } 40 | 41 | valueChanged(path: string[], form: NgForm, value: T) { 42 | this.store.dispatch({ 43 | type: FORM_CHANGED, 44 | payload: { 45 | path, 46 | form, 47 | valid: form.valid === true, 48 | value, 49 | }, 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/form/src/index.ts: -------------------------------------------------------------------------------- 1 | import { composeReducers } from './compose-reducers'; 2 | import { provideReduxForms } from './configure'; 3 | import { FormException } from './form-exception'; 4 | import { defaultFormReducer } from './form-reducer'; 5 | import { AbstractStore, FORM_CHANGED, FormStore } from './form-store'; 6 | import { formStoreFactory, NgReduxFormModule } from './module'; 7 | 8 | import { ConnectBase, ControlPair } from './connect/connect-base'; 9 | import { ReactiveConnectDirective } from './connect/connect-reactive'; 10 | import { ConnectDirective } from './connect/connect.directive'; 11 | import { NgReduxFormConnectModule } from './connect/connect.module'; 12 | 13 | import { ConnectArrayTemplate } from './connect-array/connect-array-template'; 14 | import { ConnectArrayDirective } from './connect-array/connect-array.directive'; 15 | import { NgReduxFormConnectArrayModule } from './connect-array/connect-array.module'; 16 | 17 | // Warning: don't do this: 18 | // export * from './foo' 19 | // ... because it breaks rollup. See 20 | // https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module 21 | export { 22 | AbstractStore, 23 | composeReducers, 24 | ConnectArrayDirective, 25 | ConnectArrayTemplate, 26 | ConnectBase, 27 | ConnectDirective, 28 | ControlPair, 29 | defaultFormReducer, 30 | FORM_CHANGED, 31 | FormException, 32 | FormStore, 33 | formStoreFactory, 34 | NgReduxFormConnectArrayModule, 35 | NgReduxFormConnectModule, 36 | NgReduxFormModule, 37 | provideReduxForms, 38 | ReactiveConnectDirective, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/form/src/module.ts: -------------------------------------------------------------------------------- 1 | import { NgRedux } from '@angular-redux/store'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { NgReduxFormConnectArrayModule } from './connect-array/connect-array.module'; 6 | import { NgReduxFormConnectModule } from './connect/connect.module'; 7 | import { FormStore } from './form-store'; 8 | 9 | export function formStoreFactory(ngRedux: NgRedux) { 10 | return new FormStore(ngRedux); 11 | } 12 | 13 | @NgModule({ 14 | imports: [ 15 | FormsModule, 16 | ReactiveFormsModule, 17 | NgReduxFormConnectModule, 18 | NgReduxFormConnectArrayModule, 19 | ], 20 | exports: [NgReduxFormConnectModule, NgReduxFormConnectArrayModule], 21 | providers: [ 22 | { 23 | provide: FormStore, 24 | useFactory: formStoreFactory, 25 | deps: [NgRedux], 26 | }, 27 | ], 28 | }) 29 | export class NgReduxFormModule {} 30 | -------------------------------------------------------------------------------- /packages/form/src/shims.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CheckboxControlValueAccessor, 3 | ControlContainer, 4 | ControlValueAccessor, 5 | RadioControlValueAccessor, 6 | SelectControlValueAccessor, 7 | SelectMultipleControlValueAccessor, 8 | } from '@angular/forms'; 9 | 10 | export function controlPath(name: string, parent: ControlContainer): string[] { 11 | return [...(parent.path || []), name]; 12 | } 13 | 14 | const BUILTIN_ACCESSORS = [ 15 | CheckboxControlValueAccessor, 16 | SelectControlValueAccessor, 17 | SelectMultipleControlValueAccessor, 18 | RadioControlValueAccessor, 19 | ]; 20 | 21 | export function isBuiltInAccessor( 22 | valueAccessor: ControlValueAccessor, 23 | ): boolean { 24 | return BUILTIN_ACCESSORS.some(a => valueAccessor.constructor === a); 25 | } 26 | -------------------------------------------------------------------------------- /packages/form/src/tests.utilities.ts: -------------------------------------------------------------------------------- 1 | import { flushMicrotasks } from '@angular/core/testing'; 2 | 3 | import { isCollection } from 'immutable'; 4 | import { Middleware } from 'redux'; 5 | // redux-logger is a dev dependency in the workspace 6 | // tslint:disable-next-line:no-implicit-dependencies 7 | import { createLogger } from 'redux-logger'; 8 | 9 | export const logger: Middleware = createLogger({ 10 | level: 'debug', 11 | collapsed: true, 12 | predicate: () => true, 13 | stateTransformer: state => { 14 | const newState: any = new Object(); 15 | 16 | for (const i of Object.keys(state)) { 17 | newState[i] = isCollection(state[i]) ? state[i].toJS() : state[i]; 18 | } 19 | 20 | return newState; 21 | }, 22 | }); 23 | 24 | export const simulateUserTyping = ( 25 | control: any, 26 | text: string, 27 | ): Promise => { 28 | return new Promise((resolve, reject) => { 29 | try { 30 | dispatchKeyEvents(control, text); 31 | resolve(); 32 | } catch (error) { 33 | console.error('Failed to dispatch typing events', error); 34 | reject(error); 35 | } finally { 36 | flushMicrotasks(); 37 | } 38 | }); 39 | }; 40 | 41 | export const dispatchKeyEvents = (control: any, text: string) => { 42 | if (!text) { 43 | return; 44 | } 45 | 46 | control.focus(); 47 | 48 | for (const character of text) { 49 | const c = character.charCodeAt(0); 50 | 51 | const keyboardEventFactory = (eventType: string, value: any) => { 52 | return new KeyboardEvent(eventType, { 53 | altKey: false, 54 | cancelable: false, 55 | bubbles: true, 56 | ctrlKey: false, 57 | metaKey: false, 58 | detail: value, 59 | view: window, 60 | shiftKey: false, 61 | repeat: false, 62 | key: value, 63 | }); 64 | }; 65 | 66 | const eventFactory = (eventType: string) => { 67 | return new Event(eventType, { 68 | bubbles: true, 69 | cancelable: false, 70 | }); 71 | }; 72 | 73 | control.dispatchEvent(keyboardEventFactory('keydown', c)); 74 | control.dispatchEvent(keyboardEventFactory('keypress', c)); 75 | control.dispatchEvent(keyboardEventFactory('keyup', c)); 76 | control.value += character; 77 | control.dispatchEvent(eventFactory('input')); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /packages/router/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [10.0.0](https://github.com/angular-redux/platform/compare/v9.0.1...v10.0.0) (2019-05-04) 7 | 8 | ### chore 9 | 10 | - **build:** use ng-packagr ([#37](https://github.com/angular-redux/platform/issues/37)) ([dffe23a](https://github.com/angular-redux/platform/commit/dffe23a)), closes [#9](https://github.com/angular-redux/platform/issues/9) 11 | - **linting:** add global tslint rules ([#35](https://github.com/angular-redux/platform/issues/35)) ([336cc60](https://github.com/angular-redux/platform/commit/336cc60)), closes [#4](https://github.com/angular-redux/platform/issues/4) 12 | 13 | ### Features 14 | 15 | - upgrade to angular 7 ([#72](https://github.com/angular-redux/platform/issues/72)) ([18d9245](https://github.com/angular-redux/platform/commit/18d9245)), closes [#65](https://github.com/angular-redux/platform/issues/65) [#66](https://github.com/angular-redux/platform/issues/66) [#67](https://github.com/angular-redux/platform/issues/67) [#68](https://github.com/angular-redux/platform/issues/68) [#69](https://github.com/angular-redux/platform/issues/69) [#70](https://github.com/angular-redux/platform/issues/70) [#71](https://github.com/angular-redux/platform/issues/71) [#74](https://github.com/angular-redux/platform/issues/74) [#79](https://github.com/angular-redux/platform/issues/79) 16 | 17 | ### BREAKING CHANGES 18 | 19 | - Upgrades Angular dependencies to v7 20 | - **build:** - changes the output to conform to the Angular Package Format. This may cause subtle differences in consumption behaviour 21 | 22 | * peer dependencies have been corrected to actual dependencies 23 | 24 | - **linting:** - ConnectArray has been renamed to ConnectArrayDirective 25 | 26 | * ReactiveConnect has been renamed to ReactiveConnectDirective 27 | * Connect has been renamed to ConnectDirective 28 | * interfaces with an "I" prefix have had that prefix removed (e.g "IAppStore" -> "AppStore") 29 | 30 | # 9.0.0 - Angular 6, RxJS 6 Support 31 | 32 | Adapts to breaking changes in Angular 6 and RxJS 6. Also updates to Typescript 2.7.2. 33 | 34 | # 7.0.0 - Angular 5+ only support 35 | 36 | - Update to Angular 5 compiler 37 | - Update RxJS, change to use let-able operators 38 | - Requires @angular-redux/store 7+ 39 | 40 | ** Breaking Change ** 41 | 42 | - NgReduxRouterModule now needs to be imported with `.forRoot` 43 | 44 | **before** 45 | 46 | ```ts 47 | @NgModule({ 48 | declarations: [AppComponent], 49 | imports: [ 50 | RouterModule.forRoot(appRoutes), 51 | /* .... */ 52 | NgReduxRouterModule, 53 | ], 54 | bootstrap: [AppComponent], 55 | }) 56 | export class AppModule {} 57 | ``` 58 | 59 | **after** 60 | 61 | ```ts 62 | @NgModule({ 63 | declarations: [AppComponent], 64 | imports: [ 65 | RouterModule.forRoot(appRoutes), 66 | /* .... */ 67 | NgReduxRouterModule.forRoot(), 68 | ], 69 | bootstrap: [AppComponent], 70 | }) 71 | export class AppModule {} 72 | ``` 73 | 74 | # 6.4.0 - Angular 5 Support 75 | 76 | Added support for Angular 5. 77 | 78 | # 6.3.1 - Toolchain Update 79 | 80 | - Typescript 2.4.1 81 | - Compile with `strict: true` in tsconfig.json 82 | - Fix for issue #17. 83 | - Add package-lock.json for contributors using npm 5+. 84 | 85 | # 6.3.0 - Version bump to match Store@6.3.0 86 | 87 | https://github.com/angular-redux/store/blob/master/CHANGELOG.md 88 | 89 | # 6.2.0 - Version bump to match Store@6.2.0 90 | 91 | https://github.com/angular-redux/store/blob/master/CHANGELOG.md 92 | 93 | # 6.1.0 - Angular 4 Support 94 | 95 | We now support versions 2 and 4 of Angular. Version 2 support is deprecated and 96 | support will be removed in the next major version. 97 | 98 | # 6.0.1 99 | 100 | - Include the `src`-folder in the release so webpack can build source maps. 101 | 102 | # 6.0.0 - The big-rename. 103 | 104 | Due to the impending release of Angular4, the name 'ng2-redux' no longer makes a 105 | ton of sense. The Angular folks have moved to a model where all versions are 106 | just called 'Angular', and we should match that. 107 | 108 | After discussion with the other maintainers, we decided that since we have to 109 | rename things anyway, this is a good opportunity to collect ng2-redux and its 110 | related libraries into a set of scoped packages. This will allow us to grow the 111 | feature set in a coherent but decoupled way. 112 | 113 | As of v6, the following packages are deprecated: 114 | 115 | - ng2-redux 116 | - ng2-redux-router 117 | - ng2-redux-form 118 | 119 | Those packages will still be available on npm for as long as they are being 120 | used. 121 | 122 | However we have published the same code under a new package naming scheme: 123 | 124 | - @angular-redux/store (formerly ng2-redux) 125 | - @angular-redux/router (formerly ng2-redux-router) 126 | - @angular-redux/form (formerly ng2-redux-form). 127 | 128 | We have also decided that it's easier to reason about things if these packages 129 | align at least on major versions. So everything has at this point been bumped to 130 | 6.0.0. 131 | 132 | # Breaking changes 133 | 134 | Apart from the rename, the following API changes are noted: 135 | 136 | - @angular-redux/store: none. 137 | - @angular-redux/router: none. 138 | - @angular-redux/form: `NgReduxForms` renamed to `NgReduxFormModule` for 139 | consistency. 140 | -------------------------------------------------------------------------------- /packages/router/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dag Stuan 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 | -------------------------------------------------------------------------------- /packages/router/README.md: -------------------------------------------------------------------------------- 1 | # @angular-redux/router 2 | 3 | [![npm version](https://img.shields.io/npm/v/@angular-redux/router.svg)](https://www.npmjs.com/package/@angular-redux/router) 4 | [![downloads per month](https://img.shields.io/npm/dm/@angular-redux/router.svg)](https://www.npmjs.com/package/@angular-redux/router) 5 | 6 | Bindings to connect @angular/router to @angular-redux/core 7 | 8 | ## Setup 9 | 10 | 1. Use npm to install the bindings: 11 | 12 | ``` 13 | npm install @angular-redux/router --save 14 | ``` 15 | 16 | 2. Use the `routerReducer` when providing `Store`: 17 | 18 | ```ts 19 | import { combineReducers } from 'redux'; 20 | import { routerReducer } from '@angular-redux/router'; 21 | 22 | export default combineReducers({ 23 | // your reducers.. 24 | router: routerReducer, 25 | }); 26 | ``` 27 | 28 | 3. Add the bindings to your root module. 29 | 30 | ```ts 31 | import { NgModule } from '@angular/core'; 32 | import { NgReduxModule, NgRedux } from '@angular-redux/core'; 33 | import { NgReduxRouterModule, NgReduxRouter } from '@angular-redux/router'; 34 | import { RouterModule } from '@angular/router'; 35 | import { routes } from './routes'; 36 | 37 | @NgModule({ 38 | imports: [ 39 | RouterModule.forRoot(routes), 40 | NgReduxModule, 41 | NgReduxRouterModule.forRoot(), 42 | // ...your imports 43 | ], 44 | // Other stuff.. 45 | }) 46 | export class AppModule { 47 | constructor(ngRedux: NgRedux, ngReduxRouter: NgReduxRouter) { 48 | ngRedux.configureStore(/* args */); 49 | ngReduxRouter.initialize(/* args */); 50 | } 51 | } 52 | ``` 53 | 54 | ## What if I use Immutable.js with my Redux store? 55 | 56 | When using a wrapper for your store's state, such as Immutable.js, you will need to change two things from the standard setup: 57 | 58 | 1. Provide your own reducer function that will receive actions of type `UPDATE_LOCATION` and return the payload merged into state. 59 | 2. Pass a selector to access the payload state and convert it to a JS object via the `selectLocationFromState` option on `NgReduxRouter`'s `initialize()`. 60 | 61 | These two hooks will allow you to store the state that this library uses in whatever format or wrapper you would like. 62 | 63 | ## What if I have a different way of supplying the current URL of the page? 64 | 65 | Depending on your app's needs. It may need to supply the current URL of the page differently than directly 66 | through the router. This can be achieved by initializing the bindings with a second argument: `urlState$`. 67 | The `urlState$` argument lets you give `NgReduxRouter` an `Observable` of the current URL of the page. 68 | If this argument is not given to the bindings, it defaults to subscribing to the `@angular/router`'s events, and 69 | getting the URL from there. 70 | 71 | ## Examples 72 | 73 | - [Example-app: An example of using @angular-redux/router along with the other companion packages.](https://github.com/angular-redux/platform/tree/master/packages/example-app) 74 | -------------------------------------------------------------------------------- /packages/router/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "src/index.ts", 5 | "languageLevel": ["esnext", "dom", "dom.iterable"], 6 | "umdModuleIds": { 7 | "@angular-redux/store": "angularReduxStore" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angular-redux/router", 3 | "version": "10.0.0", 4 | "description": "Keep your Angular 2+ router state in Redux.", 5 | "author": "Dag Stuan", 6 | "license": "MIT", 7 | "homepage": "https://github.com/angular-redux/platform", 8 | "main": "src/index.ts", 9 | "scripts": { 10 | "build": "ng-packagr -p ." 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/angular-redux/platform.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/angular-redux/platform/issues" 18 | }, 19 | "keywords": [ 20 | "angular", 21 | "angular2", 22 | "redux", 23 | "routing", 24 | "router" 25 | ], 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "engines": { 30 | "node": ">=8" 31 | }, 32 | "peerDependencies": { 33 | "@angular-redux/store": "^10.0.0", 34 | "@angular/common": "^7.0.0", 35 | "@angular/core": "^7.0.0", 36 | "@angular/router": "^7.0.0", 37 | "redux": "^4.0.0", 38 | "rxjs": "^6.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/router/src/actions.ts: -------------------------------------------------------------------------------- 1 | export const UPDATE_LOCATION: string = '@angular-redux/router::UPDATE_LOCATION'; 2 | -------------------------------------------------------------------------------- /packages/router/src/exports.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NgReduxRouter, 3 | NgReduxRouterModule, 4 | routerReducer, 5 | UPDATE_LOCATION, 6 | } from './index'; 7 | 8 | describe('The @angular-redux/router package exports', () => { 9 | it('should contain the NgReduxRouter class', () => { 10 | expect(NgReduxRouter).toBeDefined(); 11 | }); 12 | 13 | it('should contain the NgReduxRouterModule class', () => { 14 | expect(NgReduxRouterModule).toBeDefined(); 15 | }); 16 | 17 | it('should contain the routerReducer function', () => { 18 | expect(routerReducer).toBeDefined(); 19 | }); 20 | 21 | it('should contain the UPDATE_LOCATION const', () => { 22 | expect(UPDATE_LOCATION).toBeDefined(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/router/src/index.ts: -------------------------------------------------------------------------------- 1 | import { UPDATE_LOCATION } from './actions'; 2 | import { NgReduxRouterModule } from './module'; 3 | import { RouterAction, routerReducer } from './reducer'; 4 | import { NgReduxRouter } from './router'; 5 | 6 | // Warning: don't do this: 7 | // export * from './foo' 8 | // ... because it breaks rollup. See 9 | // https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module 10 | export { 11 | NgReduxRouter, 12 | NgReduxRouterModule, 13 | RouterAction, 14 | routerReducer, 15 | UPDATE_LOCATION, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/router/src/module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { NgReduxRouter } from './router'; 3 | 4 | @NgModule() 5 | export class NgReduxRouterModule { 6 | static forRoot(): ModuleWithProviders { 7 | return { 8 | ngModule: NgReduxRouterModule, 9 | providers: [NgReduxRouter], 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/router/src/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | 3 | import { UPDATE_LOCATION } from './actions'; 4 | 5 | export const DefaultRouterState: string = ''; 6 | 7 | export interface RouterAction extends Action { 8 | payload?: string; 9 | } 10 | 11 | export function routerReducer( 12 | state: string | undefined = DefaultRouterState, 13 | action: RouterAction, 14 | ): string { 15 | switch (action.type) { 16 | case UPDATE_LOCATION: 17 | return action.payload || DefaultRouterState; 18 | default: 19 | return state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/router/src/router.ts: -------------------------------------------------------------------------------- 1 | import { NgRedux } from '@angular-redux/store'; 2 | import { Location } from '@angular/common'; 3 | import { Injectable } from '@angular/core'; 4 | import { NavigationEnd, Router } from '@angular/router'; 5 | import { Observable, Subscription } from 'rxjs'; 6 | import { distinctUntilChanged, filter, map } from 'rxjs/operators'; 7 | import { UPDATE_LOCATION } from './actions'; 8 | 9 | @Injectable() 10 | export class NgReduxRouter { 11 | private initialized = false; 12 | private currentLocation?: string; 13 | private initialLocation?: string; 14 | private urlState?: Observable; 15 | 16 | private urlStateSubscription?: Subscription; 17 | private reduxSubscription?: Subscription; 18 | 19 | constructor( 20 | private router: Router, 21 | private ngRedux: NgRedux, 22 | private location: Location, 23 | ) {} 24 | 25 | /** 26 | * Destroys the bindings between @angular-redux/router and @angular/router. 27 | * This method unsubscribes from both @angular-redux/router and @angular router, in case 28 | * your app needs to tear down the bindings without destroying Angular or Redux 29 | * at the same time. 30 | */ 31 | destroy() { 32 | if (this.urlStateSubscription) { 33 | this.urlStateSubscription.unsubscribe(); 34 | } 35 | 36 | if (this.reduxSubscription) { 37 | this.reduxSubscription.unsubscribe(); 38 | } 39 | 40 | this.initialized = false; 41 | } 42 | 43 | /** 44 | * Initialize the bindings between @angular-redux/router and @angular/router 45 | * 46 | * This should only be called once for the lifetime of your app, for 47 | * example in the constructor of your root component. 48 | * 49 | * 50 | * @param selectLocationFromState Optional: If your 51 | * router state is in a custom location, supply this argument to tell the 52 | * bindings where to find the router location in the state. 53 | * @param urlState$ Optional: If you have a custom setup 54 | * when listening to router changes, or use a different router than @angular/router 55 | * you can supply this argument as an Observable of the current url state. 56 | */ 57 | initialize( 58 | selectLocationFromState: (state: any) => string = state => state.router, 59 | urlState$?: Observable | undefined, 60 | ) { 61 | if (this.initialized) { 62 | throw new Error( 63 | '@angular-redux/router already initialized! If you meant to re-initialize, call destroy first.', 64 | ); 65 | } 66 | 67 | this.selectLocationFromState = selectLocationFromState; 68 | 69 | this.urlState = urlState$ || this.getDefaultUrlStateObservable(); 70 | 71 | this.listenToRouterChanges(); 72 | this.listenToReduxChanges(); 73 | this.initialized = true; 74 | } 75 | 76 | private selectLocationFromState: (state: any) => string = state => 77 | state.router; 78 | 79 | private getDefaultUrlStateObservable() { 80 | return this.router.events.pipe( 81 | filter(event => event instanceof NavigationEnd), 82 | map(() => this.location.path()), 83 | distinctUntilChanged(), 84 | ); 85 | } 86 | 87 | private getLocationFromStore(useInitial: boolean = false) { 88 | return ( 89 | this.selectLocationFromState(this.ngRedux.getState()) || 90 | (useInitial ? this.initialLocation : '') 91 | ); 92 | } 93 | 94 | private listenToRouterChanges() { 95 | const handleLocationChange = (location: string) => { 96 | if (this.currentLocation === location) { 97 | // Dont dispatch changes if we haven't changed location. 98 | return; 99 | } 100 | 101 | this.currentLocation = location; 102 | if (this.initialLocation === undefined) { 103 | this.initialLocation = location; 104 | 105 | // Fetch initial location from store and make sure 106 | // we dont dispath an event if the current url equals 107 | // the initial url. 108 | const locationFromStore = this.getLocationFromStore(); 109 | if (locationFromStore === this.currentLocation) { 110 | return; 111 | } 112 | } 113 | 114 | this.ngRedux.dispatch({ 115 | type: UPDATE_LOCATION, 116 | payload: location, 117 | }); 118 | }; 119 | 120 | if (this.urlState) { 121 | this.urlStateSubscription = this.urlState.subscribe(handleLocationChange); 122 | } 123 | } 124 | 125 | private listenToReduxChanges() { 126 | const handleLocationChange = (location: string) => { 127 | if (this.initialLocation === undefined) { 128 | // Wait for router to set initial location. 129 | return; 130 | } 131 | 132 | const locationInStore = this.getLocationFromStore(true); 133 | if (this.currentLocation === locationInStore) { 134 | // Dont change router location if its equal to the one in the store. 135 | return; 136 | } 137 | 138 | this.currentLocation = location; 139 | this.router.navigateByUrl(location); 140 | }; 141 | 142 | this.reduxSubscription = this.ngRedux 143 | .select(state => this.selectLocationFromState(state)) 144 | .pipe(distinctUntilChanged()) 145 | .subscribe(handleLocationChange); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /packages/store/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 William Buchwalter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /packages/store/articles/action-creator-service.md: -------------------------------------------------------------------------------- 1 | # Using Angular Services in your Action Creators 2 | 3 | In order to use services in action creators, we need to integrate 4 | them into Angular's dependency injector. 5 | 6 | We may as well adopt a more class-based approach to satisfy 7 | Angular 2's OOP idiom, and to allow us to 8 | 9 | 1. make our actions `@Injectable()`, and 10 | 2. inject other services for our action creators to use. 11 | 12 | Take a look at this example, which injects NgRedux to access 13 | `dispatch` and `getState` (a replacement for `redux-thunk`), 14 | and a simple `RandomNumberService` to show a side effect. 15 | 16 | ```typescript 17 | import { Injectable } from '@angular/core'; 18 | import { NgRedux } from '@angular-redux/store'; 19 | import * as Redux from 'redux'; 20 | import { RootState } from '../store'; 21 | import { RandomNumberService } from '../services/random-number'; 22 | 23 | @Injectable() 24 | export class CounterActions { 25 | constructor( 26 | private ngRedux: NgRedux, 27 | private randomNumberService: RandomNumberService, 28 | ) {} 29 | 30 | static INCREMENT_COUNTER: string = 'INCREMENT_COUNTER'; 31 | static DECREMENT_COUNTER: string = 'DECREMENT_COUNTER'; 32 | static RANDOMIZE_COUNTER: string = 'RANDOMIZE_COUNTER'; 33 | 34 | // Basic action 35 | increment(): void { 36 | this.ngRedux.dispatch({ type: CounterActions.INCREMENT_COUNTER }); 37 | } 38 | 39 | // Basic action 40 | decrement(): void { 41 | this.ngRedux.dispatch({ type: CounterActions.DECREMENT_COUNTER }); 42 | } 43 | 44 | // Async action. 45 | incrementAsync(delay: number = 1000): void { 46 | setTimeout(this.increment.bind(this), delay); 47 | } 48 | 49 | // State-dependent action 50 | incrementIfOdd(): void { 51 | const { counter } = this.ngRedux.getState(); 52 | if (counter % 2 !== 0) { 53 | this.increment(); 54 | } 55 | } 56 | 57 | // Service-dependent action 58 | randomize(): void { 59 | this.ngRedux.dispatch({ 60 | type: CounterActions.RANDOMIZE_COUNTER, 61 | payload: this.randomNumberService.pick(), 62 | }); 63 | } 64 | } 65 | ``` 66 | 67 | To use these action creators, we can just go ahead and inject 68 | them into our component: 69 | 70 | ```typescript 71 | import { Component } from '@angular/core'; 72 | import { NgRedux, select } from '@angular-redux/store'; 73 | import { CounterActions } from '../actions/counter-actions'; 74 | import { RandomNumberService } from '../services/random-number'; 75 | 76 | @Component({ 77 | selector: 'counter', 78 | providers: [CounterActions, RandomNumberService], 79 | template: ` 80 |

81 | Clicked: {{ counter$ | async }} times 82 | 83 | 84 | 85 | 86 | 87 |

88 | `, 89 | }) 90 | export class Counter { 91 | @select('counter') counter$: any; 92 | 93 | constructor(private actions: CounterActions) {} 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /packages/store/articles/di-middleware.md: -------------------------------------------------------------------------------- 1 | # Using Angular 2 Services in your Middleware 2 | 3 | Again, we just want to use Angular DI the way it was meant to be used. 4 | 5 | Here's a contrived example that fetches a name from a remote API using Angular's 6 | `Http` service: 7 | 8 | ```typescript 9 | import { Injectable } from '@angular/core'; 10 | import { Http } from '@angular/http'; 11 | import 'rxjs/add/operator/toPromise'; 12 | 13 | @Injectable() 14 | export class LogRemoteName { 15 | constructor(private http: Http) {} 16 | 17 | middleware = store => next => action => { 18 | console.log('getting user name'); 19 | this.http.get('http://jsonplaceholder.typicode.com/users/1') 20 | .map(response => { 21 | console.log('got name:', response.json().name); 22 | return next(action); 23 | }) 24 | .catch(err => console.log('get name failed:', err)); 25 | } 26 | return next(action); 27 | } 28 | ``` 29 | 30 | As with the action example above, we've attached our middleware function to 31 | an `@Injectable` class that can itself receive services from Angular's 32 | dependency injector. 33 | 34 | Note the arrow function called `middleware`: this is what we can pass to the 35 | middlewares parameter when we initialize ngRedux in our top-level component. We 36 | use an arrow function to make sure that what we pass to ngRedux has a 37 | properly-bound function context. 38 | 39 | ```typescript 40 | import { NgModule } from '@angular/core'; 41 | import { NgReduxModule, NgRedux } from '@angular-redux/store'; 42 | import reduxLogger from 'redux-logger'; 43 | import { LogRemoteName } from './middleware/log-remote-name'; 44 | 45 | @NgModule({ 46 | /* ... */ 47 | imports: [, /* ... */ NgReduxModule], 48 | providers: [ 49 | LogRemoteName, 50 | /* ... */ 51 | ], 52 | }) 53 | export class AppModule { 54 | constructor( 55 | private ngRedux: NgRedux, 56 | logRemoteName: LogRemoteName, 57 | ) { 58 | const middleware = [reduxLogger, logRemoteName.middleware]; 59 | this.ngRedux.configureStore(rootReducer, {}, middleware); 60 | } 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /packages/store/articles/epics.md: -------------------------------------------------------------------------------- 1 | # Side-Effect Management Using Epics 2 | 3 | `@angular-redux/store` also works well with the `Epic` feature of 4 | [redux-observable](https://github.com/redux-observable). For 5 | example, a common use case for a side-effect is making an API call; while 6 | we can use asynchronous actions for this, epics provide a much cleaner 7 | approach. 8 | 9 | Consider the following example of a user login implementation. First, we 10 | create some trivial actions: 11 | 12 | **session.actions.ts:** 13 | 14 | ```typescript 15 | import { Injectable } from '@angular/core'; 16 | import { NgRedux } from '@angular-redux/store'; 17 | import { IAppState } from '../reducers'; 18 | 19 | @Injectable() 20 | export class SessionActions { 21 | static LOGIN_USER = 'LOGIN_USER'; 22 | static LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS'; 23 | static LOGIN_USER_ERROR = 'LOGIN_USER_ERROR'; 24 | static LOGOUT_USER = 'LOGOUT_USER'; 25 | 26 | constructor(private ngRedux: NgRedux) {} 27 | 28 | loginUser(credentials) { 29 | this.ngRedux.dispatch({ 30 | type: SessionActions.LOGIN_USER, 31 | payload: credentials, 32 | }); 33 | } 34 | 35 | logoutUser() { 36 | this.ngRedux.dispatch({ type: SessionActions.LOGOUT_USER }); 37 | } 38 | } 39 | ``` 40 | 41 | Next, we create an `@Injectable SessionEpic` service: 42 | 43 | **session.epics.ts:** 44 | 45 | ```typescript 46 | import { Injectable } from '@angular/core'; 47 | import { Http } from '@angular/http'; 48 | import { ActionsObservable } from 'redux-observable'; 49 | import { SessionActions } from '../actions/session.actions'; 50 | import { Observable } from 'rxjs/Observable'; 51 | import 'rxjs/add/observable/of'; 52 | import 'rxjs/add/operator/mergeMap'; 53 | import 'rxjs/add/operator/map'; 54 | import 'rxjs/add/operator/catch'; 55 | 56 | const BASE_URL = '/api'; 57 | 58 | @Injectable() 59 | export class SessionEpics { 60 | constructor(private http: Http) {} 61 | 62 | login = (action$: ActionsObservable) => { 63 | return action$.ofType(SessionActions.LOGIN_USER).mergeMap(({ payload }) => { 64 | return this.http 65 | .post(`${BASE_URL}/auth/login`, payload) 66 | .map(result => ({ 67 | type: SessionActions.LOGIN_USER_SUCCESS, 68 | payload: result.json().meta, 69 | })) 70 | .catch(error => 71 | Observable.of({ 72 | type: SessionActions.LOGIN_USER_ERROR, 73 | }), 74 | ); 75 | }); 76 | }; 77 | } 78 | ``` 79 | 80 | This needs to be a service so that we can inject Angular's `HTTP` service. 81 | However in this case we're using the same "arrow function bind trick" as we 82 | did for the dependency-injected middleware cookbook above. 83 | 84 | This allows us to configure our Redux store with the new epic as follows: 85 | 86 | **app.component.ts:** 87 | 88 | ```typescript 89 | import { NgModule } from '@angular/core'; 90 | import { NgReduxModule, NgRedux } from '@angular-redux/store'; 91 | import { createEpicMiddleware } from 'redux-observable'; 92 | import rootReducer from './reducers'; 93 | import { SessionEpics } from './epics'; 94 | 95 | @NgModule({ 96 | /* ... */ 97 | imports: [, /* ... */ NgReduxModule], 98 | providers: [ 99 | SessionEpics, 100 | /* ... */ 101 | ], 102 | }) 103 | export class AppModule { 104 | constructor( 105 | private ngRedux: NgRedux, 106 | private epics: SessionEpics, 107 | ) { 108 | const middleware = [createEpicMiddleware(this.epics.login)]; 109 | ngRedux.configureStore(rootReducer, {}, middleware); 110 | } 111 | } 112 | ``` 113 | 114 | Now, whenever you dispatch a "USER_LOGIN" action, the epic will trigger the 115 | HTTP request, and fire a corresponding success or failure action. This allows 116 | you to keep your action creators very simple, and to cleanly describe your 117 | side effects as a set of simple RxJS epics. 118 | -------------------------------------------------------------------------------- /packages/store/articles/images/counter-hooked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/store/articles/images/counter-hooked.png -------------------------------------------------------------------------------- /packages/store/articles/images/counter-unhooked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/store/articles/images/counter-unhooked.png -------------------------------------------------------------------------------- /packages/store/articles/images/devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/store/articles/images/devtools.png -------------------------------------------------------------------------------- /packages/store/articles/images/startup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/store/articles/images/startup.png -------------------------------------------------------------------------------- /packages/store/articles/immutable-js.md: -------------------------------------------------------------------------------- 1 | # Using ImmutableJS 2 | 3 | ## What is ImmutableJS 4 | 5 | [ImmutableJS](https://facebook.github.io/immutable-js/) is a library that 6 | provides efficient immutable data structures for JavaScript, and it's a great 7 | tool to help enforce immutability in your reducers. 8 | 9 | It provides two main structures, `Map` and `List`, which are analogues of 10 | `Object` and `Array`. However they provide an efficiently-implemented 11 | copy-on-write semantic that can help you enforce immutability in your reducers 12 | without the performance problems of `Object.freeze` or the GC churn of 13 | `Object.assign`. 14 | 15 | It also provides helper methods for deeply querying (`getIn`) or modifying 16 | (`setIn`) nested objects. 17 | 18 | ## Why do I care? 19 | 20 | Many people who do Redux implement their stores in terms of ImmutableJS data 21 | structures. This provides a safety-net against accidental mutation of the store, 22 | either in reducers or in reactive operator sequences attached to your 23 | observables. However it comes at a syntactic cost: with `Immutable.Map`, you 24 | can no longer easily dereference properties: 25 | 26 | ```typescript 27 | const mutableFoo = { 28 | foo: 1, 29 | }; 30 | 31 | const foo: number = mutableFoo.foo; 32 | ``` 33 | 34 | becomes: 35 | 36 | ```typescript 37 | const immutableFoo: Map = Immutable.fromJS({ 38 | foo: 1; 39 | }); 40 | 41 | const foo: number = immutableFoo.get('foo'); 42 | ``` 43 | 44 | ## Pre 3.3.0: 45 | 46 | Previous to 3.3.0 we were forced to choose between the guarantees of ImmutableJS 47 | and the syntactic convenience of raw objects: 48 | 49 | ### Raw Objects in the Store 50 | 51 | Imagine a store with the following shape: 52 | 53 | ```typescript 54 | { 55 | totalCount: 0, 56 | counts: { 57 | firstCount: 0, 58 | secondCount: 0 59 | } 60 | }; 61 | ``` 62 | 63 | Without ImmutableJS, we could write in our components: 64 | 65 | ```typescript 66 | // Path selector 67 | @select(['counts', 'firstCount']) firstCount$: Observable; 68 | 69 | // Selecting an immutable object 70 | @select() counts$: Observable; 71 | 72 | constructor() { 73 | this.counts$.map(counts: ICount => { 74 | // oh noes: bad mutation, subtle bug! 75 | return counts.firstCount++; 76 | }); 77 | } 78 | ``` 79 | 80 | We get the syntactic convenience of raw objects, but no protection against 81 | accidental mutation. 82 | 83 | ### Immutable Objects in the Store 84 | 85 | Here's that same conceptual store, defined immutably: 86 | 87 | ```typescript 88 | Immutable.Map({ 89 | totalCount: 0, 90 | counts: Immutable.map({ 91 | firstCount: 0, 92 | secondCount: 0, 93 | }), 94 | }); 95 | ``` 96 | 97 | Now we are protected against accidental mutation: 98 | 99 | ```typescript 100 | constructor() { 101 | this.counts$.map(counts: Map => { 102 | // Type error: firstCount is not a property of Immutable.Map. 103 | return counts.firstCount++; 104 | }); 105 | } 106 | ``` 107 | 108 | But we are restricted to using the function selectors. which are less 109 | declarative: 110 | 111 | ```typescript 112 | // Path selector no longer possible: must supply a function. 113 | @select(s => s.getIn(['counts', 'firstCount']) firstCount$: Observable; 114 | @select(s => s.get('counts')) counts$: Observable>; 115 | 116 | constructor() { 117 | this.counts$.map(counts: Map => { 118 | // Correct: we are forced into the non-mutating approach. 119 | return counts.get('firstCount') + 1; 120 | }); 121 | } 122 | ``` 123 | 124 | ## Post 3.3.0: 125 | 126 | In `@angular-redux/store` 3.3.0 we've allowed you to have your cake and eat it too: the 127 | `@select` decorator can now detect if the selected state is an ImmutableJS 128 | construct and call `.get` or `.getIn` for you. 129 | 130 | So you no longer have to sacrifice declarative syntax for mutation-safety: 131 | 132 | ```typescript 133 | // Path selector 134 | @select(['counts', 'firstCount']) firstCount$: Observable; 135 | 136 | // Selecting an immutable object 137 | @select() counts$: Observable>; 138 | 139 | constructor() { 140 | this.counts$.map(counts: Map => { 141 | // Correct: we are forced into the non-mutating approach. 142 | return counts.get('firstCount') + 1; 143 | }); 144 | } 145 | ``` 146 | 147 | Note that ImmutableJS is still optional. We don't depend on it directly 148 | and you're not required to use it. But if you do, we've got you covered! 149 | -------------------------------------------------------------------------------- /packages/store/articles/quickstart.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | `@angular-redux/store` has a peer dependency on redux, so we need to install it as well. 4 | 5 | ```sh 6 | npm install --save redux @angular-redux/store 7 | ``` 8 | 9 | ## Quick Start 10 | 11 | ```typescript 12 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 13 | import { AppModule } from './containers/app.module'; 14 | 15 | platformBrowserDynamic().bootstrapModule(AppModule); 16 | ``` 17 | 18 | Import the `NgReduxModule` class and add it to your application module as an 19 | `import`. Once you've done this, you'll be able to inject `NgRedux` into your 20 | Angular components. In your top-level app module, you 21 | can configure your Redux store with reducers, initial state, 22 | and optionally middlewares and enhancers as you would in Redux directly. 23 | 24 | ```typescript 25 | import { NgReduxModule, NgRedux } from '@angular-redux/store'; 26 | import { createLogger } from 'redux-logger'; 27 | import { rootReducer } from './reducers'; 28 | 29 | interface IAppState { 30 | /* ... */ 31 | } 32 | 33 | @NgModule({ 34 | /* ... */ 35 | imports: [, /* ... */ NgReduxModule], 36 | }) 37 | export class AppModule { 38 | constructor(ngRedux: NgRedux) { 39 | ngRedux.configureStore(rootReducer, {}, [createLogger()]); 40 | } 41 | } 42 | ``` 43 | 44 | Or if you prefer to create the Redux store yourself you can do that and use the 45 | `provideStore()` function instead: 46 | 47 | ```typescript 48 | import { 49 | applyMiddleware, 50 | Store, 51 | combineReducers, 52 | compose, 53 | createStore, 54 | } from 'redux'; 55 | import { NgReduxModule, NgRedux } from '@angular-redux/store'; 56 | import { createLogger } from 'redux-logger'; 57 | import { rootReducer } from './reducers'; 58 | 59 | interface IAppState { 60 | /* ... */ 61 | } 62 | 63 | export const store: Store = createStore( 64 | rootReducer, 65 | applyMiddleware(createLogger()), 66 | ); 67 | 68 | @NgModule({ 69 | /* ... */ 70 | imports: [, /* ... */ NgReduxModule], 71 | }) 72 | class AppModule { 73 | constructor(ngRedux: NgRedux) { 74 | ngRedux.provideStore(store); 75 | } 76 | } 77 | ``` 78 | 79 | > Note that we're also using a Redux middleware from the community here: 80 | > [redux-logger](https://www.npmjs.com/package/redux-logger). This is just to show 81 | > off that `@angular-redux/store` is indeed compatible with Redux middlewares as you 82 | > might expect. 83 | > 84 | > Note that to use it, you'll need to install it with `npm install --save redux-logger` 85 | > and type definitions for it with `npm install --save-dev @types/redux-logger`. 86 | 87 | Now your Angular app has been reduxified! Use the `@select` decorator to 88 | access your store state, and `.dispatch()` to dispatch actions: 89 | 90 | ```typescript 91 | import { select } from '@angular-redux/store'; 92 | 93 | @Component({ 94 | template: 95 | '', 96 | }) 97 | class App { 98 | @select() count$: Observable; 99 | 100 | constructor(private ngRedux: NgRedux) {} 101 | 102 | onClick() { 103 | this.ngRedux.dispatch({ type: INCREMENT }); 104 | } 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /packages/store/articles/redux-dev-tools.md: -------------------------------------------------------------------------------- 1 | # Using DevTools 2 | 3 | `@angular-redux/store` is fully compatible with the Chrome extension version of the Redux dev 4 | tools: 5 | 6 | https://github.com/zalmoxisus/redux-devtools-extension 7 | 8 | However, due to peculiarities of Angular's change detection logic, 9 | events that come from external tools don't trigger a refresh in Angular's 10 | zone. 11 | 12 | We've taken the liberty of providing a wrapper around the extension 13 | tools that handles this for you. 14 | 15 | Here's how to hook the extension up to your app: 16 | 17 | ```typescript 18 | import { 19 | NgReduxModule, 20 | NgRedux, 21 | DevToolsExtension, 22 | } from '@angular-redux/store'; 23 | 24 | // Add the dev tools enhancer your ngRedux.configureStore called 25 | // when you initialize your root component: 26 | @NgModule({ 27 | /* ... */ 28 | imports: [, /* ... */ NgReduxModule], 29 | }) 30 | export class AppModule { 31 | constructor(private ngRedux: NgRedux, private devTools: DevToolsExtension) { 32 | let enhancers = []; 33 | // ... add whatever other enhancers you want. 34 | 35 | // You probably only want to expose this tool in devMode. 36 | if (__DEVMODE__ && devTools.isEnabled()) { 37 | enhancers = [...enhancers, devTools.enhancer()]; 38 | } 39 | 40 | this.ngRedux.configureStore(rootReducer, initialState, [], enhancers); 41 | } 42 | } 43 | ``` 44 | 45 | `ReduxDevTools.enhancer()` takes the same options parameter as 46 | documented here: https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#windowdevtoolsextensionconfig 47 | -------------------------------------------------------------------------------- /packages/store/articles/select-pattern.md: -------------------------------------------------------------------------------- 1 | # The Select Pattern 2 | 3 | The select pattern allows you to get slices of your state as RxJS observables. 4 | 5 | These plug in very efficiently to Angular's change detection mechanism and this is the 6 | preferred approach to accessing store data in Angular. 7 | 8 | ## The @select decorator 9 | 10 | The `@select` decorator can be added to the property of any class or angular 11 | component/injectable. It will turn the property into an observable which observes 12 | the Redux Store value which is selected by the decorator's parameter. 13 | 14 | The decorator expects to receive a `string`, an array of `string`s, a `function` or no 15 | parameter at all. 16 | 17 | - If a `string` is passed the `@select` decorator will attempt to observe a store 18 | property whose name matches the `string`. 19 | - If an array of strings is passed, the decorator will attempt to match that path 20 | through the store (similar to `immutableJS`'s `getIn`). 21 | - If a `function` is passed the `@select` decorator will attempt to use that function 22 | as a selector on the RxJs observable. 23 | - If nothing is passed then the `@select` decorator will attempt to use the name of the class property to find a matching value in the Redux store. Note that a utility is in place here where any \$ characters will be ignored from the class property's name. 24 | 25 | ```typescript 26 | import { Component } from '@angular/core'; 27 | import { Observable } from 'rxjs/Observable'; 28 | import { select } from '@angular-redux/store'; 29 | 30 | @Component({ 31 | selector: 'counter-value-printed-many-times', 32 | template: ` 33 |

{{ counter$ | async }}

34 |

{{ counter | async }}

35 |

{{ counterSelectedWithString | async }}

36 |

{{ counterSelectedWithFunction | async }}

37 |

{{ counterSelectedWithFunctionAndMultipliedByTwo | async }}

38 | `, 39 | }) 40 | export class CounterValue { 41 | // this selects `counter` from the store and attaches it to this property 42 | // it uses the property name to select, and ignores the $ from it 43 | @select() counter$; 44 | 45 | // this selects `counter` from the store and attaches it to this property 46 | @select() counter; 47 | 48 | // this selects `counter` from the store and attaches it to this property 49 | @select('counter') counterSelectedWithString; 50 | 51 | // this selects `pathDemo.foo.bar` from the store and attaches it to this 52 | // property. 53 | @select(['pathDemo', 'foo', 'bar']) 54 | pathSelection; 55 | 56 | // this selects `counter` from the store and attaches it to this property 57 | @select(state => state.counter) 58 | counterSelectedWithFunction; 59 | 60 | // this selects `counter` from the store and multiples it by two 61 | @select(state => state.counter * 2) 62 | counterSelectedWithFuntionAndMultipliedByTwo: Observable; 63 | } 64 | ``` 65 | 66 | ## Select Without Decorators 67 | 68 | If you like RxJS, but aren't comfortable with decorators, you can also make 69 | store selections using the `ngRedux.select()` function. 70 | 71 | ```typescript 72 | import { Component } from '@angular/core'; 73 | import { Observable } from 'rxjs/Observable'; 74 | import { Counter } from '../components/Counter'; 75 | import * as CounterActions from '../actions/CounterActions'; 76 | import { NgRedux } from '@angular-redux/store'; 77 | 78 | interface IAppState { 79 | counter: number; 80 | } 81 | 82 | @Component({ 83 | selector: 'root', 84 | template: ` 85 | 90 | 91 | `, 92 | }) 93 | export class Counter { 94 | private count$: Observable; 95 | 96 | constructor(private ngRedux: NgRedux) {} 97 | 98 | ngOnInit() { 99 | let { increment, decrement } = CounterActions; 100 | this.counter$ = this.ngRedux.select('counter'); 101 | } 102 | 103 | incrementIfOdd = () => 104 | this.ngRedux.dispatch(CounterActions.incrementIfOdd()); 105 | 106 | incrementAsync = () => 107 | this.ngRedux.dispatch(CounterActions.incrementAsync()); 108 | } 109 | ``` 110 | 111 | `ngRedux.select` can take a property name or a function which transforms a property. 112 | Since it's an observable, you can also transform data using observable operators like 113 | `.map`, `.filter`, etc. 114 | 115 | ## The @select\$ decorator 116 | 117 | The `@select$` decorator works similar to `@select`, however you are able to specify observable chains to execute on the selected result. 118 | 119 | ```typescript 120 | import { select$ } from 'angular-redux/store'; 121 | 122 | export const debounceAndTriple = obs$ => obs$.debounce(300).map(x => 3 * x); 123 | 124 | class Foo { 125 | @select$(['foo', 'bar'], debounceAndTriple) 126 | readonly debouncedFooBar$: Observable; 127 | } 128 | ``` 129 | -------------------------------------------------------------------------------- /packages/store/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "src/index.ts", 5 | "languageLevel": ["esnext", "dom", "dom.iterable"], 6 | "umdModuleIds": { 7 | "@angular-redux/store": "angularReduxStore", 8 | "redux": "redux" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/store/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angular-redux/store", 3 | "version": "10.0.0", 4 | "description": "Angular bindings for Redux", 5 | "author": "William Buchwalter (http://github.com/wbuchwalter)", 6 | "license": "MIT", 7 | "homepage": "https://github.com/angular-redux/platform", 8 | "main": "src/index.ts", 9 | "scripts": { 10 | "build": "ng-packagr -p ." 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/angular-redux/platform.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/angular-redux/platform/issues" 18 | }, 19 | "keywords": [ 20 | "angular", 21 | "angular2", 22 | "redux", 23 | "store", 24 | "state management" 25 | ], 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "engines": { 30 | "node": ">=8" 31 | }, 32 | "peerDependencies": { 33 | "@angular/core": "^7.0.0", 34 | "redux": "^4.0.0", 35 | "rxjs": "^6.0.0" 36 | }, 37 | "devDependencies": { 38 | "redux-devtools-extension": "2.13.7" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/store/src/components/dev-tools.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationRef, Injectable, NgZone } from '@angular/core'; 2 | import { AnyAction, StoreEnhancer, Unsubscribe } from 'redux'; 3 | import { EnhancerOptions } from 'redux-devtools-extension'; 4 | import { NgRedux } from './ng-redux'; 5 | 6 | export interface ReduxDevTools { 7 | (options: EnhancerOptions): StoreEnhancer; 8 | listen: ( 9 | onMessage: (message: AnyAction) => void, 10 | instanceId?: string, 11 | ) => void; 12 | } 13 | 14 | interface WindowWithReduxDevTools extends Window { 15 | __REDUX_DEVTOOLS_EXTENSION__?: ReduxDevTools; 16 | devToolsExtension?: ReduxDevTools; 17 | } 18 | 19 | const environment: WindowWithReduxDevTools = (typeof window !== 'undefined' 20 | ? window 21 | : {}) as WindowWithReduxDevTools; 22 | 23 | /** 24 | * An angular-2-ified version of the Redux DevTools chrome extension. 25 | */ 26 | @Injectable() 27 | export class DevToolsExtension { 28 | /** @hidden */ 29 | constructor(private appRef: ApplicationRef, private ngRedux: NgRedux) {} 30 | 31 | /** 32 | * A wrapper for the Chrome Extension Redux DevTools. 33 | * Makes sure state changes triggered by the extension 34 | * trigger Angular2's change detector. 35 | * 36 | * @argument options: dev tool options; same 37 | * format as described here: 38 | * [zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md] 39 | */ 40 | enhancer = (options?: EnhancerOptions) => { 41 | let subscription: Unsubscribe; 42 | if (!this.isEnabled()) { 43 | return null; 44 | } 45 | 46 | // Make sure changes from dev tools update angular's view. 47 | this.getDevTools()!.listen(({ type }) => { 48 | if (type === 'START') { 49 | subscription = this.ngRedux.subscribe(() => { 50 | if (!NgZone.isInAngularZone()) { 51 | this.appRef.tick(); 52 | } 53 | }); 54 | } else if (type === 'STOP') { 55 | subscription(); 56 | } 57 | }); 58 | 59 | return this.getDevTools()!(options || {}); 60 | }; 61 | 62 | /** 63 | * Returns true if the extension is installed and enabled. 64 | */ 65 | isEnabled = () => !!this.getDevTools(); 66 | 67 | /** 68 | * Returns the redux devtools enhancer. 69 | */ 70 | getDevTools = () => 71 | environment && 72 | (environment.__REDUX_DEVTOOLS_EXTENSION__ || environment.devToolsExtension); 73 | } 74 | -------------------------------------------------------------------------------- /packages/store/src/components/fractal-reducer-map.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Reducer } from 'redux'; 2 | import { getIn } from '../utils/get-in'; 3 | import { setIn } from '../utils/set-in'; 4 | import { PathSelector } from './selectors'; 5 | 6 | let reducerMap: { [id: string]: Reducer } = {}; 7 | 8 | const composeReducers = ( 9 | ...reducers: Reducer[] 10 | ): Reducer => (state: any, action: AnyAction) => 11 | reducers.reduce((subState, reducer) => reducer(subState, action), state); 12 | 13 | /** 14 | * @param rootReducer Call this on your root reducer to enable SubStore 15 | * functionality for pre-configured stores (e.g. using NgRedux.provideStore()). 16 | * NgRedux.configureStore 17 | * does it for you under the hood. 18 | */ 19 | export function enableFractalReducers(rootReducer: Reducer) { 20 | reducerMap = {}; 21 | return composeReducers(rootFractalReducer, rootReducer); 22 | } 23 | 24 | /** @hidden */ 25 | export function registerFractalReducer( 26 | basePath: PathSelector, 27 | localReducer: Reducer, 28 | ): void { 29 | const existingFractalReducer = reducerMap[JSON.stringify(basePath)]; 30 | if (existingFractalReducer && existingFractalReducer !== localReducer) { 31 | throw new Error( 32 | `attempt to overwrite fractal reducer for basePath ${basePath}`, 33 | ); 34 | } 35 | 36 | reducerMap[JSON.stringify(basePath)] = localReducer; 37 | } 38 | 39 | /** @hidden */ 40 | export function replaceLocalReducer( 41 | basePath: PathSelector, 42 | nextLocalReducer: Reducer, 43 | ): void { 44 | reducerMap[JSON.stringify(basePath)] = nextLocalReducer; 45 | } 46 | 47 | function rootFractalReducer( 48 | state: {} = {}, 49 | action: AnyAction & { '@angular-redux::fractalkey'?: string }, 50 | ) { 51 | const fractalKey = action['@angular-redux::fractalkey']; 52 | const fractalPath = fractalKey ? JSON.parse(fractalKey) : []; 53 | const localReducer = reducerMap[fractalKey || '']; 54 | return fractalKey && localReducer 55 | ? setIn(state, fractalPath, localReducer(getIn(state, fractalPath), action)) 56 | : state; 57 | } 58 | -------------------------------------------------------------------------------- /packages/store/src/components/ng-redux.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AnyAction, 3 | Dispatch, 4 | Middleware, 5 | Reducer, 6 | Store, 7 | StoreEnhancer, 8 | Unsubscribe, 9 | } from 'redux'; 10 | import { Observable } from 'rxjs'; 11 | import { ObservableStore } from './observable-store'; 12 | import { Comparator, PathSelector, Selector } from './selectors'; 13 | 14 | /** 15 | * This is the public interface of @angular-redux/store. It wraps the global 16 | * redux store and adds a few other add on methods. It's what you'll inject 17 | * into your Angular application as a service. 18 | */ 19 | export abstract class NgRedux implements ObservableStore { 20 | /** @hidden, @deprecated */ 21 | static instance?: ObservableStore = undefined; 22 | 23 | /** 24 | * Configures a Redux store and allows NgRedux to observe and dispatch 25 | * to it. 26 | * 27 | * This should only be called once for the lifetime of your app, for 28 | * example in the constructor of your root component. 29 | * 30 | * @param rootReducer Your app's root reducer 31 | * @param initState Your app's initial state 32 | * @param middleware Optional Redux middlewares 33 | * @param enhancers Optional Redux store enhancers 34 | */ 35 | abstract configureStore: ( 36 | rootReducer: Reducer, 37 | initState: RootState, 38 | middleware?: Middleware[], 39 | enhancers?: StoreEnhancer[], 40 | ) => void; 41 | 42 | /** 43 | * Accepts a Redux store, then sets it in NgRedux and 44 | * allows NgRedux to observe and dispatch to it. 45 | * 46 | * This should only be called once for the lifetime of your app, for 47 | * example in the constructor of your root component. If configureStore 48 | * has been used this cannot be used. 49 | * 50 | * @param store Your app's store 51 | */ 52 | abstract provideStore: (store: Store) => void; 53 | 54 | // Redux Store methods 55 | abstract dispatch: Dispatch; 56 | abstract getState: () => RootState; 57 | abstract subscribe: (listener: () => void) => Unsubscribe; 58 | abstract replaceReducer: (nextReducer: Reducer) => void; 59 | 60 | // ObservableStore methods. 61 | abstract select: ( 62 | selector?: Selector, 63 | comparator?: Comparator, 64 | ) => Observable; 65 | abstract configureSubStore: ( 66 | basePath: PathSelector, 67 | localReducer: Reducer, 68 | ) => ObservableStore; 69 | } 70 | -------------------------------------------------------------------------------- /packages/store/src/components/observable-store.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Reducer, Store } from 'redux'; 2 | import { Observable } from 'rxjs'; 3 | import { Comparator, PathSelector, Selector } from './selectors'; 4 | 5 | /** 6 | * This interface represents the glue that connects the 7 | * subscription-oriented Redux Store with the RXJS Observable-oriented 8 | * Angular component world. 9 | * 10 | * Augments the basic Redux store interface with methods to 11 | * enable selection and fractalization. 12 | */ 13 | export interface ObservableStore extends Store { 14 | /** 15 | * Select a slice of state to expose as an observable. 16 | * 17 | * @typeparam S 18 | * @param selector key or function to select a part of the state 19 | * @param [comparator] Optional 20 | * comparison function called to test if an item is distinct 21 | * from the previous item in the source. 22 | * 23 | * @returns An Observable that emits items from the 24 | * source Observable with distinct values. 25 | */ 26 | select: ( 27 | selector: Selector, 28 | comparator?: Comparator, 29 | ) => Observable; 30 | 31 | /** 32 | * Carves off a 'subStore' or 'fractal' store from this one. 33 | * 34 | * The returned object is itself an observable store, however any 35 | * selections, dispatches, or invocations of localReducer will be 36 | * specific to that substore and will not know about the parent 37 | * ObservableStore from which it was created. 38 | * 39 | * This is handy for encapsulating component or module state while 40 | * still benefiting from time-travel, etc. 41 | */ 42 | configureSubStore: ( 43 | basePath: PathSelector, 44 | localReducer: Reducer, 45 | ) => ObservableStore; 46 | } 47 | -------------------------------------------------------------------------------- /packages/store/src/components/root-store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AnyAction, 3 | applyMiddleware, 4 | compose, 5 | createStore, 6 | Dispatch, 7 | Middleware, 8 | Reducer, 9 | Store, 10 | StoreCreator, 11 | StoreEnhancer, 12 | Unsubscribe, 13 | } from 'redux'; 14 | 15 | import { NgZone } from '@angular/core'; 16 | import { BehaviorSubject, Observable, Observer } from 'rxjs'; 17 | import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; 18 | import { assert } from '../utils/assert'; 19 | import { enableFractalReducers } from './fractal-reducer-map'; 20 | import { NgRedux } from './ng-redux'; 21 | import { ObservableStore } from './observable-store'; 22 | import { 23 | Comparator, 24 | PathSelector, 25 | resolveToFunctionSelector, 26 | Selector, 27 | } from './selectors'; 28 | import { SubStore } from './sub-store'; 29 | 30 | /** @hidden */ 31 | export class RootStore extends NgRedux { 32 | private store: Store | undefined = undefined; 33 | private store$: BehaviorSubject; 34 | 35 | constructor(private ngZone: NgZone) { 36 | super(); 37 | 38 | NgRedux.instance = this; 39 | this.store$ = new BehaviorSubject(undefined).pipe( 40 | filter(n => n !== undefined), 41 | switchMap(observableStore => observableStore as any), 42 | // TODO: fix this? needing to explicitly cast this is wrong 43 | ) as BehaviorSubject; 44 | } 45 | 46 | configureStore = ( 47 | rootReducer: Reducer, 48 | initState: RootState, 49 | middleware: Middleware[] = [], 50 | enhancers: StoreEnhancer[] = [], 51 | ): void => { 52 | assert(!this.store, 'Store already configured!'); 53 | // Variable-arity compose in typescript FTW. 54 | this.setStore( 55 | compose( 56 | applyMiddleware(...middleware), 57 | ...enhancers, 58 | )(createStore)(enableFractalReducers(rootReducer), initState), 59 | ); 60 | }; 61 | 62 | provideStore = (store: Store) => { 63 | assert(!this.store, 'Store already configured!'); 64 | this.setStore(store); 65 | }; 66 | 67 | getState = (): RootState => this.store!.getState(); 68 | 69 | subscribe = (listener: () => void): Unsubscribe => 70 | this.store!.subscribe(listener); 71 | 72 | replaceReducer = (nextReducer: Reducer): void => { 73 | this.store!.replaceReducer(nextReducer); 74 | }; 75 | 76 | dispatch: Dispatch = (action: A): A => { 77 | assert( 78 | !!this.store, 79 | 'Dispatch failed: did you forget to configure your store? ' + 80 | 'https://github.com/angular-redux/platform/blob/master/packages/store/' + 81 | 'README.md#quick-start', 82 | ); 83 | 84 | if (!NgZone.isInAngularZone()) { 85 | return this.ngZone.run(() => this.store!.dispatch(action)); 86 | } else { 87 | return this.store!.dispatch(action); 88 | } 89 | }; 90 | 91 | select = ( 92 | selector?: Selector, 93 | comparator?: Comparator, 94 | ): Observable => 95 | this.store$.pipe( 96 | distinctUntilChanged(), 97 | map(resolveToFunctionSelector(selector)), 98 | distinctUntilChanged(comparator), 99 | ); 100 | 101 | configureSubStore = ( 102 | basePath: PathSelector, 103 | localReducer: Reducer, 104 | ): ObservableStore => 105 | new SubStore(this, basePath, localReducer); 106 | 107 | private setStore(store: Store) { 108 | this.store = store; 109 | const storeServable = this.storeToObservable(store); 110 | this.store$.next(storeServable as any); 111 | } 112 | 113 | private storeToObservable = ( 114 | store: Store, 115 | ): Observable => 116 | new Observable((observer: Observer) => { 117 | observer.next(store.getState()); 118 | const unsubscribeFromRedux = store.subscribe(() => 119 | observer.next(store.getState()), 120 | ); 121 | return () => { 122 | unsubscribeFromRedux(); 123 | observer.complete(); 124 | }; 125 | }); 126 | } 127 | -------------------------------------------------------------------------------- /packages/store/src/components/selectors.spec.ts: -------------------------------------------------------------------------------- 1 | import { sniffSelectorType } from './selectors'; 2 | 3 | describe('Selectors', () => { 4 | it('sniffs a string property selector', () => 5 | expect(sniffSelectorType('propName')).toBe('property')); 6 | 7 | it('sniffs a number property selector', () => 8 | expect(sniffSelectorType(3)).toBe('property')); 9 | 10 | it('sniffs a symbol property selector', () => 11 | expect(sniffSelectorType(Symbol('whatever'))).toBe('property')); 12 | 13 | it('sniffs a function selector', () => 14 | expect(sniffSelectorType(state => state)).toBe('function')); 15 | 16 | it('sniffs a path selector', () => 17 | expect(sniffSelectorType(['one', 'two'])).toBe('path')); 18 | 19 | it('sniffs a nil selector (undefined)', () => 20 | expect(sniffSelectorType()).toBe('nil')); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/store/src/components/selectors.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { getIn } from '../utils/get-in'; 3 | 4 | /** 5 | * Custom equality checker that can be used with `.select` and `@select`. 6 | * ```ts 7 | * const customCompare: Comparator = (x: any, y: any) => { 8 | * return x.id === y.id 9 | * } 10 | * 11 | * @select(selector, customCompare) 12 | * ``` 13 | */ 14 | export type Comparator = (x: any, y: any) => boolean; 15 | export type Transformer = ( 16 | store$: Observable, 17 | scope: any, 18 | ) => Observable; 19 | export type PropertySelector = string | number | symbol; 20 | export type PathSelector = (string | number)[]; 21 | export type FunctionSelector = (s: RootState) => S; 22 | export type Selector = 23 | | PropertySelector 24 | | PathSelector 25 | | FunctionSelector; 26 | 27 | /** @hidden */ 28 | export const sniffSelectorType = ( 29 | selector?: Selector, 30 | ) => 31 | !selector 32 | ? 'nil' 33 | : Array.isArray(selector) 34 | ? 'path' 35 | : 'function' === typeof selector 36 | ? 'function' 37 | : 'property'; 38 | 39 | /** @hidden */ 40 | export const resolver = (selector?: Selector) => ({ 41 | property: (state: any) => 42 | state ? state[selector as PropertySelector] : undefined, 43 | path: (state: RootState) => getIn(state, selector as PathSelector), 44 | function: selector as FunctionSelector, 45 | nil: (state: RootState) => state, 46 | }); 47 | 48 | /** @hidden */ 49 | export const resolveToFunctionSelector = ( 50 | selector?: Selector, 51 | ) => resolver(selector)[sniffSelectorType(selector)]; 52 | -------------------------------------------------------------------------------- /packages/store/src/components/sub-store.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgZone } from '@angular/core'; 2 | import { Action, AnyAction } from 'redux'; 3 | import { take, toArray } from 'rxjs/operators'; 4 | import { NgRedux } from './ng-redux'; 5 | import { ObservableStore } from './observable-store'; 6 | import { RootStore } from './root-store'; 7 | 8 | class MockNgZone extends NgZone { 9 | run(fn: (...args: any[]) => T): T { 10 | return fn() as T; 11 | } 12 | } 13 | 14 | interface SubState { 15 | wat: { 16 | quux: number; 17 | }; 18 | } 19 | 20 | interface AppState { 21 | foo: { 22 | bar: SubState; 23 | }; 24 | } 25 | 26 | describe('Substore', () => { 27 | const defaultReducer = (state: any, _: Action) => state; 28 | 29 | const basePath = ['foo', 'bar']; 30 | let ngRedux: NgRedux; 31 | let subStore: ObservableStore; 32 | 33 | beforeEach(() => { 34 | ngRedux = new RootStore(new MockNgZone({ 35 | enableLongStackTrace: false, 36 | }) as NgZone); 37 | ngRedux.configureStore(defaultReducer, { 38 | foo: { 39 | bar: { wat: { quux: 3 } }, 40 | }, 41 | }); 42 | 43 | subStore = ngRedux.configureSubStore(basePath, defaultReducer); 44 | }); 45 | 46 | it('adds a key to actions it dispatches', () => 47 | expect(subStore.dispatch({ type: 'MY_ACTION' })).toEqual({ 48 | type: 'MY_ACTION', 49 | '@angular-redux::fractalkey': '["foo","bar"]', 50 | })); 51 | 52 | it('gets state rooted at the base path', () => 53 | expect(subStore.getState()).toEqual({ wat: { quux: 3 } })); 54 | 55 | it('selects based on base path', () => { 56 | subStore.select('wat').subscribe(wat => expect(wat).toEqual({ quux: 3 })); 57 | }); 58 | 59 | it("handles property selection on a base path that doesn't exist yet", () => { 60 | const nonExistentSubStore = ngRedux.configureSubStore( 61 | ['sure', 'whatever'], 62 | (state: any, action: any) => ({ ...state, value: action.newValue }), 63 | ); 64 | nonExistentSubStore 65 | .select('value') 66 | .pipe( 67 | take(2), 68 | toArray(), 69 | ) 70 | .subscribe(v => expect(v).toEqual([undefined, 'now I exist'])); 71 | nonExistentSubStore.dispatch({ 72 | type: 'nvm', 73 | newValue: 'now I exist', 74 | }); 75 | }); 76 | 77 | it("handles path selection on a base path that doesn't exist yet", () => { 78 | const nonExistentSubStore = ngRedux.configureSubStore( 79 | ['sure', 'whatever'], 80 | (state: any, action: any) => ({ ...state, value: action.newValue }), 81 | ); 82 | nonExistentSubStore 83 | .select(['value']) 84 | .pipe( 85 | take(2), 86 | toArray(), 87 | ) 88 | .subscribe(v => expect(v).toEqual([undefined, 'now I exist'])); 89 | nonExistentSubStore.dispatch({ 90 | type: 'nvm', 91 | newValue: 'now I exist', 92 | }); 93 | }); 94 | 95 | it("handles function selection on a base path that doesn't exist yet", () => { 96 | const nonExistentSubStore = ngRedux.configureSubStore( 97 | ['sure', 'whatever'], 98 | (state: any, action: any) => ({ ...state, value: action.newValue }), 99 | ); 100 | nonExistentSubStore 101 | .select(s => (s ? s.value : s)) 102 | .pipe( 103 | take(2), 104 | toArray(), 105 | ) 106 | .subscribe(v => expect(v).toEqual([undefined, 'now I exist'])); 107 | nonExistentSubStore.dispatch({ 108 | type: 'nvm', 109 | newValue: 'now I exist', 110 | }); 111 | }); 112 | 113 | it('can create its own sub-store', () => { 114 | const subSubStore = subStore.configureSubStore(['wat'], defaultReducer); 115 | expect(subSubStore.getState()).toEqual({ quux: 3 }); 116 | subSubStore.select('quux').subscribe(quux => expect(quux).toEqual(3)); 117 | 118 | expect(subSubStore.dispatch({ type: 'MY_ACTION' })).toEqual({ 119 | type: 'MY_ACTION', 120 | '@angular-redux::fractalkey': '["foo","bar","wat"]', 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /packages/store/src/components/sub-store.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Dispatch, Reducer } from 'redux'; 2 | import { Observable } from 'rxjs'; 3 | import { distinctUntilChanged, map } from 'rxjs/operators'; 4 | 5 | import { getIn } from '../utils/get-in'; 6 | import { 7 | registerFractalReducer, 8 | replaceLocalReducer, 9 | } from './fractal-reducer-map'; 10 | import { NgRedux } from './ng-redux'; 11 | import { ObservableStore } from './observable-store'; 12 | import { 13 | Comparator, 14 | PathSelector, 15 | resolveToFunctionSelector, 16 | Selector, 17 | } from './selectors'; 18 | 19 | /** @hidden */ 20 | export class SubStore implements ObservableStore { 21 | constructor( 22 | private rootStore: NgRedux, 23 | private basePath: PathSelector, 24 | localReducer: Reducer, 25 | ) { 26 | registerFractalReducer(basePath, localReducer); 27 | } 28 | 29 | dispatch: Dispatch = action => 30 | this.rootStore.dispatch({ 31 | ...(action as any), 32 | '@angular-redux::fractalkey': JSON.stringify(this.basePath), 33 | }); 34 | 35 | getState = (): State => getIn(this.rootStore.getState(), this.basePath); 36 | 37 | configureSubStore = ( 38 | basePath: PathSelector, 39 | localReducer: Reducer, 40 | ): ObservableStore => 41 | new SubStore( 42 | this.rootStore, 43 | [...this.basePath, ...basePath], 44 | localReducer, 45 | ); 46 | 47 | select = ( 48 | selector?: Selector, 49 | comparator?: Comparator, 50 | ): Observable => 51 | this.rootStore.select(this.basePath).pipe( 52 | map(resolveToFunctionSelector(selector)), 53 | distinctUntilChanged(comparator), 54 | ); 55 | 56 | subscribe = (listener: () => void): (() => void) => { 57 | const subscription = this.select().subscribe(listener); 58 | return () => subscription.unsubscribe(); 59 | }; 60 | 61 | replaceReducer = (nextLocalReducer: Reducer) => 62 | replaceLocalReducer(this.basePath, nextLocalReducer); 63 | } 64 | -------------------------------------------------------------------------------- /packages/store/src/decorators/dispatch.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | 3 | import { NgRedux } from '../components/ng-redux'; 4 | import { getBaseStore } from './helpers'; 5 | 6 | /** 7 | * Auto-dispatches the return value of the decorated function. 8 | * 9 | * Decorate a function creator method with @dispatch and its return 10 | * value will automatically be passed to ngRedux.dispatch() for you. 11 | */ 12 | export function dispatch(): PropertyDecorator { 13 | return function decorate( 14 | target: object, 15 | key: string | symbol | number, 16 | descriptor?: PropertyDescriptor, 17 | ): PropertyDescriptor { 18 | let originalMethod: () => Action; 19 | 20 | const wrapped = function(this: unknown, ...args: any) { 21 | const result = originalMethod.apply(this, args); 22 | if (result !== undefined) { 23 | const store = getBaseStore(this) || NgRedux.instance; 24 | if (store) { 25 | store.dispatch(result); 26 | } 27 | } 28 | return result; 29 | }; 30 | 31 | descriptor = descriptor || Object.getOwnPropertyDescriptor(target, key); 32 | 33 | if (descriptor === undefined) { 34 | const dispatchDescriptor: PropertyDescriptor = { 35 | get: () => wrapped, 36 | set: setMethod => (originalMethod = setMethod), 37 | }; 38 | Object.defineProperty(target, key, dispatchDescriptor); 39 | return dispatchDescriptor; 40 | } else { 41 | originalMethod = descriptor.value; 42 | descriptor.value = wrapped; 43 | return descriptor; 44 | } 45 | }; 46 | } 47 | // get descriptor 48 | // if no descriptor, create one with getter setter 49 | // if descriptor, set original method to descriptor, and then bind the wrapped function instead 50 | -------------------------------------------------------------------------------- /packages/store/src/decorators/select.ts: -------------------------------------------------------------------------------- 1 | import { Comparator, Selector, Transformer } from '../components/selectors'; 2 | import { getInstanceSelection } from './helpers'; 3 | 4 | /** 5 | * Selects an observable from the store, and attaches it to the decorated 6 | * property. 7 | * 8 | * ```ts 9 | * import { select } from '@angular-redux/store'; 10 | * 11 | * class SomeClass { 12 | * @select(['foo','bar']) foo$: Observable 13 | * } 14 | * ``` 15 | * 16 | * @param selector 17 | * A selector function, property name string, or property name path 18 | * (array of strings/array indices) that locates the store data to be 19 | * selected 20 | * 21 | * @param comparator Function used to determine if this selector has changed. 22 | */ 23 | export function select( 24 | selector?: Selector, 25 | comparator?: Comparator, 26 | ): PropertyDecorator { 27 | return (target: any, key: string | symbol): void => { 28 | const adjustedSelector = selector 29 | ? selector 30 | : String(key).lastIndexOf('$') === String(key).length - 1 31 | ? String(key).substring(0, String(key).length - 1) 32 | : key; 33 | decorate(adjustedSelector, undefined, comparator)(target, key); 34 | }; 35 | } 36 | 37 | /** 38 | * Selects an observable using the given path selector, and runs it through the 39 | * given transformer function. A transformer function takes the store 40 | * observable as an input and returns a derived observable from it. That derived 41 | * observable is run through distinctUntilChanges with the given optional 42 | * comparator and attached to the store property. 43 | * 44 | * Think of a Transformer as a FunctionSelector that operates on observables 45 | * instead of values. 46 | * 47 | * ```ts 48 | * import { select$ } from 'angular-redux/store'; 49 | * 50 | * export const debounceAndTriple = obs$ => obs$ 51 | * .debounce(300) 52 | * .map(x => 3 * x); 53 | * 54 | * class Foo { 55 | * @select$(['foo', 'bar'], debounceAndTriple) 56 | * readonly debouncedFooBar$: Observable; 57 | * } 58 | * ``` 59 | */ 60 | export function select$( 61 | selector: Selector, 62 | transformer: Transformer, 63 | comparator?: Comparator, 64 | ): PropertyDecorator { 65 | return decorate(selector, transformer, comparator); 66 | } 67 | 68 | function decorate( 69 | selector: Selector, 70 | transformer?: Transformer, 71 | comparator?: Comparator, 72 | ): PropertyDecorator { 73 | return function decorator(target: any, key): void { 74 | function getter(this: any) { 75 | return getInstanceSelection(this, key, selector, transformer, comparator); 76 | } 77 | 78 | // Replace decorated property with a getter that returns the observable. 79 | if (delete target[key]) { 80 | Object.defineProperty(target, key, { 81 | get: getter, 82 | enumerable: true, 83 | configurable: true, 84 | }); 85 | } 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /packages/store/src/decorators/with-sub-store.ts: -------------------------------------------------------------------------------- 1 | import { FractalStoreOptions, setClassOptions } from './helpers'; 2 | 3 | /** 4 | * Modifies the behaviour of any `@select`, `@select$`, or `@dispatch` 5 | * decorators to operate on a substore defined by the IFractalStoreOptions. 6 | * 7 | * See: 8 | * https://github.com/angular-redux/platform/blob/master/packages/store/articles/fractal-store.md 9 | * for more information about SubStores. 10 | */ 11 | export function WithSubStore({ 12 | basePathMethodName, 13 | localReducer, 14 | }: FractalStoreOptions): ClassDecorator { 15 | return function decorate(constructor: Function): void { 16 | setClassOptions(constructor, { 17 | basePathMethodName, 18 | localReducer, 19 | }); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/store/src/exports.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DevToolsExtension, 3 | dispatch, 4 | enableFractalReducers, 5 | NgRedux, 6 | NgReduxModule, 7 | select, 8 | select$, 9 | WithSubStore, 10 | } from './index'; 11 | 12 | describe('The @angular-redux/store package exports', () => { 13 | it('should contain the NgReduxModule class', () => { 14 | expect(NgReduxModule).toBeDefined(); 15 | }); 16 | 17 | it('should contain the NgRedux class', () => { 18 | expect(NgRedux).toBeDefined(); 19 | }); 20 | 21 | it('should contain the DevToolsExtension class', () => { 22 | expect(DevToolsExtension).toBeDefined(); 23 | }); 24 | 25 | it('should contain the enableFractalReducers function', () => { 26 | expect(enableFractalReducers).toBeDefined(); 27 | }); 28 | 29 | it('should contain the select property decorator', () => { 30 | expect(select).toBeDefined(); 31 | }); 32 | 33 | it('should contain the select$ property decorator', () => { 34 | expect(select$).toBeDefined(); 35 | }); 36 | 37 | it('should contain the dispatch property decorator', () => { 38 | expect(dispatch).toBeDefined(); 39 | }); 40 | 41 | it('should contain the WithSubStore class decorator', () => { 42 | expect(WithSubStore).toBeDefined(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/store/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DevToolsExtension } from './components/dev-tools'; 2 | import { enableFractalReducers } from './components/fractal-reducer-map'; 3 | import { NgRedux } from './components/ng-redux'; 4 | import { ObservableStore } from './components/observable-store'; 5 | import { 6 | Comparator, 7 | FunctionSelector, 8 | PathSelector, 9 | PropertySelector, 10 | Selector, 11 | Transformer, 12 | } from './components/selectors'; 13 | import { dispatch } from './decorators/dispatch'; 14 | import { select, select$ } from './decorators/select'; 15 | import { WithSubStore } from './decorators/with-sub-store'; 16 | import { NgReduxModule } from './ng-redux.module'; 17 | 18 | // Warning: don't do this: 19 | // export * from './foo' 20 | // ... because it breaks rollup. See 21 | // https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module 22 | export { 23 | NgRedux, 24 | Selector, 25 | PathSelector, 26 | PropertySelector, 27 | FunctionSelector, 28 | Comparator, 29 | Transformer, 30 | NgReduxModule, 31 | DevToolsExtension, 32 | enableFractalReducers, 33 | select, 34 | select$, 35 | dispatch, 36 | WithSubStore, 37 | ObservableStore, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/store/src/ng-redux.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, NgZone } from '@angular/core'; 2 | import { DevToolsExtension } from './components/dev-tools'; 3 | import { NgRedux } from './components/ng-redux'; 4 | import { RootStore } from './components/root-store'; 5 | 6 | /** @hidden */ 7 | export function _ngReduxFactory(ngZone: NgZone) { 8 | return new RootStore(ngZone); 9 | } 10 | 11 | @NgModule({ 12 | providers: [ 13 | DevToolsExtension, 14 | { provide: NgRedux, useFactory: _ngReduxFactory, deps: [NgZone] }, 15 | ], 16 | }) 17 | export class NgReduxModule {} 18 | -------------------------------------------------------------------------------- /packages/store/src/utils/assert.ts: -------------------------------------------------------------------------------- 1 | /** @hidden */ 2 | export const assert = (condition: boolean, message: string): void => { 3 | if (!condition) { 4 | throw new Error(message); 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /packages/store/src/utils/get-in.spec.ts: -------------------------------------------------------------------------------- 1 | import { getIn } from './get-in'; 2 | 3 | describe('getIn', () => { 4 | it('should select a first-level prop', () => { 5 | const test = { foo: 1 }; 6 | expect(getIn(test, ['foo'])).toEqual(1); 7 | }); 8 | 9 | it('should select a second-level prop', () => { 10 | const test = { foo: { bar: 2 } }; 11 | expect(getIn(test, ['foo', 'bar'])).toEqual(2); 12 | }); 13 | 14 | it('should select a third-level prop', () => { 15 | const test = { foo: { bar: { quux: 3 } } }; 16 | expect(getIn(test, ['foo', 'bar', 'quux'])).toEqual(3); 17 | }); 18 | 19 | it('should select falsy values properly', () => { 20 | const test = { 21 | a: false, 22 | b: 0, 23 | c: '', 24 | d: undefined, 25 | }; 26 | expect(getIn(test, ['a'])).toEqual(false); 27 | expect(getIn(test, ['b'])).toEqual(0); 28 | expect(getIn(test, ['c'])).toEqual(''); 29 | expect(getIn(test, ['d'])).toEqual(undefined); 30 | }); 31 | 32 | it('should select nested falsy values properly', () => { 33 | const test = { 34 | foo: { 35 | a: false, 36 | b: 0, 37 | c: '', 38 | d: undefined, 39 | }, 40 | }; 41 | expect(getIn(test, ['foo', 'a'])).toEqual(false); 42 | expect(getIn(test, ['foo', 'b'])).toEqual(0); 43 | expect(getIn(test, ['foo', 'c'])).toEqual(''); 44 | expect(getIn(test, ['foo', 'd'])).toEqual(undefined); 45 | }); 46 | 47 | it('should not freak if the object is null', () => 48 | expect(getIn(null, ['foo', 'd'])).toEqual(null)); 49 | 50 | it('should not freak if the object is undefined', () => 51 | expect(getIn(undefined, ['foo', 'd'])).toEqual(undefined)); 52 | 53 | it('should not freak if the object is a primitive', () => 54 | expect(getIn(42, ['foo', 'd'])).toEqual(undefined)); 55 | 56 | it('should return undefined for a nonexistent prop', () => { 57 | const test = { foo: 1 }; 58 | expect(getIn(test, ['bar'])).toBe(undefined); 59 | }); 60 | 61 | it('should return undefined for a nonexistent path', () => { 62 | const test = { foo: 1 }; 63 | expect(getIn(test, ['bar', 'quux'])).toBe(undefined); 64 | }); 65 | 66 | it('should return undefined for a nested nonexistent prop', () => { 67 | const test = { foo: 1 }; 68 | expect(getIn(test, ['foo', 'bar'])).toBe(undefined); 69 | }); 70 | 71 | it('should select array elements properly', () => { 72 | const test = ['foo', 'bar']; 73 | expect(getIn(test, [0])).toEqual('foo'); 74 | expect(getIn(test, ['0'])).toEqual('foo'); 75 | expect(getIn(test, [1])).toEqual('bar'); 76 | expect(getIn(test, ['1'])).toEqual('bar'); 77 | expect(getIn(test, [2])).toBe(undefined); 78 | expect(getIn(test, ['2'])).toBe(undefined); 79 | }); 80 | 81 | it('should select nested array elements properly', () => { 82 | const test = { quux: ['foo', 'bar'] }; 83 | expect(getIn(test, ['quux', 0])).toEqual('foo'); 84 | expect(getIn(test, ['quux', '0'])).toEqual('foo'); 85 | expect(getIn(test, ['quux', 1])).toEqual('bar'); 86 | expect(getIn(test, ['quux', '1'])).toEqual('bar'); 87 | expect(getIn(test, ['quux', 2])).toBe(undefined); 88 | expect(getIn(test, ['quux', '2'])).toBe(undefined); 89 | }); 90 | 91 | it('should defer to a native getIn function if it exists on the data', () => { 92 | const testPath = ['foo', 'bar']; 93 | const test = { 94 | getIn: (path: (string | number)[]) => 95 | path === testPath ? 42 : undefined, 96 | }; 97 | 98 | expect(getIn(test, testPath)).toEqual(42); 99 | expect(getIn(test, ['some', 'path'])).toBe(undefined); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /packages/store/src/utils/get-in.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets a deeply-nested property value from an object, given a 'path' 3 | * of property names or array indices. 4 | * 5 | * @hidden 6 | */ 7 | export function getIn( 8 | v: any | undefined, 9 | pathElems: (string | number)[], 10 | ): any | undefined { 11 | if (!v) { 12 | return v; 13 | } 14 | 15 | // If this is an ImmutableJS structure, use existing getIn function 16 | if ('function' === typeof v.getIn) { 17 | return v.getIn(pathElems); 18 | } 19 | 20 | const [firstElem, ...restElems] = pathElems; 21 | 22 | if (undefined === v[firstElem]) { 23 | return undefined; 24 | } 25 | 26 | if (restElems.length === 0) { 27 | return v[firstElem]; 28 | } 29 | 30 | return getIn(v[firstElem], restElems); 31 | } 32 | -------------------------------------------------------------------------------- /packages/store/src/utils/set-in.spec.ts: -------------------------------------------------------------------------------- 1 | import { setIn } from './set-in'; 2 | 3 | describe('setIn', () => { 4 | it('performs a shallow set correctly without mutation', () => { 5 | const original = { a: 1 }; 6 | expect(setIn(original, ['b'], 2)).toEqual({ a: 1, b: 2 }); 7 | expect(original).toEqual({ a: 1 }); 8 | }); 9 | 10 | it('performs a deeply nested set correctly without mutation', () => { 11 | const original = { a: 1 }; 12 | const expected = { 13 | a: 1, 14 | b: { 15 | c: { 16 | d: 2, 17 | }, 18 | }, 19 | }; 20 | 21 | expect(setIn(original, ['b', 'c', 'd'], 2)).toEqual(expected); 22 | expect(original).toEqual({ a: 1 }); 23 | }); 24 | 25 | it('performs a deeply nested set with existing keys without mutation', () => { 26 | const original = { 27 | a: 1, 28 | b: { 29 | wat: 3, 30 | }, 31 | }; 32 | const expected = { 33 | a: 1, 34 | b: { 35 | wat: 3, 36 | c: { 37 | d: 2, 38 | }, 39 | }, 40 | }; 41 | 42 | expect(setIn(original, ['b', 'c', 'd'], 2)).toEqual(expected); 43 | expect(original).toEqual({ a: 1, b: { wat: 3 } }); 44 | }); 45 | 46 | it('should use setIn method of an object (case of ImmutableJS)', () => { 47 | let setInCalled = false; 48 | 49 | class TestClass { 50 | setIn() { 51 | setInCalled = true; 52 | } 53 | } 54 | 55 | const original = { 56 | root: new TestClass(), 57 | }; 58 | setIn(original, ['root', 'a', 'b', 'c'], 123); 59 | expect(setInCalled).toEqual(true); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/store/src/utils/set-in.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Sets a deeply-nested property value from an object, given a 'path' 3 | * of property names or array indices. Path elements are created if 4 | * not there already. Does not mutate the given object. 5 | * 6 | * @hidden 7 | */ 8 | export const setIn = ( 9 | obj: any, 10 | [firstElem, ...restElems]: (string | number)[], 11 | value: any, 12 | ): object => 13 | 'function' === typeof (obj[firstElem] || {}).setIn 14 | ? { 15 | ...obj, 16 | [firstElem]: obj[firstElem].setIn(restElems, value), 17 | } 18 | : { 19 | ...obj, 20 | [firstElem]: 21 | restElems.length === 0 22 | ? value 23 | : setIn(obj[firstElem] || {}, restElems, value), 24 | }; 25 | -------------------------------------------------------------------------------- /packages/store/testing/dev-tools.mock.ts: -------------------------------------------------------------------------------- 1 | // TODO: See if this linting rule can be enabled with new build process (ng-packagr) 2 | // tslint:disable:no-implicit-dependencies 3 | import { DevToolsExtension } from '@angular-redux/store'; 4 | import { Injectable } from '@angular/core'; 5 | 6 | @Injectable() 7 | export class MockDevToolsExtension extends DevToolsExtension {} 8 | -------------------------------------------------------------------------------- /packages/store/testing/index.ts: -------------------------------------------------------------------------------- 1 | import { MockDevToolsExtension } from './dev-tools.mock'; 2 | import { NgReduxTestingModule } from './ng-redux-testing.module'; 3 | import { MockNgRedux } from './ng-redux.mock'; 4 | import { MockObservableStore } from './observable-store.mock'; 5 | 6 | // Warning: don't do this: 7 | // export * from './foo' 8 | // ... because it breaks rollup. See 9 | // https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module 10 | export { 11 | NgReduxTestingModule, 12 | MockDevToolsExtension, 13 | MockNgRedux, 14 | MockObservableStore, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/store/testing/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts", 5 | "languageLevel": ["esnext", "dom", "dom.iterable"], 6 | "umdModuleIds": { 7 | "@angular-redux/store": "angularReduxStore" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/store/testing/ng-redux-testing.module.ts: -------------------------------------------------------------------------------- 1 | // TODO: See if this linting rule can be enabled with new build process (ng-packagr) 2 | // tslint:disable:no-implicit-dependencies 3 | import { DevToolsExtension, NgRedux } from '@angular-redux/store'; 4 | import { NgModule } from '@angular/core'; 5 | import { MockDevToolsExtension } from './dev-tools.mock'; 6 | import { MockNgRedux } from './ng-redux.mock'; 7 | 8 | // Needs to be initialized early so @select's use the mocked version too. 9 | const mockNgRedux = MockNgRedux.getInstance(); 10 | 11 | /** @hidden */ 12 | export function _mockNgReduxFactory() { 13 | return mockNgRedux; 14 | } 15 | 16 | @NgModule({ 17 | imports: [], 18 | providers: [ 19 | { provide: NgRedux, useFactory: _mockNgReduxFactory }, 20 | { provide: DevToolsExtension, useClass: MockDevToolsExtension }, 21 | ], 22 | }) 23 | export class NgReduxTestingModule {} 24 | -------------------------------------------------------------------------------- /packages/store/testing/ng-redux.mock.spec.ts: -------------------------------------------------------------------------------- 1 | // import { TestBed } from '@angular/core/testing'; 2 | // import { Component } from '@angular/core'; 3 | // import { Observable } from 'rxjs'; 4 | // import { map, toArray } from 'rxjs/operators'; 5 | 6 | // import { MockNgRedux } from './ng-redux.mock'; 7 | // import { NgRedux, select, select$ } from '../src'; 8 | 9 | // @Component({ 10 | // template: 'whatever', 11 | // selector: 'test-component' 12 | // }) 13 | // class TestComponent { 14 | // @select('foo') readonly obs$: Observable; 15 | 16 | // @select$('bar', obs$ => obs$.pipe(map((x: any) => 2 * x))) 17 | // readonly barTimesTwo$: Observable; 18 | 19 | // readonly baz$: Observable; 20 | 21 | // constructor(ngRedux: NgRedux) { 22 | // this.baz$ = ngRedux.select('baz'); 23 | // } 24 | // } 25 | 26 | describe('NgReduxMock', () => { 27 | it('should have a fake test for now until we can fix them...', () => 28 | undefined); 29 | // beforeEach(() => { 30 | // TestBed.configureTestingModule({ 31 | // declarations: [TestComponent], 32 | // providers: [{ provide: NgRedux, useFactory: MockNgRedux.getInstance }] 33 | // }).compileComponents(); 34 | 35 | // MockNgRedux.reset(); 36 | // }); 37 | 38 | // it('should reset stubs used by @select', () => { 39 | // const instance = TestBed.createComponent(TestComponent).componentInstance; 40 | 41 | // const stub1 = MockNgRedux.getSelectorStub('foo'); 42 | // stub1.next(1); 43 | // stub1.next(2); 44 | // stub1.complete(); 45 | 46 | // instance.obs$ 47 | // .pipe(toArray()) 48 | // .subscribe((values: number[]) => expect(values).toEqual([1, 2])); 49 | 50 | // MockNgRedux.reset(); 51 | 52 | // // Reset should result in a new stub getting created. 53 | // const stub2 = MockNgRedux.getSelectorStub('foo'); 54 | // expect(stub1 === stub2).toBe(false); 55 | 56 | // stub2.next(3); 57 | // stub2.complete(); 58 | 59 | // instance.obs$ 60 | // .pipe(toArray()) 61 | // .subscribe((values: number[]) => expect(values).toEqual([3])); 62 | // }); 63 | 64 | // it('should reset stubs used by @select$', () => { 65 | // const instance = TestBed.createComponent(TestComponent).debugElement 66 | // .componentInstance; 67 | 68 | // const stub1 = MockNgRedux.getSelectorStub('bar'); 69 | // stub1.next(1); 70 | // stub1.next(2); 71 | // stub1.complete(); 72 | 73 | // instance.barTimesTwo$ 74 | // .pipe(toArray()) 75 | // .subscribe((values: number[]) => expect(values).toEqual([2, 4])); 76 | 77 | // MockNgRedux.reset(); 78 | 79 | // // Reset should result in a new stub getting created. 80 | // const stub2 = MockNgRedux.getSelectorStub('bar'); 81 | // expect(stub1 === stub2).toBe(false); 82 | 83 | // stub2.next(3); 84 | // stub2.complete(); 85 | 86 | // instance.obs$ 87 | // .pipe(toArray()) 88 | // .subscribe((values: number[]) => expect(values).toEqual([6])); 89 | // }); 90 | 91 | // it('should reset stubs used by .select', () => { 92 | // const instance = TestBed.createComponent(TestComponent).debugElement 93 | // .componentInstance; 94 | 95 | // const stub1 = MockNgRedux.getSelectorStub('baz'); 96 | // stub1.next(1); 97 | // stub1.next(2); 98 | // stub1.complete(); 99 | 100 | // instance.baz$ 101 | // .pipe(toArray()) 102 | // .subscribe((values: number[]) => expect(values).toEqual([1, 2])); 103 | 104 | // MockNgRedux.reset(); 105 | 106 | // // Reset should result in a new stub getting created. 107 | // const stub2 = MockNgRedux.getSelectorStub('baz'); 108 | // expect(stub1 === stub2).toBe(false); 109 | 110 | // stub2.next(3); 111 | // stub2.complete(); 112 | 113 | // instance.obs$ 114 | // .pipe(toArray()) 115 | // .subscribe((values: number[]) => expect(values).toEqual([3])); 116 | // }); 117 | }); 118 | -------------------------------------------------------------------------------- /packages/store/testing/ng-redux.mock.ts: -------------------------------------------------------------------------------- 1 | // TODO: See if this linting rule can be enabled with new build process (ng-packagr) 2 | // tslint:disable:no-implicit-dependencies 3 | // tslint:disable:member-ordering 4 | import { 5 | Comparator, 6 | NgRedux, 7 | PathSelector, 8 | Selector, 9 | } from '@angular-redux/store'; 10 | import { 11 | AnyAction, 12 | Dispatch, 13 | Middleware, 14 | Reducer, 15 | Store, 16 | StoreEnhancer, 17 | } from 'redux'; 18 | import { Observable, Subject } from 'rxjs'; 19 | import { MockObservableStore } from './observable-store.mock'; 20 | /** 21 | * Convenience mock to make it easier to control selector 22 | * behaviour in unit tests. 23 | */ 24 | export class MockNgRedux extends NgRedux { 25 | /** @deprecated Use MockNgRedux.getInstance() instead. */ 26 | static mockInstance?: MockNgRedux = undefined; 27 | 28 | /** 29 | * Returns a subject that's connected to any observable returned by the 30 | * given selector. You can use this subject to pump values into your 31 | * components or services under test; when they call .select or @select 32 | * in the context of a unit test, MockNgRedux will give them the values 33 | * you pushed onto your stub. 34 | */ 35 | static getSelectorStub( 36 | selector?: Selector, 37 | comparator?: Comparator, 38 | ): Subject { 39 | return MockNgRedux.getInstance().mockRootStore.getSelectorStub( 40 | selector, 41 | comparator, 42 | ); 43 | } 44 | 45 | /** 46 | * Returns a mock substore that allows you to set up selectorStubs for 47 | * any 'fractal' stores your app creates with NgRedux.configureSubStore. 48 | * 49 | * If your app creates deeply nested substores from other substores, 50 | * pass the chain of pathSelectors in as ordered arguments to mock 51 | * the nested substores out. 52 | * @param pathSelectors 53 | */ 54 | static getSubStore( 55 | ...pathSelectors: PathSelector[] 56 | ): MockObservableStore { 57 | return pathSelectors.length 58 | ? MockNgRedux.getInstance().mockRootStore.getSubStore(...pathSelectors) 59 | : MockNgRedux.getInstance().mockRootStore; 60 | } 61 | 62 | /** 63 | * Reset all previously configured stubs. 64 | */ 65 | static reset(): void { 66 | MockNgRedux.getInstance().mockRootStore.reset(); 67 | NgRedux.instance = MockNgRedux.mockInstance as any; 68 | } 69 | 70 | /** 71 | * Gets the singleton MockNgRedux instance. Useful for cases where your 72 | * tests need to spy on store methods, for example. 73 | */ 74 | static getInstance() { 75 | MockNgRedux.mockInstance = MockNgRedux.mockInstance || new MockNgRedux(); 76 | return MockNgRedux.mockInstance; 77 | } 78 | // 79 | private mockRootStore = new MockObservableStore(); 80 | 81 | configureSubStore = this.mockRootStore.configureSubStore as any; 82 | dispatch = this.mockRootStore.dispatch as Dispatch; 83 | getState = this.mockRootStore.getState as any; 84 | subscribe = this.mockRootStore.subscribe; 85 | replaceReducer = this.mockRootStore.replaceReducer; 86 | select: ( 87 | selector?: Selector, 88 | comparator?: Comparator, 89 | ) => Observable = this.mockRootStore.select; 90 | 91 | /** @hidden */ 92 | constructor() { 93 | super(); 94 | // This hooks the mock up to @select. 95 | NgRedux.instance = this as any; 96 | } 97 | 98 | provideStore = (_: Store): void => undefined; 99 | configureStore = ( 100 | _: Reducer, 101 | __: any, 102 | ___?: Middleware[], 103 | ____?: StoreEnhancer[], 104 | ): void => undefined; 105 | } 106 | -------------------------------------------------------------------------------- /packages/store/testing/observable-store.mock.ts: -------------------------------------------------------------------------------- 1 | // TODO: See if this linting rule can be enabled with new build process (ng-packagr) 2 | // tslint:disable:no-implicit-dependencies 3 | import { Comparator, PathSelector, Selector } from '@angular-redux/store'; 4 | import { AnyAction, Dispatch, Reducer } from 'redux'; 5 | import { Observable, ReplaySubject, Subject } from 'rxjs'; 6 | import { distinctUntilChanged } from 'rxjs/operators'; 7 | 8 | /** @hidden */ 9 | export interface SelectorStubRecord { 10 | subject: Subject; 11 | comparator: Comparator; 12 | } 13 | 14 | /** @hidden */ 15 | export interface SelectorStubMap { 16 | [selector: string]: SelectorStubRecord; 17 | } 18 | 19 | /** @hidden */ 20 | export interface SubStoreStubMap { 21 | [basePath: string]: MockObservableStore; 22 | } 23 | 24 | /** @hidden */ 25 | export class MockObservableStore { 26 | selections: SelectorStubMap = {}; 27 | subStores: SubStoreStubMap = {}; 28 | 29 | getSelectorStub = ( 30 | selector?: Selector, 31 | comparator?: Comparator, 32 | ): Subject => 33 | this.initSelectorStub(selector, comparator).subject; 34 | 35 | reset = () => { 36 | Object.keys(this.subStores).forEach(k => this.subStores[k].reset()); 37 | this.selections = {}; 38 | this.subStores = {}; 39 | }; 40 | 41 | dispatch: Dispatch = action => action; 42 | replaceReducer = () => null; 43 | getState = () => ({}); 44 | subscribe = () => () => null; 45 | 46 | select = ( 47 | selector?: Selector, 48 | comparator?: Comparator, 49 | ): Observable => { 50 | const stub = this.initSelectorStub(selector, comparator); 51 | return stub.comparator 52 | ? stub.subject.pipe(distinctUntilChanged(stub.comparator)) 53 | : stub.subject; 54 | }; 55 | 56 | configureSubStore = ( 57 | basePath: PathSelector, 58 | _: Reducer, 59 | ): MockObservableStore => this.initSubStore(basePath); 60 | 61 | getSubStore = ( 62 | ...pathSelectors: PathSelector[] 63 | ): MockObservableStore => { 64 | const [first, ...rest] = pathSelectors; 65 | return (first 66 | ? this.initSubStore(first).getSubStore(...rest) 67 | : this) as MockObservableStore; 68 | }; 69 | 70 | private initSubStore(basePath: PathSelector) { 71 | const result = 72 | this.subStores[JSON.stringify(basePath)] || 73 | new MockObservableStore(); 74 | this.subStores[JSON.stringify(basePath)] = result; 75 | return result; 76 | } 77 | 78 | private initSelectorStub( 79 | selector?: Selector, 80 | comparator?: Comparator, 81 | ): SelectorStubRecord { 82 | const key = selector ? selector.toString() : ''; 83 | const record = this.selections[key] || { 84 | subject: new ReplaySubject(), 85 | comparator, 86 | }; 87 | 88 | this.selections[key] = record; 89 | return record; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/store/testing/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "lib": ["esnext", "dom", "dom.iterable"], 7 | "baseUrl": ".", 8 | "strict": true, 9 | "paths": { 10 | "@angular-redux/store": ["packages/store/src/index.ts"], 11 | "@angular-redux/form": ["packages/form/source/index.ts"], 12 | "@angular-redux/router": ["packages/router/src/index.ts"] 13 | }, 14 | "sourceMap": true, 15 | "esModuleInterop": true, 16 | "emitDecoratorMetadata": true, 17 | "experimentalDecorators": true, 18 | "declaration": true, 19 | "skipLibCheck": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "pretty": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rulesDirectory": ["node_modules/codelyzer"], 4 | "rules": { 5 | // Codelyzer 6 | "banana-in-box": true, 7 | "contextual-life-cycle": true, 8 | "decorator-not-allowed": true, 9 | "pipe-impure": true, 10 | "templates-no-negated-async": true, 11 | "trackBy-function": true, 12 | "i18n": false, 13 | "no-attribute-parameter-decorator": true, 14 | "no-forward-ref": false, 15 | "no-input-rename": true, 16 | "no-output-named-after-standard-event": true, 17 | "no-output-on-prefix": true, 18 | "no-output-rename": true, 19 | "no-unused-css": false, 20 | "no-unused-expression": [true, "allow-new"], 21 | "use-life-cycle-interface": true, 22 | "use-pipe-decorator": true, 23 | "use-pipe-transform-interface": true, 24 | "use-view-encapsulation": true, 25 | "angular-whitespace": [ 26 | true, 27 | "check-interpolation", 28 | "check-pipe", 29 | "check-semicolon" 30 | ], 31 | "component-class-suffix": true, 32 | "component-selector": ["element", "rdx", "kebab-case"], 33 | "directive-class-suffix": true, 34 | "directive-selector": ["attribute", "rdx", "camelCase"], 35 | "import-destructuring-spacing": true, 36 | "pipe-naming": ["camelCase", "rdx"], 37 | "use-host-property-decorator": true, 38 | "use-input-property-decorator": true, 39 | "use-output-property-decorator": true, 40 | 41 | // Preset Overrides 42 | "quotemark": [true, "single", "avoid-escape", "avoid-template"], 43 | "object-literal-sort-keys": false, 44 | "member-access": [true, "no-public"], 45 | "arrow-parens": false, 46 | "array-type": [true, "array"], 47 | "semicolon": false, 48 | "trailing-comma": false, 49 | "interface-name": [true, "never-prefix"], 50 | "no-console": [true, "log", "debug", "info", "time", "timeEnd", "trace"], 51 | "object-literal-key-quotes": [true, "as-needed"], 52 | "ban-types": [ 53 | true, 54 | ["Object", "Avoid using the `Object` type. Did you mean `object`?"], 55 | ["Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?"], 56 | ["Number", "Avoid using the `Number` type. Did you mean `number`?"], 57 | ["String", "Avoid using the `String` type. Did you mean `string`?"], 58 | ["Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?"] 59 | ], 60 | "no-submodule-imports": [ 61 | true, 62 | "core-js", 63 | "zone.js", 64 | "rxjs", 65 | "@angular/core/testing", 66 | "@angular/router/testing", 67 | "@angular/platform-browser-dynamic/testing", 68 | "@angular/platform-browser/animations", 69 | "@angular-redux/store/testing" 70 | ], 71 | "no-implicit-dependencies": false // disabled due to monorepo hoisting 72 | } 73 | } 74 | --------------------------------------------------------------------------------