├── src ├── assets │ └── .gitkeep ├── app │ ├── app.component.css │ ├── main.ts │ ├── counter-reducer.ts │ ├── counter-actions.ts │ ├── app.module.ts │ ├── app.component.html │ └── app.component.ts ├── favicon.ico ├── styles.css ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.ts ├── tsconfig.app.json ├── index.html ├── tslint.json ├── browserslist ├── main.ts ├── tsconfig.spec.json ├── actions.ts ├── test.ts ├── test │ ├── app.component.spec.ts │ ├── actions.spec.ts │ ├── app-store-factory.spec.ts │ └── app-store.spec.ts ├── karma.conf.js ├── app-store-factory.ts ├── app-store.ts └── polyfills.ts ├── public_api.ts ├── ng-package.json ├── e2e ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts ├── tsconfig.e2e.json └── protractor.conf.js ├── .editorconfig ├── tsconfig.json ├── .vscode └── launch.json ├── .travis.yml ├── .gitignore ├── README.md ├── package.json ├── tslint.json └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index'; -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfomediaLtd/angular2-redux/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { AppStore } from './app-store'; 2 | export { Actions } from './actions'; 3 | export { createAppStoreFactory, createAppStoreFactoryWithOptions } from './app-store-factory'; 4 | -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | }, 6 | "whitelistedNonPeerDependencies": ["."] 7 | } -------------------------------------------------------------------------------- /src/app/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { AppModule } from './app.module'; 5 | 6 | 7 | platformBrowserDynamic().bootstrapModule(AppModule); 8 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /.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 | 15 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular2Redux 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "./app-store.spec.ts", 17 | "**/*.spec.ts", 18 | "src/test/*.spec.ts", 19 | "**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/counter-reducer.ts: -------------------------------------------------------------------------------- 1 | import * as CounterActions from './counter-actions'; 2 | 3 | export default (state = { counter: 0 }, action: any = {}) => { 4 | switch (action.type) { 5 | case CounterActions.INCREMENT: 6 | return Object.assign({}, state, { counter: state.counter + 1 }); 7 | case CounterActions.DECREMENT: 8 | return Object.assign({}, state, { counter: state.counter - 1 }); 9 | default: 10 | return state; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/actions.ts: -------------------------------------------------------------------------------- 1 | import { AppStore } from './app-store'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | /** 5 | * Abstract class to provide utility methods for action creators 6 | */ 7 | @Injectable() 8 | export class Actions { 9 | public appStore; 10 | constructor(private _appStore: AppStore) { 11 | this.appStore = _appStore; 12 | } 13 | 14 | public createDispatcher(action: (...n: any[]) => any): (...n) => void { 15 | return (...n) => this.appStore.dispatch(action.call(this, ...n)); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | branches: 7 | only: 8 | - master 9 | notifications: 10 | email: false 11 | node_js: 12 | - '8' 13 | before_install: 14 | - npm i -g npm@^5.6.0 15 | - npm install -g @angular/cli 16 | - printf "//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n" >> .npmrc 17 | before_script: 18 | - npm prune 19 | script: 20 | - npm install 21 | - ng lint 22 | - npm run packagr 23 | after_success: 24 | - npm run semantic-release 25 | branches: 26 | except: 27 | - "/^v\\d+\\.\\d+\\.\\d+$/" 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Other 2 | build 3 | 4 | # compiled output 5 | /dist 6 | /tmp 7 | /out-tsc 8 | /.ng_pkg_build/ 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | *.sublime-workspace 21 | 22 | # IDE - VSCode 23 | .vscode/* 24 | !.vscode/settings.json 25 | !.vscode/tasks.json 26 | !.vscode/launch.json 27 | !.vscode/extensions.json 28 | 29 | # misc 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | yarn-error.log 36 | testem.log 37 | /typings 38 | 39 | # System Files 40 | .DS_Store 41 | Thumbs.db 42 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/app/counter-actions.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AppStore, Actions } from '../index'; 3 | 4 | export const INCREMENT = 'INCREMENT'; 5 | export const DECREMENT = 'DECREMENT'; 6 | 7 | @Injectable() 8 | export class CounterActions extends Actions { 9 | 10 | constructor(appStore: AppStore) { super(appStore); } 11 | 12 | public increment() { return { type: INCREMENT }; } 13 | public decrement() { return { type: DECREMENT }; } 14 | public incrementBy(n) { 15 | return dispatch => { 16 | for (let i = 0; i < n; i++) { 17 | dispatch(this.increment()); 18 | } 19 | }; 20 | } 21 | 22 | public decrementBy(n) { 23 | return dispatch => { 24 | for (let i = 0; i < n; i++) { 25 | dispatch(this.decrement()); 26 | } 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /src/test/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from '../app//app.component'; 3 | import { AppStore } from '../index'; 4 | import { createStore } from 'redux'; 5 | import { Observable } from 'rxjs'; 6 | 7 | 8 | const createSimpleAppStore = () => { 9 | return new AppStore(createStore((state: number = 0, action): number => { 10 | if (action.type === 'inc') { 11 | return state + 1; 12 | } else { 13 | return state; 14 | } 15 | })); 16 | }; 17 | 18 | 19 | describe('AppComponent', () => { 20 | beforeEach(async(() => { 21 | TestBed.configureTestingModule({ 22 | declarations: [ 23 | AppComponent 24 | ] 25 | }).compileComponents(); 26 | })); 27 | 28 | it('should do math', () => { 29 | expect(1 + 1 === 2); 30 | expect(5 >= 4); 31 | }); 32 | 33 | xit('should skip this', () => { 34 | expect(4 === 4); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { AppStore, createAppStoreFactoryWithOptions } from '../index'; 7 | 8 | import counterReducer from './counter-reducer'; 9 | import { CounterActions } from './counter-actions'; 10 | import { Actions } from '../actions'; 11 | 12 | const logger = store => next => action => { 13 | console.log('dispatching', action); 14 | const result = next(action); 15 | console.log('next state', store.getState()); 16 | return result; 17 | }; 18 | 19 | const appStoreFactory = createAppStoreFactoryWithOptions({ 20 | reducers: counterReducer, 21 | debug: true 22 | }); 23 | 24 | @NgModule({ 25 | imports: [BrowserModule], 26 | declarations: [AppComponent], 27 | providers: [CounterActions, Actions, { provide: AppStore, useFactory: appStoreFactory }], 28 | bootstrap: [AppComponent] 29 | }) 30 | 31 | export class AppModule {} 32 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular2Redux 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.1. 4 | 5 | ## Development server 6 | 7 | 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. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | 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. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | 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). 28 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | Welcome to {{ title }}! 5 |

