Skip to content

Commit

Permalink
feat(store): support store config factory for feature
Browse files Browse the repository at this point in the history
  • Loading branch information
itay committed Dec 28, 2018
1 parent 7f57f11 commit 32fc4a6
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 13 deletions.
13 changes: 13 additions & 0 deletions modules/store/spec/fixtures/counter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
175 changes: 172 additions & 3 deletions modules/store/spec/store.spec.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -10,12 +10,15 @@ import {
ReducerManagerDispatcher,
UPDATE,
REDUCER_FACTORY,
ActionReducer,
Action,
} from '../';
import {
counterReducer,
INCREMENT,
DECREMENT,
RESET,
counterReducer2,
} from './fixtures/counter';
import Spy = jasmine.Spy;
import any = jasmine.any;
Expand All @@ -33,15 +36,18 @@ describe('ngRx Store', () => {
let store: Store<TestAppSchema>;
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,
counter3: counterReducer,
};

TestBed.configureTestingModule({
imports: [StoreModule.forRoot(reducers, { initialState })],
imports: [StoreModule.forRoot(reducers, { initialState, metaReducers })],
});

store = TestBed.get(Store);
Expand Down Expand Up @@ -471,4 +477,167 @@ 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<any, any>) {
return function(state: any, action: Action) {
return reducer(state, action);
};
}

function metaReducer2(reducer: ActionReducer<any, any>) {
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', () => {
it('should initial state with value', (done: DoneFn) => {
const FEATURE_CONFIG_TOKEN = new InjectionToken('Feature Config');
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 FEATURE_CONFIG_TOKEN = new InjectionToken('Feature Config');
const FEATURE_CONFIG2_TOKEN = new InjectionToken('Feature Config2');
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,
});
});
});
});
59 changes: 49 additions & 10 deletions modules/store/src/store_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -56,7 +58,7 @@ export class StoreRootModule {
@NgModule({})
export class StoreFeatureModule implements OnDestroy {
constructor(
@Inject(STORE_FEATURES) private features: StoreFeature<any, any>[],
@Inject(_STORE_FEATURES) private features: StoreFeature<any, any>[],
@Inject(FEATURE_REDUCERS) private featureReducers: ActionReducerMap<any>[],
private reducerManager: ReducerManager,
root: StoreRootModule
Expand Down Expand Up @@ -145,12 +147,12 @@ export class StoreModule {
static forFeature<T, V extends Action = Action>(
featureName: string,
reducers: ActionReducerMap<T, V> | InjectionToken<ActionReducerMap<T, V>>,
config?: StoreConfig<T, V>
config?: StoreConfig<T, V> | InjectionToken<StoreConfig<T, V>>
): ModuleWithProviders<StoreFeatureModule>;
static forFeature<T, V extends Action = Action>(
featureName: string,
reducer: ActionReducer<T, V> | InjectionToken<ActionReducer<T, V>>,
config?: StoreConfig<T, V>
config?: StoreConfig<T, V> | InjectionToken<StoreConfig<T, V>>
): ModuleWithProviders<StoreFeatureModule>;
static forFeature(
featureName: string,
Expand All @@ -159,23 +161,40 @@ export class StoreModule {
| InjectionToken<ActionReducerMap<any, any>>
| ActionReducer<any, any>
| InjectionToken<ActionReducer<any, any>>,
config: StoreConfig<any, any> = {}
config: StoreConfig<any, any> | InjectionToken<StoreConfig<any, any>> = {}
): ModuleWithProviders<StoreFeatureModule> {
return {
ngModule: StoreFeatureModule,
providers: [
{
provide: _FEATURE_CONFIGS,
multi: true,
useValue: config,
},
{
provide: STORE_FEATURES,
multi: true,
useValue: <StoreFeature<any, any>>{
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,
Expand Down Expand Up @@ -206,6 +225,26 @@ export function _createStoreReducers(
return reducers instanceof InjectionToken ? injector.get(reducers) : reducers;
}

// is could also be done in StoreFeatureModule constructor.
export function _createFeatureStore(
injector: Injector,
configs: StoreConfig<any, any>[] | InjectionToken<StoreConfig<any, any>>[],
featureStores: StoreFeature<any, any>[]
) {
return featureStores.map((feat, index) => {
if (configs[index] instanceof InjectionToken) {
const conf = injector.get(configs[index]);
return {
key: feat.key,
reducerFactory: conf.reducerFactory,
metaReducers: conf.metaReducers ? conf.metaReducers : [],
initialState: conf.initialState,
};
}
return feat;
});
}

export function _createFeatureReducers(
injector: Injector,
reducerCollection: ActionReducerMap<any, any>[],
Expand Down
9 changes: 9 additions & 0 deletions modules/store/src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
Expand Down

0 comments on commit 32fc4a6

Please sign in to comment.