diff --git a/docs/effects/api.md b/docs/effects/api.md index 5ffb8eaac8..ff5e946ab2 100644 --- a/docs/effects/api.md +++ b/docs/effects/api.md @@ -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 => ...) ); ``` @@ -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(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. diff --git a/modules/effects/spec/effects_feature_module.spec.ts b/modules/effects/spec/effects_feature_module.spec.ts index e3ba51a74a..d2ae5c9ce1 100644 --- a/modules/effects/spec/effects_feature_module.spec.ts +++ b/modules/effects/spec/effects_feature_module.spec.ts @@ -1,5 +1,6 @@ import { Injectable, NgModule } from '@angular/core'; import { TestBed } from '@angular/core/testing'; +import { combineLatest } from 'rxjs'; import { Action, createFeatureSelector, @@ -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: { @@ -42,6 +57,7 @@ describe('Effects Feature Module', () => { }); mockEffectSources = TestBed.get(EffectsRootModule); + mockStore = TestBed.get(Store); }); it('should add all effects when instantiated', () => { @@ -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; let store: Store; beforeEach(() => { @@ -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(); }); }); @@ -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, }; } @@ -112,11 +154,22 @@ function reducer(state: DataState = initialState, action: Action) { const getFeatureState = createFeatureSelector(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) {} + @Effect() + init = this.actions.pipe( + ofType(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)), diff --git a/modules/effects/src/effects_feature_module.ts b/modules/effects/src/effects_feature_module.ts index 3df5aa59d8..71ca6f63ab 100644 --- a/modules/effects/src/effects_feature_module.ts +++ b/modules/effects/src/effects_feature_module.ts @@ -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, @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({ + type: UPDATE_EFFECTS, + effects: effectSourceNames, + }); + }); } } diff --git a/modules/effects/src/index.ts b/modules/effects/src/index.ts index 606fb6657e..da9f9e4180 100644 --- a/modules/effects/src/index.ts +++ b/modules/effects/src/index.ts @@ -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';