6 | Angular Logo 7 |
8 |

Here are some links to help you start:

9 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { CounterActions } from './counter-actions'; 2 | import { Component, OnDestroy } from '@angular/core'; 3 | import { AppStore } from '../index'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | template: ` 8 | {{counter}} 9 |
10 | Decrement 11 | Increment 12 |

13 | Decrement 2 14 | Increment 2 15 |
16 | ` 17 | }) 18 | export class AppComponent implements OnDestroy { 19 | title = 'app'; 20 | 21 | public counter; 22 | public inc; 23 | public dec; 24 | public incBy; 25 | public decBy; 26 | 27 | private unsubscribeFromStore: () => void; 28 | 29 | constructor(appStore: AppStore, counterActions: CounterActions) { 30 | 31 | this.inc = counterActions.createDispatcher(counterActions.increment); 32 | this.dec = counterActions.createDispatcher(counterActions.decrement); 33 | this.incBy = counterActions.createDispatcher(counterActions.incrementBy); 34 | this.decBy = counterActions.createDispatcher(counterActions.decrementBy); 35 | this.counter = appStore.getState().counter; 36 | 37 | this.unsubscribeFromStore = appStore.subscribe(state => this.counter = state.counter); 38 | } 39 | 40 | public ngOnDestroy() { this.unsubscribeFromStore(); } 41 | } 42 | -------------------------------------------------------------------------------- /src/app-store-factory.ts: -------------------------------------------------------------------------------- 1 | import { AppStore } from './app-store'; 2 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | 5 | /* tslint:disable */ 6 | export function applyDevTools(debug) { 7 | // default to window query param 8 | let isDebug = false; 9 | // allow overriding with a boolean or function 10 | if (debug == undefined) { 11 | isDebug = window && !!window.location.href.match(/[?&]debug=([^&]+)\b/) 12 | } else { 13 | if (debug instanceof Function) { 14 | isDebug = debug(); 15 | } else { 16 | isDebug = debug; 17 | } 18 | } 19 | // config the dev tools extension is installed 20 | isDebug = isDebug && window && window['devToolsExtension']; 21 | 22 | // only apply is dev tools is installed 23 | return isDebug ? window['devToolsExtension']() : f => f; 24 | } 25 | /* tslint:enable */ 26 | 27 | /** 28 | * Factory for app store 29 | */ 30 | export function createAppStoreFactory(reducers?, additionalMiddlewares?) { 31 | return createAppStoreFactoryWithOptions({ 32 | reducers, 33 | additionalMiddlewares 34 | }); 35 | } 36 | 37 | export function createAppStoreFactoryWithOptions({ 38 | reducers, 39 | additionalMiddlewares = [], 40 | debug = false 41 | }) { 42 | 43 | return () => { 44 | 45 | // Figure out reducers 46 | let reducer = reducers; 47 | if (typeof reducer === 'object') { 48 | // it's not a single reducer so we need to combine the reducers on the object properties 49 | reducer = combineReducers(reducers); 50 | } 51 | 52 | const middleware = [thunk]; 53 | 54 | let reduxAppStore; 55 | let createStoreWithEnhancers; 56 | if (debug === undefined || !debug) { 57 | createStoreWithEnhancers = applyMiddleware(...middleware)(createStore); 58 | reduxAppStore = createStoreWithEnhancers(reducer); 59 | } else { 60 | reduxAppStore = createStore( 61 | reducer, 62 | compose( 63 | applyMiddleware(...middleware, ...additionalMiddlewares), 64 | applyDevTools(debug) 65 | ) 66 | ); 67 | } 68 | 69 | return new AppStore(reduxAppStore); 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-redux", 3 | "scripts": { 4 | "ng": "ng", 5 | "start": "ng serve", 6 | "build": "ng build", 7 | "packagr": "ng-packagr -p ng-package.json", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "commit": "git-cz", 12 | "semantic-release": "semantic-release pre && cp package.json dist && cat dist/package.json && npm publish dist && semantic-release post" 13 | }, 14 | "dependencies": { 15 | "@angular/animations": "^6.0.0", 16 | "@angular/common": "^6.0.0", 17 | "@angular/compiler": "^6.0.3", 18 | "@angular/core": "^6.0.0", 19 | "@angular/forms": "^6.0.0", 20 | "@angular/http": "^6.0.0", 21 | "@angular/platform-browser": "^6.0.0", 22 | "@angular/platform-browser-dynamic": "^6.0.0", 23 | "@angular/router": "^6.0.0", 24 | "core-js": "^2.5.4", 25 | "redux": "^3.7.2", 26 | "redux-thunk": "^2.2.0", 27 | "rxjs": "^6.2.0", 28 | "tslib": "^1.9.1", 29 | "zone.js": "^0.8.26" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "~0.6.1", 33 | "@angular/cli": "~6.0.1", 34 | "@angular/compiler-cli": "^6.0.0", 35 | "@angular/language-service": "^6.0.0", 36 | "@types/jasmine": "~2.8.6", 37 | "@types/jasminewd2": "~2.0.3", 38 | "@types/node": "~8.9.4", 39 | "codelyzer": "~4.2.1", 40 | "commitizen": "2.9.6", 41 | "cz-conventional-changelog": "2.0.0", 42 | "ghooks": "^2.0.4", 43 | "jasmine-core": "~2.99.1", 44 | "jasmine-spec-reporter": "~4.2.1", 45 | "karma": "~1.7.1", 46 | "karma-chrome-launcher": "~2.2.0", 47 | "karma-coverage-istanbul-reporter": "~1.4.2", 48 | "karma-jasmine": "~1.1.1", 49 | "karma-jasmine-html-reporter": "^0.2.2", 50 | "ng-packagr": "^2.4.5", 51 | "protractor": "~5.3.0", 52 | "semantic-release": "^6.3.6", 53 | "ts-node": "~5.0.1", 54 | "tslint": "~5.9.1", 55 | "typescript": "~2.7.2" 56 | }, 57 | "config": { 58 | "commitizen": { 59 | "path": "node_modules/cz-conventional-changelog" 60 | }, 61 | "ghooks": { 62 | "pre-commit": "npm run build" 63 | } 64 | }, 65 | "repository": { 66 | "type": "git", 67 | "url": "https://github.com/InfomediaLtd/angular2-redux.git" 68 | }, 69 | "main": "bundles/angular2-redux.umd.js", 70 | "module": "esm5/angular2-redux.js", 71 | "es2015": "esm2015/angular2-redux.js", 72 | "typings": "angular2-redux.d.ts", 73 | "metadata": "angular2-redux.metadata.json" 74 | } 75 | -------------------------------------------------------------------------------- /src/app-store.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | 3 | // ensure required operators are enabled 4 | import { map, distinctUntilChanged } from 'rxjs/operators'; 5 | 6 | /** 7 | * Wrapper for app store, 8 | */ 9 | export class AppStore { 10 | 11 | /** 12 | * Get current state 13 | */ 14 | public getState: () => any; 15 | /** 16 | * subscribe to a callback with the state 17 | */ 18 | public subscribe: (subscriber: (state) => void) => () => void; 19 | /** 20 | * Dispatch an action 21 | */ 22 | public dispatch: (action) => void; 23 | /** 24 | * Create a dispatcher as a curried function using the passed in action creator and an optional context 25 | */ 26 | public createDispatcher: (actionCreator, context) => (...n: any[]) => void; 27 | 28 | public store$: Observable = null; 29 | 30 | constructor(store: any) { 31 | this.store$ = of(store); 32 | this.getState = () => { 33 | return store.getState(); 34 | }; 35 | this.subscribe = (subscriber: (state) => void) => { 36 | // decorate the subscriber with the state passed in as a parameter 37 | return store.subscribe( 38 | () => { 39 | subscriber(store.getState()); 40 | } 41 | ); 42 | }; 43 | this.dispatch = (action) => { 44 | return store.dispatch(action); 45 | }; 46 | this.createDispatcher = (actionCreator, context): (...n: any[]) => void => { 47 | return (...args) => store.dispatch(actionCreator.call(context, ...args)); 48 | }; 49 | } 50 | 51 | public select(keyOrSelector: ((state: any) => R) | string | number | symbol): Observable { 52 | if (typeof keyOrSelector === 'string' || typeof keyOrSelector === 'number' 53 | || typeof keyOrSelector === 'symbol') { 54 | return this.store$ 55 | .pipe ( 56 | map(state => state[ keyOrSelector]), 57 | distinctUntilChanged() 58 | ); 59 | } else if (typeof keyOrSelector === 'function') { 60 | return this.store$.pipe( 61 | map(keyOrSelector), 62 | distinctUntilChanged() 63 | ); 64 | } else { 65 | throw new TypeError(`Unknown Parameter Type: ` 66 | + `Expected type of function or valid key type, got ${typeof keyOrSelector}`); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/actions.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from '../app/app.component'; 3 | import { AppStore } from '../index'; 4 | import { createStore } from 'redux'; 5 | import { Observable } from 'rxjs'; 6 | import { Actions } from '../actions'; 7 | 8 | 9 | class SomeActions extends Actions { 10 | public someAction1(data) { return { type: '1', data }; } 11 | public someAction2(data) { return { type: '2', data }; } 12 | } 13 | class SomeMoreActions extends Actions { 14 | constructor(appStore: AppStore) { super(appStore); } 15 | public someAction(data) { return { type: 'a', data }; } 16 | } 17 | const createAppStoreMock = () => { 18 | const appStoreMock: AppStore = new AppStore(createStore(state => state)); 19 | spyOn(appStoreMock, 'dispatch'); 20 | return appStoreMock; 21 | }; 22 | 23 | describe('Actions', () => { 24 | beforeEach(async(() => { 25 | TestBed.configureTestingModule({ 26 | declarations: [ 27 | AppComponent 28 | ] 29 | }).compileComponents(); 30 | })); 31 | 32 | it('should create dispatcher function', () => { 33 | const someActions = new SomeActions(createAppStoreMock()); 34 | const dispatcherFunction = someActions.createDispatcher(someActions.someAction1)('a'); 35 | expect(dispatcherFunction === undefined); 36 | }); 37 | 38 | it('dispatcher function should work', () => { 39 | const appStoreMock: AppStore = createAppStoreMock(); 40 | const someActions = new SomeActions(appStoreMock); 41 | 42 | const dispatcherFunction = someActions.createDispatcher( 43 | someActions.someAction1); 44 | 45 | dispatcherFunction('a'); 46 | dispatcherFunction('b'); 47 | 48 | const dispatchSpy = appStoreMock.dispatch; 49 | expect(dispatchSpy).toHaveBeenCalled(); 50 | expect(dispatchSpy).toHaveBeenCalledTimes(2); 51 | expect(someActions.someAction1('a')).toEqual({ type: '1', data: 'a' }); 52 | expect(someActions.someAction1('b')).toEqual({ type: '1', data: 'b' }); 53 | 54 | someActions.createDispatcher(someActions.someAction2)('c'); 55 | expect(someActions.someAction2('c')).toEqual({ type: '2', data: 'c' }); 56 | 57 | 58 | }); 59 | 60 | it('dispatcher function should work with injected app store', () => { 61 | 62 | const appStoreMock: AppStore = createAppStoreMock(); 63 | const someActions = new SomeMoreActions(appStoreMock); 64 | const dispatcherFunction = someActions.createDispatcher(someActions.someAction); 65 | 66 | dispatcherFunction('yo'); 67 | 68 | const dispatchSpy = appStoreMock.dispatch; 69 | expect(dispatchSpy).toHaveBeenCalled(); 70 | expect(dispatchSpy).toHaveBeenCalledTimes(1); 71 | expect(someActions.someAction('yo')).toEqual({ type: 'a', data: 'yo' }); 72 | 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "use-input-property-decorator": true, 121 | "use-output-property-decorator": true, 122 | "use-host-property-decorator": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-life-cycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular2-redux": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/angular2-redux", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "fileReplacements": [ 33 | { 34 | "replace": "src/environments/environment.ts", 35 | "with": "src/environments/environment.prod.ts" 36 | } 37 | ], 38 | "optimization": true, 39 | "outputHashing": "all", 40 | "sourceMap": false, 41 | "extractCss": true, 42 | "namedChunks": false, 43 | "aot": true, 44 | "extractLicenses": true, 45 | "vendorChunk": false, 46 | "buildOptimizer": true 47 | } 48 | } 49 | }, 50 | "serve": { 51 | "builder": "@angular-devkit/build-angular:dev-server", 52 | "options": { 53 | "browserTarget": "angular2-redux:build" 54 | }, 55 | "configurations": { 56 | "production": { 57 | "browserTarget": "angular2-redux:build:production" 58 | } 59 | } 60 | }, 61 | "extract-i18n": { 62 | "builder": "@angular-devkit/build-angular:extract-i18n", 63 | "options": { 64 | "browserTarget": "angular2-redux:build" 65 | } 66 | }, 67 | "test": { 68 | "builder": "@angular-devkit/build-angular:karma", 69 | "options": { 70 | "main": "src/test.ts", 71 | "polyfills": "src/polyfills.ts", 72 | "tsConfig": "src/tsconfig.spec.json", 73 | "karmaConfig": "src/karma.conf.js", 74 | "styles": [ 75 | "src/styles.css" 76 | ], 77 | "scripts": [], 78 | "assets": [ 79 | "src/favicon.ico", 80 | "src/assets" 81 | ] 82 | } 83 | }, 84 | "lint": { 85 | "builder": "@angular-devkit/build-angular:tslint", 86 | "options": { 87 | "tsConfig": [ 88 | "src/tsconfig.app.json", 89 | "src/tsconfig.spec.json" 90 | ], 91 | "exclude": [ 92 | "**/node_modules/**" 93 | ] 94 | } 95 | } 96 | } 97 | }, 98 | "angular2-redux-e2e": { 99 | "root": "e2e/", 100 | "projectType": "application", 101 | "architect": { 102 | "e2e": { 103 | "builder": "@angular-devkit/build-angular:protractor", 104 | "options": { 105 | "protractorConfig": "e2e/protractor.conf.js", 106 | "devServerTarget": "angular2-redux:serve" 107 | } 108 | }, 109 | "lint": { 110 | "builder": "@angular-devkit/build-angular:tslint", 111 | "options": { 112 | "tsConfig": "e2e/tsconfig.e2e.json", 113 | "exclude": [ 114 | "**/node_modules/**" 115 | ] 116 | } 117 | } 118 | } 119 | } 120 | }, 121 | "defaultProject": "angular2-redux" 122 | } -------------------------------------------------------------------------------- /src/test/app-store-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppStore } from '../index'; 3 | import { createStore } from 'redux'; 4 | import { Observable } from 'rxjs'; 5 | import { Actions } from '../actions'; 6 | import { AppComponent } from '../app/app.component'; 7 | import { createAppStoreFactory, createAppStoreFactoryWithOptions, applyDevTools } from '../app-store-factory'; 8 | 9 | const reducer = (state = 0, action) => { 10 | if (action.type === 'inc') { 11 | return state + 1; 12 | } else { 13 | return state; 14 | } 15 | }; 16 | 17 | describe('applyDevTools', () => { 18 | beforeEach(async(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [ 21 | AppComponent 22 | ] 23 | }).compileComponents(); 24 | })); 25 | 26 | it('applies debug options properly', () => { 27 | 28 | const wrapper = { 29 | devToolsMiddleware: () => { } 30 | }; 31 | spyOn(wrapper, 'devToolsMiddleware'); 32 | const devToolsMiddlewareSpy = wrapper.devToolsMiddleware; 33 | 34 | window['devToolsExtension'] = () => wrapper.devToolsMiddleware; 35 | 36 | // specifying debug option 37 | applyDevTools(true)(); 38 | expect(devToolsMiddlewareSpy).toHaveBeenCalledTimes(1); 39 | // expect(devToolsMiddlewareSpy.calls.count() === 1); 40 | applyDevTools(false)(); 41 | expect(devToolsMiddlewareSpy).toHaveBeenCalledTimes(1); 42 | 43 | // using function to specify debug option 44 | applyDevTools(() => true)(); 45 | expect(devToolsMiddlewareSpy).toHaveBeenCalledTimes(2); 46 | applyDevTools(() => false)(); 47 | expect(devToolsMiddlewareSpy).toHaveBeenCalledTimes(2); 48 | 49 | // not specifying 50 | applyDevTools(true)(); 51 | expect(devToolsMiddlewareSpy).toHaveBeenCalledTimes(3); 52 | applyDevTools(undefined)(); 53 | expect(devToolsMiddlewareSpy).toHaveBeenCalledTimes(3); 54 | applyDevTools(null)(); 55 | expect(devToolsMiddlewareSpy).toHaveBeenCalledTimes(3); 56 | 57 | }); 58 | }); 59 | 60 | describe('createAppStoreFactoryWithOptions', () => { 61 | beforeEach(async(() => { 62 | TestBed.configureTestingModule({ 63 | declarations: [ 64 | AppComponent 65 | ] 66 | }).compileComponents(); 67 | })); 68 | 69 | it('returns a function that creates an AppStore', () => { 70 | const f = createAppStoreFactory(reducer); 71 | const appStore = f(); 72 | expect(typeof appStore === 'object'); 73 | appStore.dispatch({ type: 'inc' }); 74 | expect(appStore.getState() === 1); 75 | }); 76 | 77 | it('Supports multiple reducers', () => { 78 | const f = createAppStoreFactoryWithOptions({ reducers: { a: reducer, b: reducer } }); 79 | const appStore = f(); 80 | appStore.dispatch({ type: 'inc' }); 81 | expect(appStore.getState() === { a: 1, b: 1 }); 82 | }); 83 | 84 | it('Supports thunks', () => { 85 | const f = createAppStoreFactoryWithOptions({ reducers: reducer }); 86 | const appStore = f(); 87 | appStore.dispatch((dispatch) => { 88 | dispatch({ type: 'inc' }); 89 | dispatch({ type: 'inc' }); 90 | }); 91 | expect(appStore.getState() === 2); 92 | }); 93 | 94 | it('Supports additional middleware', () => { 95 | 96 | let counterInsideLogger = 0; 97 | const logger = store => next => action => { 98 | counterInsideLogger++; 99 | return next(action); 100 | }; 101 | 102 | const f = createAppStoreFactoryWithOptions({ 103 | reducers: reducer 104 | }); 105 | const appStore = f(); 106 | appStore.dispatch((dispatch) => { 107 | dispatch({ type: 'inc' }); 108 | dispatch({ type: 'inc' }); 109 | }); 110 | expect(appStore.getState() === 2); 111 | expect(counterInsideLogger === 2); 112 | }); 113 | 114 | }); 115 | -------------------------------------------------------------------------------- /src/test/app-store.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from '../app/app.component'; 3 | import { AppStore } from '../index'; 4 | import { createStore } from 'redux'; 5 | import { Observable } from 'rxjs'; 6 | import { Actions } from '../actions'; 7 | import { map, distinctUntilChanged } from 'rxjs/operators'; 8 | 9 | const createSimpleAppStore = () => { 10 | return new AppStore(createStore((state: number = 0, action): number => { 11 | if (action.type === 'inc') { 12 | return state + 1; 13 | } else { 14 | return state; 15 | } 16 | })); 17 | }; 18 | 19 | describe('Dispatching Actions', () => { 20 | beforeEach(async(() => { 21 | TestBed.configureTestingModule({ 22 | declarations: [ 23 | AppComponent 24 | ] 25 | }).compileComponents(); 26 | })); 27 | 28 | it('subscription is called when dispatching actions', () => { 29 | 30 | const appStore: AppStore = createSimpleAppStore(); 31 | 32 | let testCounter = 0; 33 | appStore.subscribe(state => testCounter = state); 34 | 35 | appStore.dispatch({ type: 'inc' }); 36 | expect(testCounter === 1); 37 | 38 | }); 39 | 40 | it('createDispatcher works as expected', () => { 41 | 42 | const appStore: AppStore = createSimpleAppStore(); 43 | 44 | let testCounter = 0; 45 | appStore.subscribe(state => testCounter = state); 46 | 47 | const dispatcher = appStore.createDispatcher(() => ({ type: 'inc' }), null); 48 | dispatcher(); 49 | dispatcher(); 50 | expect(testCounter === 2); 51 | 52 | }); 53 | 54 | }); 55 | 56 | describe('Observable', () => { 57 | 58 | it('returned by select()', () => { 59 | const appStore = createSimpleAppStore(); 60 | const state$ = appStore.select(state => state); 61 | expect(state$ === undefined); 62 | }); 63 | 64 | it('contains initial state', () => { 65 | const appStore = createSimpleAppStore(); 66 | let currentState; 67 | 68 | appStore.select(state => state) 69 | .subscribe(state => currentState = state); 70 | 71 | expect(currentState === 0); 72 | }); 73 | 74 | it('updates on dispatch', () => { 75 | const appStore = createSimpleAppStore(); 76 | let currentState; 77 | 78 | appStore.select(state => state) 79 | .subscribe(state => currentState = state); 80 | 81 | appStore.dispatch({ type: 'inc' }); 82 | 83 | expect(currentState === 1); 84 | }); 85 | 86 | it('maps with given selector function', () => { 87 | const appStore = createSimpleAppStore(); 88 | const selector = jasmine.createSpy().and.callFake(state => state * state); 89 | let currentState; 90 | 91 | appStore.select(selector) 92 | .subscribe(state => currentState = state); 93 | 94 | appStore.dispatch({ type: 'inc' }); 95 | expect(currentState === 1); 96 | 97 | appStore.dispatch({ type: 'inc' }); 98 | expect(currentState === 4); 99 | 100 | expect(selector.calls.count() === 3); 101 | }); 102 | 103 | it('maps with given key string', () => { 104 | interface NestedState { foo: number; } 105 | const appStore = new AppStore(createStore((state: NestedState = { foo: 0 }, action) => { 106 | if (action.type === 'inc') { 107 | return { foo: state.foo + 1 }; 108 | } else { 109 | return state; 110 | } 111 | })); 112 | let currentState; 113 | 114 | appStore.select('foo') 115 | .subscribe(state => currentState = state); 116 | 117 | appStore.dispatch({ type: 'inc' }); 118 | expect(currentState === 1); 119 | 120 | appStore.dispatch({ type: 'inc' }); 121 | expect(currentState === 2); 122 | }); 123 | 124 | it('did not emit when selector returns equal values', () => { 125 | const appStore = createSimpleAppStore(); 126 | const sameInstance = {}; 127 | const selector = jasmine.createSpy().and.returnValue(sameInstance); 128 | let currentState; 129 | const listener = jasmine.createSpy().and.callFake(state => currentState = state); 130 | 131 | appStore.select(selector) 132 | .subscribe(listener); 133 | 134 | appStore.dispatch({ type: 'inc' }); 135 | appStore.dispatch({ type: 'inc' }); 136 | expect(currentState === sameInstance); 137 | 138 | expect(selector.calls.count() === 3); 139 | expect(listener.calls.count() === 1); 140 | }); 141 | }); 142 | 143 | --------------------------------------------------------------------------------