From 59ce3e276957df6c61e98811e8e3b0373ea34695 Mon Sep 17 00:00:00 2001 From: Leon Marzahn Date: Tue, 17 Mar 2020 12:58:11 +0100 Subject: [PATCH] feat(effects): add user provided effects to EffectsModule.forFeature (#2231) Closes #2232 --- modules/effects/spec/integration.spec.ts | 82 ++++++++++++++++++- modules/effects/src/effects_module.ts | 60 ++++++++++++-- modules/effects/src/index.ts | 1 + modules/effects/src/tokens.ts | 9 ++ .../ngrx.io/content/guide/effects/index.md | 21 +++++ 5 files changed, 164 insertions(+), 9 deletions(-) diff --git a/modules/effects/spec/integration.spec.ts b/modules/effects/spec/integration.spec.ts index e33220d2db..b5bd2aca74 100644 --- a/modules/effects/spec/integration.spec.ts +++ b/modules/effects/spec/integration.spec.ts @@ -13,6 +13,7 @@ import { OnIdentifyEffects, EffectSources, Actions, + USER_PROVIDED_EFFECTS, } from '..'; import { ofType, createEffect, OnRunEffects, EffectNotification } from '../src'; import { mapTo, exhaustMap, tap } from 'rxjs/operators'; @@ -215,6 +216,60 @@ describe('NgRx Effects Integration spec', () => { // ngrxOnRunEffects should receive all actions except STORE_INIT expect(logger.actionsLog).toEqual(expectedLog.slice(1)); }); + + it('should dispatch user provided effects actions in order', async () => { + let dispatchedActionsLog: string[] = []; + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({ + dispatched: createDispatchedReducer(dispatchedActionsLog), + }), + EffectsModule.forRoot([ + EffectLoggerWithOnRunEffects, + RootEffectWithInitAction, + ]), + RouterTestingModule.withRoutes([]), + ], + providers: [ + UserProvidedEffect1, + { + provide: USER_PROVIDED_EFFECTS, + multi: true, + useValue: [UserProvidedEffect1], + }, + ], + }); + + const logger = TestBed.inject(EffectLoggerWithOnRunEffects); + const router: Router = TestBed.inject(Router); + const loader: SpyNgModuleFactoryLoader = TestBed.inject( + NgModuleFactoryLoader + ) as SpyNgModuleFactoryLoader; + + loader.stubbedModules = { feature: FeatModuleWithUserProvidedEffects }; + router.resetConfig([{ path: 'feature-path', loadChildren: 'feature' }]); + + await router.navigateByUrl('/feature-path'); + + const expectedLog = [ + // Store init + INIT, + + // Root effects + '[RootEffectWithInitAction]: INIT', + + // User provided effects loaded by root module + '[UserProvidedEffect1]: INIT', + + // Effects init + ROOT_EFFECTS_INIT, + + // User provided effects loaded by feature module + '[UserProvidedEffect2]: INIT', + ]; + expect(dispatchedActionsLog).toEqual(expectedLog); + }); }); @Injectable() @@ -281,6 +336,31 @@ describe('NgRx Effects Integration spec', () => { class RootEffectWithoutLifecycle {} + class UserProvidedEffect1 implements OnInitEffects { + public ngrxOnInitEffects(): Action { + return { type: '[UserProvidedEffect1]: INIT' }; + } + } + + class UserProvidedEffect2 implements OnInitEffects { + public ngrxOnInitEffects(): Action { + return { type: '[UserProvidedEffect2]: INIT' }; + } + } + + @NgModule({ + imports: [EffectsModule.forFeature()], + providers: [ + UserProvidedEffect2, + { + provide: USER_PROVIDED_EFFECTS, + multi: true, + useValue: [UserProvidedEffect2], + }, + ], + }) + class FeatModuleWithUserProvidedEffects {} + class FeatEffectWithInitAction implements OnInitEffects { ngrxOnInitEffects(): Action { return { type: '[FeatEffectWithInitAction]: INIT' }; @@ -307,7 +387,7 @@ describe('NgRx Effects Integration spec', () => { } @NgModule({ - imports: [EffectsModule.forRoot([])], + imports: [EffectsModule.forRoot()], }) class FeatModuleWithForRoot {} diff --git a/modules/effects/src/effects_module.ts b/modules/effects/src/effects_module.ts index 26442a24c1..4ff5008202 100644 --- a/modules/effects/src/effects_module.ts +++ b/modules/effects/src/effects_module.ts @@ -1,4 +1,5 @@ import { + Injector, ModuleWithProviders, NgModule, Optional, @@ -12,33 +13,46 @@ import { defaultEffectsErrorHandler } from './effects_error_handler'; import { EffectsRootModule } from './effects_root_module'; import { EffectsRunner } from './effects_runner'; import { + _FEATURE_EFFECTS, + _ROOT_EFFECTS, _ROOT_EFFECTS_GUARD, EFFECTS_ERROR_HANDLER, FEATURE_EFFECTS, ROOT_EFFECTS, + USER_PROVIDED_EFFECTS, } from './tokens'; @NgModule({}) export class EffectsModule { static forFeature( - featureEffects: Type[] + featureEffects: Type[] = [] ): ModuleWithProviders { return { ngModule: EffectsFeatureModule, providers: [ featureEffects, + { + provide: _FEATURE_EFFECTS, + multi: true, + useValue: featureEffects, + }, + { + provide: USER_PROVIDED_EFFECTS, + multi: true, + useValue: [], + }, { provide: FEATURE_EFFECTS, multi: true, - deps: featureEffects, - useFactory: createSourceInstances, + useFactory: createEffects, + deps: [Injector, _FEATURE_EFFECTS, USER_PROVIDED_EFFECTS], }, ], }; } static forRoot( - rootEffects: Type[] + rootEffects: Type[] = [] ): ModuleWithProviders { return { ngModule: EffectsRootModule, @@ -56,18 +70,48 @@ export class EffectsModule { EffectSources, Actions, rootEffects, + { + provide: _ROOT_EFFECTS, + useValue: [rootEffects], + }, + { + provide: USER_PROVIDED_EFFECTS, + multi: true, + useValue: [], + }, { provide: ROOT_EFFECTS, - deps: rootEffects, - useFactory: createSourceInstances, + useFactory: createEffects, + deps: [Injector, _ROOT_EFFECTS, USER_PROVIDED_EFFECTS], }, ], }; } } -export function createSourceInstances(...instances: any[]) { - return instances; +export function createEffects( + injector: Injector, + effectGroups: Type[][], + userProvidedEffectGroups: Type[][] +): any[] { + const mergedEffects: Type[] = []; + + for (let effectGroup of effectGroups) { + mergedEffects.push(...effectGroup); + } + + for (let userProvidedEffectGroup of userProvidedEffectGroups) { + mergedEffects.push(...userProvidedEffectGroup); + } + + return createEffectInstances(injector, mergedEffects); +} + +export function createEffectInstances( + injector: Injector, + effects: Type[] +): any[] { + return effects.map(effect => injector.get(effect)); } export function _provideForRootGuard(runner: EffectsRunner): any { diff --git a/modules/effects/src/index.ts b/modules/effects/src/index.ts index 58e42b6879..00efcb88ab 100644 --- a/modules/effects/src/index.ts +++ b/modules/effects/src/index.ts @@ -26,3 +26,4 @@ export { OnRunEffects, OnInitEffects, } from './lifecycle_hooks'; +export { USER_PROVIDED_EFFECTS } from './tokens'; diff --git a/modules/effects/src/tokens.ts b/modules/effects/src/tokens.ts index 666c666334..517dcaa016 100644 --- a/modules/effects/src/tokens.ts +++ b/modules/effects/src/tokens.ts @@ -7,9 +7,18 @@ export const _ROOT_EFFECTS_GUARD = new InjectionToken( export const IMMEDIATE_EFFECTS = new InjectionToken( 'ngrx/effects: Immediate Effects' ); +export const USER_PROVIDED_EFFECTS = new InjectionToken[][]>( + 'ngrx/effects: User Provided Effects' +); +export const _ROOT_EFFECTS = new InjectionToken[]>( + 'ngrx/effects: Internal Root Effects' +); export const ROOT_EFFECTS = new InjectionToken[]>( 'ngrx/effects: Root Effects' ); +export const _FEATURE_EFFECTS = new InjectionToken[]>( + 'ngrx/effects: Internal Feature Effects' +); export const FEATURE_EFFECTS = new InjectionToken( 'ngrx/effects: Feature Effects' ); diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 377aa2f287..3efb60c11a 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -225,6 +225,27 @@ export class MovieModule {} +## Alternative way of registering effects + +You can provide root-/feature-level effects with the provider `USER_PROVIDED_EFFECTS`. + + +providers: [ + MovieEffects, + { + provide: USER_PROVIDED_EFFECTS, + multi: true, + useValue: [MovieEffects], + }, +] + + +
+ +The `EffectsModule.forFeature()` method must be added to the module imports even if you only provide effects over token, and don't pass them via parameters. (Same goes for `EffectsModule.forRoot()`) + +
+ ## Incorporating State If additional metadata is needed to perform an effect besides the initiating action's `type`, we should rely on passed metadata from an action creator's `props` method.