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): dispatch init feature effects action on init #1305

Merged
merged 4 commits into from
Aug 31, 2018
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
27 changes: 26 additions & 1 deletion docs/effects/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ You can see this action as a lifecycle hook, which you can use in order to execu
@Effect()
init$ = this.actions$.pipe(
ofType(ROOT_EFFECTS_INIT),
map(_ => ...)
map(action => ...)
);
```

Expand All @@ -45,6 +45,31 @@ Usage:
export class FeatureModule {}
```

### UPDATE_EFFECTS

After feature effects are registered, an `UPDATE_EFFECTS` action is dispatched.

```ts
type UpdateEffects = {
type: typeof UPDATE_EFFECTS;
effects: string[];
};
```

For example, when you register your feature module as `EffectsModule.forFeature([SomeEffectsClass, AnotherEffectsClass])`,
it has `SomeEffectsClass` and `AnotherEffectsClass` in an array as its payload.

To dispatch an action when the `SomeEffectsClass` effect has been registered, listen to the `UPDATE_EFFECTS` action and use the `effects` payload to filter out non-important effects.

```ts
@Effect()
init = this.actions.pipe(
ofType<UpdateEffects>(UPDATE_EFFECTS)
filter(action => action.effects.includes('SomeEffectsClass')),
map(action => ...)
);
```

## Actions

Stream of all actions dispatched in your application including actions dispatched by effect streams.
Expand Down
77 changes: 65 additions & 12 deletions modules/effects/spec/effects_feature_module.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable, NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { combineLatest } from 'rxjs';
import {
Action,
createFeatureSelector,
Expand All @@ -8,25 +9,39 @@ import {
Store,
StoreModule,
} from '@ngrx/store';
import { Observable } from 'rxjs';
import { map, withLatestFrom } from 'rxjs/operators';

import { Actions, Effect, EffectsModule } from '../';
import { EffectsFeatureModule } from '../src/effects_feature_module';
import { map, withLatestFrom, filter } from 'rxjs/operators';
import { Actions, Effect, EffectsModule, ofType } from '../';
import {
EffectsFeatureModule,
UPDATE_EFFECTS,
UpdateEffects,
} from '../src/effects_feature_module';
import { EffectsRootModule } from '../src/effects_root_module';
import { FEATURE_EFFECTS } from '../src/tokens';

describe('Effects Feature Module', () => {
describe('when registered', () => {
const sourceA = 'sourceA';
const sourceB = 'sourceB';
const sourceC = 'sourceC';
const effectSourceGroups = [[sourceA], [sourceB], [sourceC]];
class SourceA {}
class SourceB {}
class SourceC {}

const sourceA = new SourceA();
const sourceB = new SourceB();
const sourceC = new SourceC();

const effectSourceGroups = [[sourceA], [sourceB, sourceC]];
let mockEffectSources: { addEffects: jasmine.Spy };
let mockStore: { dispatch: jasmine.Spy };

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: Store,
useValue: {
dispatch: jasmine.createSpy('dispatch'),
},
},
{
provide: EffectsRootModule,
useValue: {
Expand All @@ -42,6 +57,7 @@ describe('Effects Feature Module', () => {
});

mockEffectSources = TestBed.get(EffectsRootModule);
mockStore = TestBed.get(Store);
});

it('should add all effects when instantiated', () => {
Expand All @@ -51,11 +67,24 @@ describe('Effects Feature Module', () => {
expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceB);
expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceC);
});

it('should dispatch update-effects actions when instantiated', () => {
TestBed.get(EffectsFeatureModule);

expect(mockStore.dispatch).toHaveBeenCalledWith({
type: UPDATE_EFFECTS,
effects: ['SourceA'],
});

expect(mockStore.dispatch).toHaveBeenCalledWith({
type: UPDATE_EFFECTS,
effects: ['SourceB', 'SourceC'],
});
});
});

describe('when registered in a different NgModule from the feature state', () => {
let effects: FeatureEffects;
let actions$: Observable<any>;
let store: Store<any>;

beforeEach(() => {
Expand All @@ -77,8 +106,12 @@ describe('Effects Feature Module', () => {

store.dispatch(action);

store.pipe(select(getDataState)).subscribe(res => {
expect(res).toBe(110);
combineLatest(
store.pipe(select(getDataState)),
store.pipe(select(getInitialized))
).subscribe(([data, initialized]) => {
expect(data).toBe(110);
expect(initialized).toBe(true);
done();
});
});
Expand All @@ -93,16 +126,25 @@ interface State {

interface DataState {
data: number;
initialized: boolean;
}

const initialState: DataState = {
data: 100,
initialized: false,
};

function reducer(state: DataState = initialState, action: Action) {
switch (action.type) {
case 'INITIALIZE_FEATURE': {
return {
...state,
initialized: true,
};
}
case 'INCREASE':
return {
...state,
data: state.data + 10,
};
}
Expand All @@ -112,11 +154,22 @@ function reducer(state: DataState = initialState, action: Action) {
const getFeatureState = createFeatureSelector<DataState>(FEATURE_KEY);

const getDataState = createSelector(getFeatureState, state => state.data);
const getInitialized = createSelector(
getFeatureState,
state => state.initialized
);

@Injectable()
class FeatureEffects {
constructor(private actions: Actions, private store: Store<State>) {}

@Effect()
init = this.actions.pipe(
ofType<UpdateEffects>(UPDATE_EFFECTS),
filter(action => action.effects.includes('FeatureEffects')),
map(action => ({ type: 'INITIALIZE_FEATURE' }))
);

@Effect()
effectWithStore = this.actions.ofType('INCREMENT').pipe(
withLatestFrom(this.store.select(getDataState)),
Expand Down
32 changes: 25 additions & 7 deletions modules/effects/src/effects_feature_module.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
import { NgModule, Inject, Optional } from '@angular/core';
import { StoreRootModule, StoreFeatureModule } from '@ngrx/store';
import { StoreRootModule, StoreFeatureModule, Store } from '@ngrx/store';
import { EffectsRootModule } from './effects_root_module';
import { FEATURE_EFFECTS } from './tokens';
import { getSourceForInstance } from './effects_metadata';

export const UPDATE_EFFECTS = '@ngrx/effects/update-effects';
export type UpdateEffects = {
type: typeof UPDATE_EFFECTS;
effects: string[];
};

@NgModule({})
export class EffectsFeatureModule {
constructor(
private root: EffectsRootModule,
root: EffectsRootModule,
store: Store<any>,
@Inject(FEATURE_EFFECTS) effectSourceGroups: any[][],
@Optional() storeRootModule: StoreRootModule,
@Optional() storeFeatureModule: StoreFeatureModule
) {
effectSourceGroups.forEach(group =>
group.forEach(effectSourceInstance =>
root.addEffects(effectSourceInstance)
)
);
effectSourceGroups.forEach(group => {
let effectSourceNames: string[] = [];

group.forEach(effectSourceInstance => {
root.addEffects(effectSourceInstance);

const { constructor } = getSourceForInstance(effectSourceInstance);
effectSourceNames.push(constructor.name);
});

store.dispatch(<UpdateEffects>{
type: UPDATE_EFFECTS,
effects: effectSourceNames,
});
});
}
}
1 change: 1 addition & 0 deletions modules/effects/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { EffectSources } from './effect_sources';
export { OnRunEffects } from './on_run_effects';
export { EffectNotification } from './effect_notification';
export { ROOT_EFFECTS_INIT } from './effects_root_module';
export { UPDATE_EFFECTS, UpdateEffects } from './effects_feature_module';