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 1 commit
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
118 changes: 113 additions & 5 deletions modules/effects/spec/integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { NgModuleFactoryLoader, NgModule } from '@angular/core';
import { NgModule, NgModuleFactoryLoader } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
RouterTestingModule,
SpyNgModuleFactoryLoader,
} from '@angular/router/testing';
import { Router } from '@angular/router';
import { Store, Action } from '@ngrx/store';
import { Action, Store } from '@ngrx/store';
import {
EffectsModule,
EffectSources,
OnIdentifyEffects,
OnInitEffects,
ROOT_EFFECTS_INIT,
OnIdentifyEffects,
EffectSources,
USER_PROVIDED_FEATURE_EFFECTS,
USER_PROVIDED_ROOT_EFFECTS,
} from '..';

describe('NgRx Effects Integration spec', () => {
Expand Down Expand Up @@ -90,6 +92,56 @@ describe('NgRx Effects Integration spec', () => {
});
});

it('should execute user provided root effects', (done: DoneFn) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of writing these tests, what about adding the providers in the should dispatch init actions in the correct order spec? This way, we also test the order of effects registration

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also create a simple unit test to verify the effect is added?
Something as:

describe('when registered with the USER_PROVIDED_FEATURE_EFFECTS token', () => {
    class SourceA {}
    const sourceA = new SourceA();

    let mockEffectSources: { addEffects: jasmine.Spy };

    beforeEach(() => {
      TestBed.configureTestingModule({
        imports: [EffectsModule.forFeature()],
        providers: [
          SourceA,
          {
            provide: EffectsRootModule,
            useValue: {
              addEffects: jasmine.createSpy('addEffects'),
            },
          },
          {
            provide: USER_PROVIDED_FEATURE_EFFECTS,
            useValue: [SourceA],
            multi: true,
          },
        ],
      });
      mockEffectSources = TestBed.get(EffectsRootModule);
    });

    it('should add effects when provided', () => {
      TestBed.get(EffectsFeatureModule);
      expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceA);
    });
  });

TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [EffectsModule.forRoot()],
providers: [
{
provide: Store,
useValue: {
dispatch: jasmine.createSpy('dispatch'),
},
},
RootUserProvidedEffect,
{
provide: USER_PROVIDED_ROOT_EFFECTS,
multi: true,
useValue: [RootUserProvidedEffect],
},
],
});

const store = TestBed.get(Store) as Store<any>;
dispatch = store.dispatch as jasmine.Spy;

expect(dispatch).toHaveBeenCalledWith({
type: '[RootUserProvidedEffect]: INIT',
});
done();
});

it('should execute user provided feature effects', (done: DoneFn) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

let router: Router = TestBed.get(Router);
const loader: SpyNgModuleFactoryLoader = TestBed.get(NgModuleFactoryLoader);

loader.stubbedModules = { feature: FeatModuleWithUserProvidedEffects };
router.resetConfig([{ path: 'feature-path', loadChildren: 'feature' }]);

router.navigateByUrl('/feature-path').then(() => {
expect(dispatch).toHaveBeenCalledWith({
type: '[FeatUserProvidedEffect]: INIT',
});
expect(dispatch).toHaveBeenCalledWith({
type: '[FeatUserProvidedEffect2]: INIT',
});
expect(dispatch).toHaveBeenCalledWith({
type: '[FeatUserProvidedEffect3]: INIT',
});
done();
});
});

