Skip to content

Commit

Permalink
feat(Effects): add OnInitEffects interface to dispatch an action on i…
Browse files Browse the repository at this point in the history
…nitialization
  • Loading branch information
timdeschryver authored and brandonroberts committed Dec 20, 2018
1 parent e9cc9ae commit e921cd9
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 10 deletions.
28 changes: 26 additions & 2 deletions modules/effects/spec/effect_sources.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@ import { cold, getTestScheduler } from 'jasmine-marbles';
import { concat, NEVER, Observable, of, throwError, timer } from 'rxjs';
import { map } from 'rxjs/operators';

import { Effect, EffectSources, OnIdentifyEffects } from '../';
import { Effect, EffectSources, OnIdentifyEffects, OnInitEffects } from '../';
import { Store } from '@ngrx/store';

describe('EffectSources', () => {
let mockErrorReporter: ErrorHandler;
let effectSources: EffectSources;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [EffectSources],
providers: [
EffectSources,
{
provide: Store,
useValue: {
dispatch: jasmine.createSpy('dispatch'),
},
},
],
});

mockErrorReporter = TestBed.get(ErrorHandler);
Expand All @@ -30,6 +39,21 @@ describe('EffectSources', () => {
expect(effectSources.next).toHaveBeenCalledWith(effectSource);
});

it('should dispatch an action on ngrxOnInitEffects after being registered', () => {
class EffectWithInitAction implements OnInitEffects {
ngrxOnInitEffects() {
return { type: '[EffectWithInitAction] Init' };
}
}

effectSources.addEffects(new EffectWithInitAction());

const store = TestBed.get(Store);
expect(store.dispatch).toHaveBeenCalledWith({
type: '[EffectWithInitAction] Init',
});
});

describe('toActions() Operator', () => {
const a = { type: 'From Source A' };
const b = { type: 'From Source B' };
Expand Down
113 changes: 113 additions & 0 deletions modules/effects/spec/integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { TestBed } from '@angular/core/testing';
import { Store, Action } from '@ngrx/store';
import {
EffectsModule,
OnInitEffects,
ROOT_EFFECTS_INIT,
OnIdentifyEffects,
EffectSources,
} from '..';

describe('NgRx Effects Integration spec', () => {
let dispatch: jasmine.Spy;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
EffectsModule.forRoot([
RootEffectWithInitAction,
RootEffectWithoutLifecycle,
RootEffectWithInitActionWithPayload,
]),
EffectsModule.forFeature([FeatEffectWithInitAction]),
],
providers: [
{
provide: Store,
useValue: {
dispatch: jasmine.createSpy('dispatch'),
},
},
],
});

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

const effectSources = TestBed.get(EffectSources) as EffectSources;
effectSources.addEffects(new FeatEffectWithIdentifierAndInitAction('one'));
effectSources.addEffects(new FeatEffectWithIdentifierAndInitAction('two'));
effectSources.addEffects(new FeatEffectWithIdentifierAndInitAction('one'));

dispatch = store.dispatch as jasmine.Spy;
});

it('should dispatch init actions in the correct order', () => {
expect(dispatch.calls.count()).toBe(7);

// All of the root effects init actions are dispatched first
expect(dispatch.calls.argsFor(0)).toEqual([
{ type: '[RootEffectWithInitAction]: INIT' },
]);

expect(dispatch.calls.argsFor(1)).toEqual([new ActionWithPayload()]);

// After all of the root effects are registered, the ROOT_EFFECTS_INIT action is dispatched
expect(dispatch.calls.argsFor(2)).toEqual([{ type: ROOT_EFFECTS_INIT }]);

// After the root effects init, the feature effects are dispatched
expect(dispatch.calls.argsFor(3)).toEqual([
{ type: '[FeatEffectWithInitAction]: INIT' },
]);

expect(dispatch.calls.argsFor(4)).toEqual([
{ type: '[FeatEffectWithIdentifierAndInitAction]: INIT' },
]);

expect(dispatch.calls.argsFor(5)).toEqual([
{ type: '[FeatEffectWithIdentifierAndInitAction]: INIT' },
]);

// While the effect has the same identifier the init effect action is still being dispatched
expect(dispatch.calls.argsFor(6)).toEqual([
{ type: '[FeatEffectWithIdentifierAndInitAction]: INIT' },
]);
});

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

