From 069b12fcb62b2f905893022e7e8abbcf47b44b42 Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Sun, 11 Jun 2017 19:50:31 -0500 Subject: [PATCH] feat(Store): Allow parent modules to provide reducers with tokens (#36) Also allows feature modules to declare just one reducer function instead of a complete action reducer map. Closes #34 --- modules/store/spec/modules.spec.ts | 74 ++++++++++++++++++++++------ modules/store/spec/ngc/main.ts | 18 ++++--- modules/store/src/models.ts | 2 +- modules/store/src/reducer_manager.ts | 2 +- modules/store/src/selector.ts | 4 ++ modules/store/src/store.ts | 8 ++- modules/store/src/store_module.ts | 19 +++---- 7 files changed, 93 insertions(+), 34 deletions(-) diff --git a/modules/store/spec/modules.spec.ts b/modules/store/spec/modules.spec.ts index f9397e5976..be334abd00 100644 --- a/modules/store/spec/modules.spec.ts +++ b/modules/store/spec/modules.spec.ts @@ -1,29 +1,73 @@ -import 'rxjs/add/operator/take'; -import { zip } from 'rxjs/observable/zip'; -import { ReflectiveInjector } from '@angular/core'; -import { createInjector, createChildInjector } from './helpers/injector'; -import { StoreModule, Store } from '../'; +import { TestBed } from '@angular/core/testing'; +import { NgModule, InjectionToken } from '@angular/core'; +import { StoreModule, Store, ActionReducer, ActionReducerMap } from '../'; describe('Nested Store Modules', () => { - let store: Store; + type RootState = { fruit: string }; + type FeatureAState = number; + type FeatureBState = { list: number[], index: number }; + type State = RootState & { a: FeatureAState } & { b: FeatureBState }; - beforeEach(() => { - const parentReducers = { stateKey: () => 'root' }; - const featureReducers = { stateKey: () => 'child' }; + let store: Store; + + const reducersToken = new InjectionToken>('Root Reducers'); + const rootFruitReducer: ActionReducer = () => 'apple'; + const featureAReducer: ActionReducer = () => 5; + const featureBListReducer: ActionReducer = () => [1, 2, 3]; + const featureBIndexReducer: ActionReducer = () => 2; + const featureBReducerMap: ActionReducerMap = { + list: featureBListReducer, + index: featureBIndexReducer, + }; + + @NgModule({ + imports: [ + StoreModule.forFeature('a', featureAReducer), + ] + }) + class FeatureAModule { } + + @NgModule({ + imports: [ + StoreModule.forFeature('b', featureBReducerMap), + ] + }) + class FeatureBModule { } - const rootInjector = createInjector(StoreModule.forRoot(parentReducers)); - const featureInjector = createChildInjector(rootInjector, StoreModule.forFeature('inner', featureReducers)); + @NgModule({ + imports: [ + StoreModule.forRoot(reducersToken), + FeatureAModule, + FeatureBModule, + ], + providers: [ + { + provide: reducersToken, + useValue: { fruit: rootFruitReducer }, + } + ] + }) + class RootModule { } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + RootModule, + ] + }); - store = rootInjector.get(Store); + store = TestBed.get(Store); }); it('should nest the child module in the root store object', () => { store.take(1).subscribe(state => { expect(state).toEqual({ - stateKey: 'root', - inner: { - stateKey: 'child' + fruit: 'apple', + a: 5, + b: { + list: [1, 2, 3], + index: 2, } }); }); diff --git a/modules/store/spec/ngc/main.ts b/modules/store/spec/ngc/main.ts index bd348af620..baed338dee 100644 --- a/modules/store/spec/ngc/main.ts +++ b/modules/store/spec/ngc/main.ts @@ -45,13 +45,13 @@ export const reducerToken = new InjectionToken('Reducers'); }) export class NgcSpecComponent { count: Observable; - constructor(public store:Store){ + constructor(public store: Store) { this.count = store.select(state => state.count); } - increment(){ + increment() { this.store.dispatch({ type: INCREMENT }); } - decrement(){ + decrement() { this.store.dispatch({ type: DECREMENT }); } } @@ -59,13 +59,19 @@ export class NgcSpecComponent { @NgModule({ imports: [ BrowserModule, - StoreModule.forRoot({ count: counterReducer }, { - initialState: { count : 0 }, + StoreModule.forRoot(reducerToken, { + initialState: { count: 0 }, reducerFactory: combineReducers }), FeatureModule ], + providers: [ + { + provide: reducerToken, + useValue: { count: counterReducer } + } + ], declarations: [NgcSpecComponent], bootstrap: [NgcSpecComponent] }) -export class NgcSpecModule {} +export class NgcSpecModule { } diff --git a/modules/store/src/models.ts b/modules/store/src/models.ts index 3e29d4b68c..e79fec2654 100644 --- a/modules/store/src/models.ts +++ b/modules/store/src/models.ts @@ -16,7 +16,7 @@ export interface ActionReducerFactory { export interface StoreFeature { key: string; - reducers: ActionReducerMap; + reducers: ActionReducerMap | ActionReducer; reducerFactory: ActionReducerFactory; initialState: T | undefined; } diff --git a/modules/store/src/reducer_manager.ts b/modules/store/src/reducer_manager.ts index e7e3d551b1..cd7e12abca 100644 --- a/modules/store/src/reducer_manager.ts +++ b/modules/store/src/reducer_manager.ts @@ -23,7 +23,7 @@ export class ReducerManager extends BehaviorSubject> imp } addFeature({ reducers, reducerFactory, initialState, key }: StoreFeature) { - const reducer = reducerFactory(reducers, initialState); + const reducer = typeof reducers === 'function' ? reducers : reducerFactory(reducers, initialState); this.addReducer(key, reducer); } diff --git a/modules/store/src/selector.ts b/modules/store/src/selector.ts index aab037f264..a03ed660ad 100644 --- a/modules/store/src/selector.ts +++ b/modules/store/src/selector.ts @@ -125,3 +125,7 @@ export function createFeatureSelector(featureName: string): MemoizedSelector< return Object.assign(memoized, { release: reset }); } + +export function isSelector(v: any): v is MemoizedSelector { + return typeof v === 'function' && v.release && typeof v.release === 'function'; +} diff --git a/modules/store/src/store.ts b/modules/store/src/store.ts index 9de1de0b71..5d36ced139 100644 --- a/modules/store/src/store.ts +++ b/modules/store/src/store.ts @@ -9,6 +9,7 @@ import { Action, ActionReducer } from './models'; import { ActionsSubject } from './actions_subject'; import { StateObservable } from './state'; import { ReducerManager } from './reducer_manager'; +import { isSelector, createSelector } from './selector'; @Injectable() @@ -36,11 +37,14 @@ export class Store extends Observable> implements Observer s, pathOrMapFn)); + } else { - throw new TypeError(`Unexpected type '${ typeof pathOrMapFn }' in select operator,` + throw new TypeError(`Unexpected type '${typeof pathOrMapFn}' in select operator,` + ` expected 'string' or 'function'`); } diff --git a/modules/store/src/store_module.ts b/modules/store/src/store_module.ts index 68c78c3183..a4ff94f115 100644 --- a/modules/store/src/store_module.ts +++ b/modules/store/src/store_module.ts @@ -1,5 +1,5 @@ -import { NgModule, Inject, ModuleWithProviders, OnDestroy } from '@angular/core'; -import { Action, ActionReducerMap, ActionReducerFactory, StoreFeature } from './models'; +import { NgModule, Inject, ModuleWithProviders, OnDestroy, InjectionToken } from '@angular/core'; +import { Action, ActionReducer, ActionReducerMap, ActionReducerFactory, StoreFeature } from './models'; import { combineReducers } from './utils'; import { INITIAL_STATE, INITIAL_REDUCERS, REDUCER_FACTORY, STORE_FEATURES } from './tokens'; import { ACTIONS_SUBJECT_PROVIDERS } from './actions_subject'; @@ -10,12 +10,12 @@ import { STORE_PROVIDERS } from './store'; -@NgModule({ }) +@NgModule({}) export class StoreRootModule { } -@NgModule({ }) +@NgModule({}) export class StoreFeatureModule implements OnDestroy { constructor( @Inject(STORE_FEATURES) private features: StoreFeature[], @@ -31,15 +31,15 @@ export class StoreFeatureModule implements OnDestroy { export type StoreConfig = { initialState?: T, reducerFactory?: ActionReducerFactory }; -@NgModule({ }) +@NgModule({}) export class StoreModule { - static forRoot(reducers: ActionReducerMap, config?: StoreConfig): ModuleWithProviders; - static forRoot(reducers: ActionReducerMap, config: StoreConfig = { }): ModuleWithProviders { + static forRoot(reducers: ActionReducerMap | InjectionToken>, config?: StoreConfig): ModuleWithProviders; + static forRoot(reducers: ActionReducerMap | InjectionToken>, config: StoreConfig = {}): ModuleWithProviders { return { ngModule: StoreRootModule, providers: [ { provide: INITIAL_STATE, useValue: config.initialState }, - { provide: INITIAL_REDUCERS, useValue: reducers }, + reducers instanceof InjectionToken ? { provide: INITIAL_REDUCERS, useExisting: reducers } : { provide: INITIAL_REDUCERS, useValue: reducers }, { provide: REDUCER_FACTORY, useValue: config.reducerFactory ? config.reducerFactory : combineReducers }, ACTIONS_SUBJECT_PROVIDERS, REDUCER_MANAGER_PROVIDERS, @@ -51,7 +51,8 @@ export class StoreModule { } static forFeature(featureName: string, reducers: ActionReducerMap, config?: StoreConfig): ModuleWithProviders; - static forFeature(featureName: string, reducers: ActionReducerMap, config: StoreConfig = {}): ModuleWithProviders { + static forFeature(featureName: string, reducer: ActionReducer, config?: StoreConfig): ModuleWithProviders; + static forFeature(featureName: string, reducers: ActionReducerMap | ActionReducer, config: StoreConfig = {}): ModuleWithProviders { return { ngModule: StoreFeatureModule, providers: [