Skip to content

Commit

Permalink
feat(effects): add user provided effects to EffectsModule.forFeature (#…
Browse files Browse the repository at this point in the history
…2231)

Closes #2232
  • Loading branch information
Leon Marzahn committed Mar 17, 2020
1 parent 7598dc3 commit 59ce3e2
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 9 deletions.
82 changes: 81 additions & 1 deletion modules/effects/spec/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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' };
Expand All @@ -307,7 +387,7 @@ describe('NgRx Effects Integration spec', () => {
}

@NgModule({
imports: [EffectsModule.forRoot([])],
imports: [EffectsModule.forRoot()],
})
class FeatModuleWithForRoot {}

Expand Down
60 changes: 52 additions & 8 deletions modules/effects/src/effects_module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Injector,
ModuleWithProviders,
NgModule,
Optional,
Expand All @@ -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<any>[]
featureEffects: Type<any>[] = []
): ModuleWithProviders<EffectsFeatureModule> {
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<any>[]
rootEffects: Type<any>[] = []
): ModuleWithProviders<EffectsRootModule> {
return {
ngModule: EffectsRootModule,
Expand All @@ -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<any>[][],
userProvidedEffectGroups: Type<any>[][]
): any[] {
const mergedEffects: Type<any>[] = [];

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>[]
): any[] {
return effects.map(effect => injector.get(effect));
}

export function _provideForRootGuard(runner: EffectsRunner): any {
Expand Down
1 change: 1 addition & 0 deletions modules/effects/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export {
OnRunEffects,
OnInitEffects,
} from './lifecycle_hooks';
export { USER_PROVIDED_EFFECTS } from './tokens';
9 changes: 9 additions & 0 deletions modules/effects/src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@ export const _ROOT_EFFECTS_GUARD = new InjectionToken<void>(
export const IMMEDIATE_EFFECTS = new InjectionToken<any[]>(
'ngrx/effects: Immediate Effects'
);
export const USER_PROVIDED_EFFECTS = new InjectionToken<Type<any>[][]>(
'ngrx/effects: User Provided Effects'
);
export const _ROOT_EFFECTS = new InjectionToken<Type<any>[]>(
'ngrx/effects: Internal Root Effects'
);
export const ROOT_EFFECTS = new InjectionToken<Type<any>[]>(
'ngrx/effects: Root Effects'
);
export const _FEATURE_EFFECTS = new InjectionToken<Type<any>[]>(
'ngrx/effects: Internal Feature Effects'
);
export const FEATURE_EFFECTS = new InjectionToken<any[][]>(
'ngrx/effects: Feature Effects'
);
Expand Down
21 changes: 21 additions & 0 deletions projects/ngrx.io/content/guide/effects/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,27 @@ export class MovieModule {}

</div>

## Alternative way of registering effects

You can provide root-/feature-level effects with the provider `USER_PROVIDED_EFFECTS`.

<code-example header="movies.module.ts">
providers: [
MovieEffects,
{
provide: USER_PROVIDED_EFFECTS,
multi: true,
useValue: [MovieEffects],
},
]
</code-example>

<div class="alert is-critical">

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()`)

</div>

## 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.
Expand Down

0 comments on commit 59ce3e2

Please sign in to comment.