class RootEffectWithInitAction implements OnInitEffects {
ngrxOnInitEffects(): Action {
return { type: '[RootEffectWithInitAction]: INIT' };
Expand All @@ -109,6 +161,24 @@ describe('NgRx Effects Integration spec', () => {

class RootEffectWithoutLifecycle {}

class RootUserProvidedEffect implements OnInitEffects {
public ngrxOnInitEffects(): Action {
return { type: '[RootUserProvidedEffect]: INIT' };
}
}

@NgModule({
imports: [EffectsModule.forRoot()],
providers: [
{
provide: USER_PROVIDED_ROOT_EFFECTS,
multi: true,
useValue: [RootUserProvidedEffect],
},
],
})
class RootModuleWithUserProvidedEffects {}

class FeatEffectWithInitAction implements OnInitEffects {
ngrxOnInitEffects(): Action {
return { type: '[FeatEffectWithInitAction]: INIT' };
Expand All @@ -128,8 +198,46 @@ describe('NgRx Effects Integration spec', () => {
constructor(private effectIdentifier: string) {}
}

class FeatUserProvidedEffect implements OnInitEffects {
public ngrxOnInitEffects(): Action {
return { type: '[FeatUserProvidedEffect]: INIT' };
}
}

class FeatUserProvidedEffect2 implements OnInitEffects {
public ngrxOnInitEffects(): Action {
return { type: '[FeatUserProvidedEffect2]: INIT' };
}
}

class FeatUserProvidedEffect3 implements OnInitEffects {
public ngrxOnInitEffects(): Action {
return { type: '[FeatUserProvidedEffect3]: INIT' };
}
}

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

@NgModule({
imports: [EffectsModule.forFeature()],
providers: [
FeatUserProvidedEffect,
FeatUserProvidedEffect2,
FeatUserProvidedEffect3,
{
provide: USER_PROVIDED_FEATURE_EFFECTS,
multi: true,
useValue: [FeatUserProvidedEffect, FeatUserProvidedEffect2],
},
{
provide: USER_PROVIDED_FEATURE_EFFECTS,
multi: true,
useValue: [FeatUserProvidedEffect3],
},
],
})
class FeatModuleWithUserProvidedEffects {}
});
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 @@ -7,6 +8,16 @@ import {
} from '@angular/core';
import { Actions } from './actions';
import { EffectSources } from './effect_sources';
import { Actions } from './actions';
leon-marzahn marked this conversation as resolved.
Show resolved Hide resolved
import {
_FEATURE_EFFECTS,
_ROOT_EFFECTS,
_ROOT_EFFECTS_GUARD,
FEATURE_EFFECTS,
ROOT_EFFECTS,
USER_PROVIDED_FEATURE_EFFECTS,
USER_PROVIDED_ROOT_EFFECTS,
} from './tokens';
import { EffectsFeatureModule } from './effects_feature_module';
import { defaultEffectsErrorHandler } from './effects_error_handler';
import { EffectsRootModule } from './effects_root_module';
Expand All @@ -21,24 +32,33 @@ import {
@NgModule({})
export class EffectsModule {
static forFeature(
featureEffects: Type<any>[]
featureEffects: Type<any>[] = []
): ModuleWithProviders<EffectsFeatureModule> {
return {
ngModule: EffectsFeatureModule,
providers: [
featureEffects,
{
provide: USER_PROVIDED_FEATURE_EFFECTS,
multi: true,
useValue: [],
},
{
provide: _FEATURE_EFFECTS,
useValue: featureEffects,
},
{
provide: FEATURE_EFFECTS,
multi: true,
deps: featureEffects,
useFactory: createSourceInstances,
useFactory: createEffects,
deps: [Injector, _FEATURE_EFFECTS, USER_PROVIDED_FEATURE_EFFECTS],
},
],
};
}

static forRoot(
rootEffects: Type<any>[]
rootEffects: Type<any>[] = []
): ModuleWithProviders<EffectsRootModule> {
return {
ngModule: EffectsRootModule,
Expand All @@ -56,18 +76,42 @@ export class EffectsModule {
EffectSources,
Actions,
rootEffects,
{
provide: USER_PROVIDED_ROOT_EFFECTS,
multi: true,
leon-marzahn marked this conversation as resolved.
Show resolved Hide resolved
useValue: [],
},
{
provide: _ROOT_EFFECTS,
useValue: rootEffects,
},
{
provide: ROOT_EFFECTS,
deps: rootEffects,
useFactory: createSourceInstances,
useFactory: createEffects,
deps: [Injector, _ROOT_EFFECTS, USER_PROVIDED_ROOT_EFFECTS],
},
],
};
}
}

export function createSourceInstances(...instances: any[]) {
return instances;
export function createEffects(
injector: Injector,
effects: Type<any>[],
userProvidedEffectGroups: Type<any>[][]
): any[] {
const mergedEffects: Type<any>[] = effects;
leon-marzahn marked this conversation as resolved.
Show resolved Hide resolved
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
4 changes: 4 additions & 0 deletions modules/effects/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ export {
OnRunEffects,
OnInitEffects,
} from './lifecycle_hooks';
export {
USER_PROVIDED_ROOT_EFFECTS,
USER_PROVIDED_FEATURE_EFFECTS,
} from './tokens';
12 changes: 12 additions & 0 deletions modules/effects/src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@ export const _ROOT_EFFECTS_GUARD = new InjectionToken<void>(
export const IMMEDIATE_EFFECTS = new InjectionToken<any[]>(
'ngrx/effects: Immediate Effects'
);
export const USER_PROVIDED_ROOT_EFFECTS = new InjectionToken<Type<any>[][]>(
'ngrx/effects: User Provided Root 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 USER_PROVIDED_FEATURE_EFFECTS = new InjectionToken<Type<any>[][]>(
'ngrx/effects: User Provided Feature 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
26 changes: 26 additions & 0 deletions projects/ngrx.io/content/guide/effects/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,16 @@ The `EffectsModule.forRoot()` method must be added to your `AppModule` imports e

Effects start running immediately after the AppModule is loaded to ensure they are listening for all relevant actions as soon as possible.

Alternatively, you can provide root-level effects with the provider `USER_PROVIDED_ROOT_EFFECTS`.

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

## Registering feature effects

For feature modules, register your effects by adding the `EffectsModule.forFeature()` method in the `imports` array of your `NgModule`.
Expand All @@ -225,6 +235,22 @@ export class MovieModule {}

</div>

Alternatively, you can provide feature-level effects with the provider `USER_PROVIDED_FEATURE_EFFECTS`.

<code-example header="admin.module.ts">
leon-marzahn marked this conversation as resolved.
Show resolved Hide resolved
{
provide: USER_PROVIDED_FEATURE_EFFECTS,
multi: true,
useValue: [MovieEffects],
}
</code-example>

<div class="alert is-critical">

The `EffectsModule.forChild()` method must be added to your `MovieModule` imports even if you only provide the effects over the token, and don't pass them via parameters.
leon-marzahn marked this conversation as resolved.
Show resolved Hide resolved

</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