Skip to content

Commit

Permalink
feat(Store): Allow parent modules to provide reducers with tokens (#36)
Browse files Browse the repository at this point in the history
Also allows feature modules to declare just one reducer function instead
of a complete action reducer map.

Closes #34
  • Loading branch information
MikeRyanDev authored and brandonroberts committed Jun 12, 2017
1 parent 3459bc5 commit 069b12f
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 34 deletions.
74 changes: 59 additions & 15 deletions modules/store/spec/modules.spec.ts
Original file line number Diff line number Diff line change
@@ -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<any>;
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<State>;

const reducersToken = new InjectionToken<ActionReducerMap<RootState>>('Root Reducers');
const rootFruitReducer: ActionReducer<string> = () => 'apple';
const featureAReducer: ActionReducer<FeatureAState> = () => 5;
const featureBListReducer: ActionReducer<number[]> = () => [1, 2, 3];
const featureBIndexReducer: ActionReducer<number> = () => 2;
const featureBReducerMap: ActionReducerMap<FeatureBState> = {
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<RootState>(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,
}
});
});
Expand Down
18 changes: 12 additions & 6 deletions modules/store/spec/ngc/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,33 @@ export const reducerToken = new InjectionToken('Reducers');
})
export class NgcSpecComponent {
count: Observable<number>;
constructor(public store:Store<AppState>){
constructor(public store: Store<AppState>) {
this.count = store.select(state => state.count);
}
increment(){
increment() {
this.store.dispatch({ type: INCREMENT });
}
decrement(){
decrement() {
this.store.dispatch({ type: DECREMENT });
}
}

@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 { }
2 changes: 1 addition & 1 deletion modules/store/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface ActionReducerFactory<T, V extends Action = Action> {

export interface StoreFeature<T, V extends Action = Action> {
key: string;
reducers: ActionReducerMap<T, V>;
reducers: ActionReducerMap<T, V> | ActionReducer<T, V>;
reducerFactory: ActionReducerFactory<T, V>;
initialState: T | undefined;
}
Expand Down
2 changes: 1 addition & 1 deletion modules/store/src/reducer_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class ReducerManager extends BehaviorSubject<ActionReducer<any, any>> imp
}

addFeature({ reducers, reducerFactory, initialState, key }: StoreFeature<any, any>) {
const reducer = reducerFactory(reducers, initialState);
const reducer = typeof reducers === 'function' ? reducers : reducerFactory(reducers, initialState);

this.addReducer(key, reducer);
}
Expand Down
4 changes: 4 additions & 0 deletions modules/store/src/selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,7 @@ export function createFeatureSelector<T>(featureName: string): MemoizedSelector<

return Object.assign(memoized, { release: reset });
}

export function isSelector(v: any): v is MemoizedSelector<any, any> {
return typeof v === 'function' && v.release && typeof v.release === 'function';
}
8 changes: 6 additions & 2 deletions modules/store/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -36,11 +37,14 @@ export class Store<T> extends Observable<Readonly<T>> implements Observer<Action
if (typeof pathOrMapFn === 'string') {
mapped$ = pluck.call(this, pathOrMapFn, ...paths);
}
else if (typeof pathOrMapFn === 'function') {
else if (typeof pathOrMapFn === 'function' && isSelector(pathOrMapFn)) {
mapped$ = map.call(this, pathOrMapFn);
}
else if (typeof pathOrMapFn === 'function') {
mapped$ = map.call(this, createSelector(s => 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'`);
}

Expand Down
19 changes: 10 additions & 9 deletions modules/store/src/store_module.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<any, any>[],
Expand All @@ -31,15 +31,15 @@ export class StoreFeatureModule implements OnDestroy {

export type StoreConfig<T, V extends Action = Action> = { initialState?: T, reducerFactory?: ActionReducerFactory<T, V> };

@NgModule({ })
@NgModule({})
export class StoreModule {
static forRoot<T, V extends Action = Action>(reducers: ActionReducerMap<T, V>, config?: StoreConfig<T, V>): ModuleWithProviders;
static forRoot(reducers: ActionReducerMap<any, any>, config: StoreConfig<any, any> = { }): ModuleWithProviders {
static forRoot<T, V extends Action = Action>(reducers: ActionReducerMap<T, V> | InjectionToken<ActionReducerMap<T, V>>, config?: StoreConfig<T, V>): ModuleWithProviders;
static forRoot(reducers: ActionReducerMap<any, any> | InjectionToken<ActionReducerMap<any, any>>, config: StoreConfig<any, any> = {}): 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,
Expand All @@ -51,7 +51,8 @@ export class StoreModule {
}

static forFeature<T, V extends Action = Action>(featureName: string, reducers: ActionReducerMap<T, V>, config?: StoreConfig<T, V>): ModuleWithProviders;
static forFeature(featureName: string, reducers: ActionReducerMap<any, any>, config: StoreConfig<any, any> = {}): ModuleWithProviders {
static forFeature<T, V extends Action = Action>(featureName: string, reducer: ActionReducer<T, V>, config?: StoreConfig<T, V>): ModuleWithProviders;
static forFeature(featureName: string, reducers: ActionReducerMap<any, any> | ActionReducer<any, any>, config: StoreConfig<any, any> = {}): ModuleWithProviders {
return {
ngModule: StoreFeatureModule,
providers: [
Expand Down

0 comments on commit 069b12f

Please sign in to comment.