Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(effects): add user provided effects to EffectsModule.forFeature #2231

Merged
merged 13 commits into from
Mar 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
leon-marzahn marked this conversation as resolved.
Show resolved Hide resolved
{
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,
timdeschryver marked this conversation as resolved.
Show resolved Hide resolved
{
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