class ActionWithPayload implements Action {
readonly type = '[RootEffectWithInitActionWithPayload]: INIT';
readonly payload = 47;
}

class RootEffectWithInitActionWithPayload implements OnInitEffects {
ngrxOnInitEffects(): Action {
return new ActionWithPayload();
}
}

class RootEffectWithoutLifecycle {}

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

class FeatEffectWithIdentifierAndInitAction
implements OnInitEffects, OnIdentifyEffects {
ngrxOnIdentifyEffects(): string {
return this.effectIdentifier;
}

ngrxOnInitEffects(): Action {
return { type: '[FeatEffectWithIdentifierAndInitAction]: INIT' };
}

constructor(private effectIdentifier: string) {}
}
});
12 changes: 10 additions & 2 deletions modules/effects/src/effect_sources.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ErrorHandler, Injectable } from '@angular/core';
import { Action } from '@ngrx/store';
import { Action, Store } from '@ngrx/store';
import { Notification, Observable, Subject } from 'rxjs';
import {
dematerialize,
Expand All @@ -18,16 +18,24 @@ import {
onRunEffectsKey,
onRunEffectsFn,
OnRunEffects,
onInitEffects,
} from './lifecycle_hooks';

@Injectable()
export class EffectSources extends Subject<any> {
constructor(private errorHandler: ErrorHandler) {
constructor(private errorHandler: ErrorHandler, private store: Store<any>) {
super();
}

addEffects(effectSourceInstance: any) {
this.next(effectSourceInstance);

if (
onInitEffects in effectSourceInstance &&
typeof effectSourceInstance[onInitEffects] === 'function'
) {
this.store.dispatch(effectSourceInstance[onInitEffects]());
}
}

/**
Expand Down
6 changes: 5 additions & 1 deletion modules/effects/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ export { EffectsModule } from './effects_module';
export { EffectSources } from './effect_sources';
export { EffectNotification } from './effect_notification';
export { ROOT_EFFECTS_INIT } from './effects_root_module';
export { OnIdentifyEffects, OnRunEffects } from './lifecycle_hooks';
export {
OnIdentifyEffects,
OnRunEffects,
OnInitEffects,
} from './lifecycle_hooks';
40 changes: 35 additions & 5 deletions modules/effects/src/lifecycle_hooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Observable } from 'rxjs';
import { EffectNotification } from '.';
import { Action } from '@ngrx/store';

/**
* @description
Expand All @@ -19,21 +20,19 @@ import { EffectNotification } from '.';
* class EffectWithIdentifier implements OnIdentifyEffects {
* private effectIdentifier: string;
*
* ngrxOnIdentifyEffects () {
* ngrxOnIdentifyEffects() {
* return this.effectIdentifier;
* }
*
* constructor(identifier: string) {
* this.effectIdentifier = identifier;
* }
* constructor(private effectIdentifier: string) {}
* ```
*/
export interface OnIdentifyEffects {
/**
* @description
* String identifier to differentiate effect instances.
*/
ngrxOnIdentifyEffects: () => string;
ngrxOnIdentifyEffects(): string;
}

export const onIdentifyEffectsKey: keyof OnIdentifyEffects =
Expand Down Expand Up @@ -79,3 +78,34 @@ export interface OnRunEffects {
}

export const onRunEffectsKey: keyof OnRunEffects = 'ngrxOnRunEffects';

/**
* @description
* Interface to dispatch an action after effect registration.
*
* Implement this interface to dispatch a custom action after
* the effect has been added. You can listen to this action
* in the rest of the application to execute something after
* the effect is registered.
*
* @usageNotes
*
* ### Set an identifier for an Effects class
*
* ```ts
* class EffectWithInitAction implements OnInitEffects {
*
* ngrxOnInitEffects() {
* return { type: '[EffectWithInitAction] Init' };
* }
* ```
*/
export interface OnInitEffects {
/**
* @description
* Action to be dispatched after the effect is registered.
*/
ngrxOnInitEffects(): Action;
}

export const onInitEffects: keyof OnInitEffects = 'ngrxOnInitEffects';

0 comments on commit e921cd9

Please sign in to comment.