From 6aa5645144e2b64664b32f09047c9149ee0691df Mon Sep 17 00:00:00 2001 From: Itay Oded Date: Fri, 4 Jan 2019 17:03:14 +0200 Subject: [PATCH] feat(store): support store config factory for feature (#1445) Closes #1414 --- modules/store/spec/fixtures/counter.ts | 13 ++ modules/store/spec/store.spec.ts | 244 ++++++++++++++++++++++++- modules/store/src/store_module.ts | 60 +++++- modules/store/src/tokens.ts | 9 + 4 files changed, 313 insertions(+), 13 deletions(-) diff --git a/modules/store/spec/fixtures/counter.ts b/modules/store/spec/fixtures/counter.ts index 1255e2f70c..1c5ae37945 100644 --- a/modules/store/spec/fixtures/counter.ts +++ b/modules/store/spec/fixtures/counter.ts @@ -16,3 +16,16 @@ export function counterReducer(state = 0, action: Action) { return state; } } + +export function counterReducer2(state = 0, action: Action) { + switch (action.type) { + case INCREMENT: + return state + 1; + case DECREMENT: + return state - 1; + case RESET: + return 0; + default: + return state; + } +} diff --git a/modules/store/spec/store.spec.ts b/modules/store/spec/store.spec.ts index 482b535eb7..eb9dd4a759 100644 --- a/modules/store/spec/store.spec.ts +++ b/modules/store/spec/store.spec.ts @@ -1,4 +1,4 @@ -import { ReflectiveInjector } from '@angular/core'; +import { ReflectiveInjector, InjectionToken } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { hot } from 'jasmine-marbles'; import { @@ -10,12 +10,16 @@ import { ReducerManagerDispatcher, UPDATE, REDUCER_FACTORY, + ActionReducer, + Action, } from '../'; +import { StoreConfig } from '../src/store_module'; import { counterReducer, INCREMENT, DECREMENT, RESET, + counterReducer2, } from './fixtures/counter'; import Spy = jasmine.Spy; import any = jasmine.any; @@ -33,7 +37,10 @@ describe('ngRx Store', () => { let store: Store; let dispatcher: ActionsSubject; - function setup(initialState: any = { counter1: 0, counter2: 1 }) { + function setup( + initialState: any = { counter1: 0, counter2: 1 }, + metaReducers: any = [] + ) { const reducers = { counter1: counterReducer, counter2: counterReducer, @@ -41,7 +48,7 @@ describe('ngRx Store', () => { }; TestBed.configureTestingModule({ - imports: [StoreModule.forRoot(reducers, { initialState })], + imports: [StoreModule.forRoot(reducers, { initialState, metaReducers })], }); store = TestBed.get(Store); @@ -471,4 +478,235 @@ describe('ngRx Store', () => { mockStore.dispatch(action); }); }); + + describe('Meta Reducers', () => { + let metaReducerContainer: any; + let metaReducerSpy1: Spy; + let metaReducerSpy2: Spy; + + beforeEach(() => { + metaReducerContainer = (function() { + function metaReducer1(reducer: ActionReducer) { + return function(state: any, action: Action) { + return reducer(state, action); + }; + } + + function metaReducer2(reducer: ActionReducer) { + return function(state: any, action: Action) { + return reducer(state, action); + }; + } + + return { + metaReducer1: metaReducer1, + metaReducer2: metaReducer2, + }; + })(); + + metaReducerSpy1 = spyOn( + metaReducerContainer, + 'metaReducer1' + ).and.callThrough(); + + metaReducerSpy2 = spyOn( + metaReducerContainer, + 'metaReducer2' + ).and.callThrough(); + }); + + it('should create a meta reducer for root and call it through', () => { + setup({}, [metaReducerContainer.metaReducer1]); + const action = { type: INCREMENT }; + store.dispatch(action); + expect(metaReducerSpy1).toHaveBeenCalled(); + }); + + it('should call two meta reducers', () => { + setup({}, [ + metaReducerContainer.metaReducer1, + metaReducerContainer.metaReducer2, + ]); + const action = { type: INCREMENT }; + store.dispatch(action); + + expect(metaReducerSpy1).toHaveBeenCalled(); + expect(metaReducerSpy2).toHaveBeenCalled(); + }); + + it('should create a meta reducer for feature and call it with the expected reducer', () => { + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({}), + StoreModule.forFeature('counter1', counterReducer, { + metaReducers: [metaReducerContainer.metaReducer1], + }), + StoreModule.forFeature('counter2', counterReducer2, { + metaReducers: [metaReducerContainer.metaReducer2], + }), + ], + }); + const mockStore = TestBed.get(Store); + const action = { type: INCREMENT }; + mockStore.dispatch(action); + + expect(metaReducerSpy1).toHaveBeenCalledWith(counterReducer); + expect(metaReducerSpy2).toHaveBeenCalledWith(counterReducer2); + }); + }); + + describe('Feature config token', () => { + let FEATURE_CONFIG_TOKEN: InjectionToken>; + let FEATURE_CONFIG2_TOKEN: InjectionToken>; + + beforeEach(() => { + FEATURE_CONFIG_TOKEN = new InjectionToken('Feature Config'); + FEATURE_CONFIG2_TOKEN = new InjectionToken('Feature Config2'); + }); + + it('should initial state with value', (done: DoneFn) => { + const initialState = { counter1: 1 }; + const featureKey = 'counter'; + + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({}), + StoreModule.forFeature( + featureKey, + counterReducer, + FEATURE_CONFIG_TOKEN + ), + ], + providers: [ + { + provide: FEATURE_CONFIG_TOKEN, + useValue: { initialState: initialState }, + }, + ], + }); + + const mockStore = TestBed.get(Store); + + mockStore.pipe(take(1)).subscribe({ + next(val: any) { + expect(val[featureKey]).toEqual(initialState); + }, + error: done, + complete: done, + }); + }); + + it('should initial state with value for multi features', (done: DoneFn) => { + const initialState = 1; + const initialState2 = 2; + const initialState3 = 3; + const featureKey = 'counter'; + const featureKey2 = 'counter2'; + const featureKey3 = 'counter3'; + + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({}), + StoreModule.forFeature( + featureKey, + counterReducer, + FEATURE_CONFIG_TOKEN + ), + StoreModule.forFeature( + featureKey2, + counterReducer, + FEATURE_CONFIG2_TOKEN + ), + StoreModule.forFeature(featureKey3, counterReducer, { + initialState: initialState3, + }), + ], + providers: [ + { + provide: FEATURE_CONFIG_TOKEN, + useValue: { initialState: initialState }, + }, + { + provide: FEATURE_CONFIG2_TOKEN, + useValue: { initialState: initialState2 }, + }, + ], + }); + + const mockStore = TestBed.get(Store); + + mockStore.pipe(take(1)).subscribe({ + next(val: any) { + expect(val[featureKey]).toEqual(initialState); + expect(val[featureKey2]).toEqual(initialState2); + expect(val[featureKey3]).toEqual(initialState3); + }, + error: done, + complete: done, + }); + }); + + it('should create a meta reducer with config injection token and call it with the expected reducer', () => { + const metaReducerContainer = (function() { + function metaReducer1(reducer: ActionReducer) { + return function(state: any, action: Action) { + return reducer(state, action); + }; + } + + function metaReducer2(reducer: ActionReducer) { + return function(state: any, action: Action) { + return reducer(state, action); + }; + } + + return { + metaReducer1: metaReducer1, + metaReducer2: metaReducer2, + }; + })(); + + const metaReducerSpy1 = spyOn( + metaReducerContainer, + 'metaReducer1' + ).and.callThrough(); + + const metaReducerSpy2 = spyOn( + metaReducerContainer, + 'metaReducer2' + ).and.callThrough(); + + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({}), + StoreModule.forFeature( + 'counter1', + counterReducer, + FEATURE_CONFIG_TOKEN + ), + StoreModule.forFeature( + 'counter2', + counterReducer2, + FEATURE_CONFIG2_TOKEN + ), + ], + providers: [ + { + provide: FEATURE_CONFIG_TOKEN, + useValue: { metaReducers: [metaReducerContainer.metaReducer1] }, + }, + { + provide: FEATURE_CONFIG2_TOKEN, + useValue: { metaReducers: [metaReducerContainer.metaReducer2] }, + }, + ], + }); + const mockStore = TestBed.get(Store); + const action = { type: INCREMENT }; + mockStore.dispatch(action); + + expect(metaReducerSpy1).toHaveBeenCalledWith(counterReducer); + expect(metaReducerSpy2).toHaveBeenCalledWith(counterReducer2); + }); + }); }); diff --git a/modules/store/src/store_module.ts b/modules/store/src/store_module.ts index 4f0b7a3544..407f78e409 100644 --- a/modules/store/src/store_module.ts +++ b/modules/store/src/store_module.ts @@ -29,6 +29,8 @@ import { FEATURE_REDUCERS, _FEATURE_REDUCERS, _FEATURE_REDUCERS_TOKEN, + _STORE_FEATURES, + _FEATURE_CONFIGS, } from './tokens'; import { ACTIONS_SUBJECT_PROVIDERS, ActionsSubject } from './actions_subject'; import { @@ -56,7 +58,7 @@ export class StoreRootModule { @NgModule({}) export class StoreFeatureModule implements OnDestroy { constructor( - @Inject(STORE_FEATURES) private features: StoreFeature[], + @Inject(_STORE_FEATURES) private features: StoreFeature[], @Inject(FEATURE_REDUCERS) private featureReducers: ActionReducerMap[], private reducerManager: ReducerManager, root: StoreRootModule @@ -145,12 +147,12 @@ export class StoreModule { static forFeature( featureName: string, reducers: ActionReducerMap | InjectionToken>, - config?: StoreConfig + config?: StoreConfig | InjectionToken> ): ModuleWithProviders; static forFeature( featureName: string, reducer: ActionReducer | InjectionToken>, - config?: StoreConfig + config?: StoreConfig | InjectionToken> ): ModuleWithProviders; static forFeature( featureName: string, @@ -159,23 +161,40 @@ export class StoreModule { | InjectionToken> | ActionReducer | InjectionToken>, - config: StoreConfig = {} + config: StoreConfig | InjectionToken> = {} ): ModuleWithProviders { return { ngModule: StoreFeatureModule, providers: [ + { + provide: _FEATURE_CONFIGS, + multi: true, + useValue: config, + }, { provide: STORE_FEATURES, multi: true, - useValue: >{ + useValue: { key: featureName, - reducerFactory: config.reducerFactory - ? config.reducerFactory - : combineReducers, - metaReducers: config.metaReducers ? config.metaReducers : [], - initialState: config.initialState, + reducerFactory: + !(config instanceof InjectionToken) && config.reducerFactory + ? config.reducerFactory + : combineReducers, + metaReducers: + !(config instanceof InjectionToken) && config.metaReducers + ? config.metaReducers + : [], + initialState: + !(config instanceof InjectionToken) && config.initialState + ? config.initialState + : undefined, }, }, + { + provide: _STORE_FEATURES, + deps: [Injector, _FEATURE_CONFIGS, STORE_FEATURES], + useFactory: _createFeatureStore, + }, { provide: _FEATURE_REDUCERS, multi: true, useValue: reducers }, { provide: _FEATURE_REDUCERS_TOKEN, @@ -206,6 +225,27 @@ export function _createStoreReducers( return reducers instanceof InjectionToken ? injector.get(reducers) : reducers; } +export function _createFeatureStore( + injector: Injector, + configs: StoreConfig[] | InjectionToken>[], + featureStores: StoreFeature[] +) { + return featureStores.map((feat, index) => { + if (configs[index] instanceof InjectionToken) { + const conf = injector.get(configs[index]); + return { + key: feat.key, + reducerFactory: conf.reducerFactory + ? conf.reducerFactory + : combineReducers, + metaReducers: conf.metaReducers ? conf.metaReducers : [], + initialState: conf.initialState, + }; + } + return feat; + }); +} + export function _createFeatureReducers( injector: Injector, reducerCollection: ActionReducerMap[], diff --git a/modules/store/src/tokens.ts b/modules/store/src/tokens.ts index 7e2bd233aa..ae5f43fe6d 100644 --- a/modules/store/src/tokens.ts +++ b/modules/store/src/tokens.ts @@ -24,6 +24,15 @@ export const _STORE_REDUCERS = new InjectionToken( export const _FEATURE_REDUCERS = new InjectionToken( '@ngrx/store Internal Feature Reducers' ); + +export const _FEATURE_CONFIGS = new InjectionToken( + '@ngrx/store Internal Feature Configs' +); + +export const _STORE_FEATURES = new InjectionToken( + '@ngrx/store Internal Store Features' +); + export const _FEATURE_REDUCERS_TOKEN = new InjectionToken( '@ngrx/store Internal Feature Reducers Token' );