Skip to content

Commit e921cd9

Browse files
timdeschryverbrandonroberts
authored andcommitted
feat(Effects): add OnInitEffects interface to dispatch an action on initialization
1 parent e9cc9ae commit e921cd9

File tree

5 files changed

+189
-10
lines changed

5 files changed

+189
-10
lines changed

modules/effects/spec/effect_sources.spec.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@ import { cold, getTestScheduler } from 'jasmine-marbles';
44
import { concat, NEVER, Observable, of, throwError, timer } from 'rxjs';
55
import { map } from 'rxjs/operators';
66

7-
import { Effect, EffectSources, OnIdentifyEffects } from '../';
7+
import { Effect, EffectSources, OnIdentifyEffects, OnInitEffects } from '../';
8+
import { Store } from '@ngrx/store';
89

910
describe('EffectSources', () => {
1011
let mockErrorReporter: ErrorHandler;
1112
let effectSources: EffectSources;
1213

1314
beforeEach(() => {
1415
TestBed.configureTestingModule({
15-
providers: [EffectSources],
16+
providers: [
17+
EffectSources,
18+
{
19+
provide: Store,
20+
useValue: {
21+
dispatch: jasmine.createSpy('dispatch'),
22+
},
23+
},
24+
],
1625
});
1726

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

42+
it('should dispatch an action on ngrxOnInitEffects after being registered', () => {
43+
class EffectWithInitAction implements OnInitEffects {
44+
ngrxOnInitEffects() {
45+
return { type: '[EffectWithInitAction] Init' };
46+
}
47+
}
48+
49+
effectSources.addEffects(new EffectWithInitAction());
50+
51+
const store = TestBed.get(Store);
52+
expect(store.dispatch).toHaveBeenCalledWith({
53+
type: '[EffectWithInitAction] Init',
54+
});
55+
});
56+
3357
describe('toActions() Operator', () => {
3458
const a = { type: 'From Source A' };
3559
const b = { type: 'From Source B' };
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { Store, Action } from '@ngrx/store';
3+
import {
4+
EffectsModule,
5+
OnInitEffects,
6+
ROOT_EFFECTS_INIT,
7+
OnIdentifyEffects,
8+
EffectSources,
9+
} from '..';
10+
11+
describe('NgRx Effects Integration spec', () => {
12+
let dispatch: jasmine.Spy;
13+
14+
beforeEach(() => {
15+
TestBed.configureTestingModule({
16+
imports: [
17+
EffectsModule.forRoot([
18+
RootEffectWithInitAction,
19+
RootEffectWithoutLifecycle,
20+
RootEffectWithInitActionWithPayload,
21+
]),
22+
EffectsModule.forFeature([FeatEffectWithInitAction]),
23+
],
24+
providers: [
25+
{
26+
provide: Store,
27+
useValue: {
28+
dispatch: jasmine.createSpy('dispatch'),
29+
},
30+
},
31+
],
32+
});
33+
34+
const store = TestBed.get(Store) as Store<any>;
35+
36+
const effectSources = TestBed.get(EffectSources) as EffectSources;
37+
effectSources.addEffects(new FeatEffectWithIdentifierAndInitAction('one'));
38+
effectSources.addEffects(new FeatEffectWithIdentifierAndInitAction('two'));
39+
effectSources.addEffects(new FeatEffectWithIdentifierAndInitAction('one'));
40+
41+
dispatch = store.dispatch as jasmine.Spy;
42+
});
43+
44+
it('should dispatch init actions in the correct order', () => {
45+
expect(dispatch.calls.count()).toBe(7);
46+
47+
// All of the root effects init actions are dispatched first
48+
expect(dispatch.calls.argsFor(0)).toEqual([
49+
{ type: '[RootEffectWithInitAction]: INIT' },
50+
]);
51+
52+
expect(dispatch.calls.argsFor(1)).toEqual([new ActionWithPayload()]);
53+
54+
// After all of the root effects are registered, the ROOT_EFFECTS_INIT action is dispatched
55+
expect(dispatch.calls.argsFor(2)).toEqual([{ type: ROOT_EFFECTS_INIT }]);
56+
57+
// After the root effects init, the feature effects are dispatched
58+
expect(dispatch.calls.argsFor(3)).toEqual([
59+
{ type: '[FeatEffectWithInitAction]: INIT' },
60+
]);
61+
62+
expect(dispatch.calls.argsFor(4)).toEqual([
63+
{ type: '[FeatEffectWithIdentifierAndInitAction]: INIT' },
64+
]);
65+
66+
expect(dispatch.calls.argsFor(5)).toEqual([
67+
{ type: '[FeatEffectWithIdentifierAndInitAction]: INIT' },
68+
]);
69+
70+
// While the effect has the same identifier the init effect action is still being dispatched
71+
expect(dispatch.calls.argsFor(6)).toEqual([
72+
{ type: '[FeatEffectWithIdentifierAndInitAction]: INIT' },
73+
]);
74+
});
75+
76+
class RootEffectWithInitAction implements OnInitEffects {
77+
ngrxOnInitEffects(): Action {
78+
return { type: '[RootEffectWithInitAction]: INIT' };
79+
}
80+
}
81+
82+
class ActionWithPayload implements Action {
83+
readonly type = '[RootEffectWithInitActionWithPayload]: INIT';
84+
readonly payload = 47;
85+
}
86+
87+
class RootEffectWithInitActionWithPayload implements OnInitEffects {
88+
ngrxOnInitEffects(): Action {
89+
return new ActionWithPayload();
90+
}
91+
}
92+
93+
class RootEffectWithoutLifecycle {}
94+
95+
class FeatEffectWithInitAction implements OnInitEffects {
96+
ngrxOnInitEffects(): Action {
97+
return { type: '[FeatEffectWithInitAction]: INIT' };
98+
}
99+
}
100+
101+
class FeatEffectWithIdentifierAndInitAction
102+
implements OnInitEffects, OnIdentifyEffects {
103+
ngrxOnIdentifyEffects(): string {
104+
return this.effectIdentifier;
105+
}
106+
107+
ngrxOnInitEffects(): Action {
108+
return { type: '[FeatEffectWithIdentifierAndInitAction]: INIT' };
109+
}
110+
111+
constructor(private effectIdentifier: string) {}
112+
}
113+
});

modules/effects/src/effect_sources.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ErrorHandler, Injectable } from '@angular/core';
2-
import { Action } from '@ngrx/store';
2+
import { Action, Store } from '@ngrx/store';
33
import { Notification, Observable, Subject } from 'rxjs';
44
import {
55
dematerialize,
@@ -18,16 +18,24 @@ import {
1818
onRunEffectsKey,
1919
onRunEffectsFn,
2020
OnRunEffects,
21+
onInitEffects,
2122
} from './lifecycle_hooks';
2223

2324
@Injectable()
2425
export class EffectSources extends Subject<any> {
25-
constructor(private errorHandler: ErrorHandler) {
26+
constructor(private errorHandler: ErrorHandler, private store: Store<any>) {
2627
super();
2728
}
2829

2930
addEffects(effectSourceInstance: any) {
3031
this.next(effectSourceInstance);
32+
33+
if (
34+
onInitEffects in effectSourceInstance &&
35+
typeof effectSourceInstance[onInitEffects] === 'function'
36+
) {
37+
this.store.dispatch(effectSourceInstance[onInitEffects]());
38+
}
3139
}
3240

3341
/**

modules/effects/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ export { EffectsModule } from './effects_module';
99
export { EffectSources } from './effect_sources';
1010
export { EffectNotification } from './effect_notification';
1111
export { ROOT_EFFECTS_INIT } from './effects_root_module';
12-
export { OnIdentifyEffects, OnRunEffects } from './lifecycle_hooks';
12+
export {
13+
OnIdentifyEffects,
14+
OnRunEffects,
15+
OnInitEffects,
16+
} from './lifecycle_hooks';

modules/effects/src/lifecycle_hooks.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Observable } from 'rxjs';
22
import { EffectNotification } from '.';
3+
import { Action } from '@ngrx/store';
34

45
/**
56
* @description
@@ -19,21 +20,19 @@ import { EffectNotification } from '.';
1920
* class EffectWithIdentifier implements OnIdentifyEffects {
2021
* private effectIdentifier: string;
2122
*
22-
* ngrxOnIdentifyEffects () {
23+
* ngrxOnIdentifyEffects() {
2324
* return this.effectIdentifier;
2425
* }
2526
*
26-
* constructor(identifier: string) {
27-
* this.effectIdentifier = identifier;
28-
* }
27+
* constructor(private effectIdentifier: string) {}
2928
* ```
3029
*/
3130
export interface OnIdentifyEffects {
3231
/**
3332
* @description
3433
* String identifier to differentiate effect instances.
3534
*/
36-
ngrxOnIdentifyEffects: () => string;
35+
ngrxOnIdentifyEffects(): string;
3736
}
3837

3938
export const onIdentifyEffectsKey: keyof OnIdentifyEffects =
@@ -79,3 +78,34 @@ export interface OnRunEffects {
7978
}
8079

8180
export const onRunEffectsKey: keyof OnRunEffects = 'ngrxOnRunEffects';
81+
82+
/**
83+
* @description
84+
* Interface to dispatch an action after effect registration.
85+
*
86+
* Implement this interface to dispatch a custom action after
87+
* the effect has been added. You can listen to this action
88+
* in the rest of the application to execute something after
89+
* the effect is registered.
90+
*
91+
* @usageNotes
92+
*
93+
* ### Set an identifier for an Effects class
94+
*
95+
* ```ts
96+
* class EffectWithInitAction implements OnInitEffects {
97+
*
98+
* ngrxOnInitEffects() {
99+
* return { type: '[EffectWithInitAction] Init' };
100+
* }
101+
* ```
102+
*/
103+
export interface OnInitEffects {
104+
/**
105+
* @description
106+
* Action to be dispatched after the effect is registered.
107+
*/
108+
ngrxOnInitEffects(): Action;
109+
}
110+
111+
export const onInitEffects: keyof OnInitEffects = 'ngrxOnInitEffects';

0 commit comments

Comments
 (0)