Skip to content

Commit

Permalink
feat(effects): dispatch feature effects action on init (#1305)
Browse files Browse the repository at this point in the history
Closes #683
  • Loading branch information
timdeschryver authored and brandonroberts committed Aug 31, 2018
1 parent fa21f29 commit 15a4b58
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 20 deletions.
27 changes: 26 additions & 1 deletion docs/effects/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ You can see this action as a lifecycle hook, which you can use in order to execu
@Effect()
init$ = this.actions$.pipe(
ofType(ROOT_EFFECTS_INIT),
map(_ => ...)
map(action => ...)
);
```

Expand All @@ -45,6 +45,31 @@ Usage:
export class FeatureModule {}
```

### UPDATE_EFFECTS

After feature effects are registered, an `UPDATE_EFFECTS` action is dispatched.

```ts
type UpdateEffects = {
type: typeof UPDATE_EFFECTS;
effects: string[];
};
```

For example, when you register your feature module as `EffectsModule.forFeature([SomeEffectsClass, AnotherEffectsClass])`,
it has `SomeEffectsClass` and `AnotherEffectsClass` in an array as its payload.

To dispatch an action when the `SomeEffectsClass` effect has been registered, listen to the `UPDATE_EFFECTS` action and use the `effects` payload to filter out non-important effects.

```ts
@Effect()
init = this.actions.pipe(
ofType<UpdateEffects>(UPDATE_EFFECTS)
filter(action => action.effects.includes('SomeEffectsClass')),
map(action => ...)
);
```

## Actions

Stream of all actions dispatched in your application including actions dispatched by effect streams.
Expand Down
77 changes: 65 additions & 12 deletions modules/effects/spec/effects_feature_module.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable, NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { combineLatest } from 'rxjs';
import {
Action,
createFeatureSelector,
Expand All @@ -8,25 +9,39 @@ import {
Store,
StoreModule,
} from '@ngrx/store';
import { Observable } from 'rxjs';
import { map, withLatestFrom } from 'rxjs/operators';

import { Actions, Effect, EffectsModule } from '../';
import { EffectsFeatureModule } from '../src/effects_feature_module';
import { map, withLatestFrom, filter } from 'rxjs/operators';
import { Actions, Effect, EffectsModule, ofType } from '../';
import {
EffectsFeatureModule,
UPDATE_EFFECTS,
UpdateEffects,
} from '../src/effects_feature_module';
import { EffectsRootModule } from '../src/effects_root_module';
import { FEATURE_EFFECTS } from '../src/tokens';

describe('Effects Feature Module', () => {
describe('when registered', () => {
const sourceA = 'sourceA';
const sourceB = 'sourceB';
const sourceC = 'sourceC';
const effectSourceGroups = [[sourceA], [sourceB], [sourceC]];
class SourceA {}
class SourceB {}
class SourceC {}

const sourceA = new SourceA();
const sourceB = new SourceB();
const sourceC = new SourceC();

const effectSourceGroups = [[sourceA], [sourceB, sourceC]];
let mockEffectSources: { addEffects: jasmine.Spy };
let mockStore: { dispatch: jasmine.Spy };

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: Store,
useValue: {
dispatch: jasmine.createSpy('dispatch'),
},
},
{
provide: EffectsRootModule,
useValue: {
Expand All @@ -42,6 +57,7 @@ describe('Effects Feature Module', () => {
});

mockEffectSources = TestBed.get(EffectsRootModule);
mockStore = TestBed.get(Store);
});

it('should add all effects when instantiated', () => {
Expand All @@ -51,11 +67,24 @@ describe('Effects Feature Module', () => {
expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceB);
expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceC);
});

it('should dispatch update-effects actions when instantiated', () => {
TestBed.get(EffectsFeatureModule);

expect(mockStore.dispatch).toHaveBeenCalledWith({
type: UPDATE_EFFECTS,
effects: ['SourceA'],
});

expect(mockStore.dispatch).toHaveBeenCalledWith({
type: UPDATE_EFFECTS,
effects: ['SourceB', 'SourceC'],
});
});
});

describe('when registered in a different NgModule from the feature state', () => {
let effects: FeatureEffects;
let actions$: Observable<any>;
let store: Store<any>;

beforeEach(() => {
Expand All @@ -77,8 +106,12 @@ describe('Effects Feature Module', () => {

store.dispatch(action);

store.pipe(select(getDataState)).subscribe(res => {
expect(res).toBe(110);
combineLatest(
store.pipe(select(getDataState)),
store.pipe(select(getInitialized))
).subscribe(([data, initialized]) => {
expect(data).toBe(110);
expect(initialized).toBe(true);
done();
});
});
Expand All @@ -93,16 +126,25 @@ interface State {

interface DataState {
data: number;
initialized: boolean;
}

const initialState: DataState = {
data: 100,
initialized: false,
};

function reducer(state: DataState = initialState, action: Action) {
switch (action.type) {
case 'INITIALIZE_FEATURE': {
return {
...state,
initialized: true,
};
}
case 'INCREASE':
return {
...state,
data: state.data + 10,
};
}
Expand All @@ -112,11 +154,22 @@ function reducer(state: DataState = initialState, action: Action) {
const getFeatureState = createFeatureSelector<DataState>(FEATURE_KEY);

const getDataState = createSelector(getFeatureState, state => state.data);
const getInitialized = createSelector(
getFeatureState,
state => state.initialized
);

@Injectable()
class FeatureEffects {
constructor(private actions: Actions, private store: Store<State>) {}

@Effect()
init = this.actions.pipe(
ofType<UpdateEffects>(UPDATE_EFFECTS),
filter(action => action.effects.includes('FeatureEffects')),
map(action => ({ type: 'INITIALIZE_FEATURE' }))
);

@Effect()
effectWithStore = this.actions.ofType('INCREMENT').pipe(
withLatestFrom(this.store.select(getDataState)),
Expand Down
32 changes: 25 additions & 7 deletions modules/effects/src/effects_feature_module.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
import { NgModule, Inject, Optional } from '@angular/core';
import { StoreRootModule, StoreFeatureModule } from '@ngrx/store';
import { StoreRootModule, StoreFeatureModule, Store } from '@ngrx/store';
import { EffectsRootModule } from './effects_root_module';
import { FEATURE_EFFECTS } from './tokens';
import { getSourceForInstance } from './effects_metadata';

export const UPDATE_EFFECTS = '@ngrx/effects/update-effects';
export type UpdateEffects = {
type: typeof UPDATE_EFFECTS;
effects: string[];
};

@NgModule({})
export class EffectsFeatureModule {
constructor(
private root: EffectsRootModule,
root: EffectsRootModule,
store: Store<any>,
@Inject(FEATURE_EFFECTS) effectSourceGroups: any[][],
@Optional() storeRootModule: StoreRootModule,
@Optional() storeFeatureModule: StoreFeatureModule
) {
effectSourceGroups.forEach(group =>
group.forEach(effectSourceInstance =>
root.addEffects(effectSourceInstance)
)
);
effectSourceGroups.forEach(group => {
let effectSourceNames: string[] = [];

group.forEach(effectSourceInstance => {
root.addEffects(effectSourceInstance);

const { constructor } = getSourceForInstance(effectSourceInstance);
effectSourceNames.push(constructor.name);
});

store.dispatch(<UpdateEffects>{
type: UPDATE_EFFECTS,
effects: effectSourceNames,
});
});
}
}
1 change: 1 addition & 0 deletions modules/effects/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { EffectSources } from './effect_sources';
export { OnRunEffects } from './on_run_effects';
export { EffectNotification } from './effect_notification';
export { ROOT_EFFECTS_INIT } from './effects_root_module';
export { UPDATE_EFFECTS, UpdateEffects } from './effects_feature_module';

0 comments on commit 15a4b58

Please sign in to comment.