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): allow ofType to handle ActionCreator #1676

Merged
merged 2 commits into from
Apr 1, 2019
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
214 changes: 197 additions & 17 deletions modules/effects/spec/actions.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Injector } from '@angular/core';
import {
Action,
StoreModule,
props,
ScannedActionsSubject,
ActionsSubject,
createAction,
} from '@ngrx/store';
import { Actions, ofType } from '../';
import { map, toArray, switchMap } from 'rxjs/operators';
Expand All @@ -25,16 +26,12 @@ describe('Actions', function() {
type: 'SUBTRACT';
}

function reducer(state: number = 0, action: Action) {
switch (action.type) {
case ADD:
return state + 1;
case SUBTRACT:
return state - 1;
default:
return state;
}
}
const square = createAction('SQUARE');
const multiply = createAction('MULTYPLY', props<{ by: number }>());
const divide = createAction('DIVIDE', props<{ by: number }>());

// Class-based Action types
const actions = [ADD, ADD, SUBTRACT, ADD, SUBTRACT];

beforeEach(function() {
const injector = Injector.create([
Expand Down Expand Up @@ -69,12 +66,12 @@ describe('Actions', function() {
});

actions.forEach(action => dispatcher.next(action));
dispatcher.complete();
});

const actions = [ADD, ADD, SUBTRACT, ADD, SUBTRACT];
const expected = actions.filter(type => type === ADD);
it('should filter out actions', () => {
const expected = actions.filter(type => type === ADD);

it('should let you filter out actions', function() {
actions$
.pipe(
ofType(ADD),
Expand All @@ -83,15 +80,17 @@ describe('Actions', function() {
)
.subscribe({
next(actual) {
expect(actual).toEqual(expected as any[]);
expect(actual).toEqual(expected);
},
});

actions.forEach(action => dispatcher.next({ type: action }));
dispatcher.complete();
});

it('should let you filter out actions and ofType can take an explicit type argument', function() {
it('should filter out actions and ofType can take an explicit type argument', () => {
const expected = actions.filter(type => type === ADD);

actions$
.pipe(
ofType<AddAction>(ADD),
Expand All @@ -100,11 +99,192 @@ describe('Actions', function() {
)
.subscribe({
next(actual) {
expect(actual).toEqual(expected as any[]);
expect(actual).toEqual(expected);
},
});

actions.forEach(action => dispatcher.next({ type: action }));
dispatcher.complete();
});

it('should let you filter out multiple action types with explicit type argument', () => {
const expected = actions.filter(type => type === ADD || type === SUBTRACT);

actions$
.pipe(
ofType<AddAction | SubtractAction>(ADD, SUBTRACT),
map(update => update.type),
toArray()
)
.subscribe({
next(actual) {
expect(actual).toEqual(expected);
},
});

actions.forEach(action => dispatcher.next({ type: action }));
dispatcher.complete();
});

it('should filter out actions by action creator', () => {
actions$
.pipe(
ofType(square),
map(update => update.type),
toArray()
)
.subscribe({
next(actual) {
expect(actual).toEqual(['SQUARE']);
},
});

[...actions, square.type].forEach(action =>
dispatcher.next({ type: action })
);
dispatcher.complete();
});

it('should infer the type for the action when it is filter by action creator with property', () => {
const MULTYPLY_BY = 5;

actions$
.pipe(
ofType(multiply),
map(update => update.by),
toArray()
)
.subscribe({
next(actual) {
expect(actual).toEqual([MULTYPLY_BY]);
},
});

// Unrelated Actions
actions.forEach(action => dispatcher.next({ type: action }));
// Action under test
dispatcher.next(multiply({ by: MULTYPLY_BY }));
dispatcher.complete();
});

it('should infer the type for the action when it is filter by action creator', () => {
// Types are not provided for generic Actions
const untypedActions$: Actions = actions$;
const MULTYPLY_BY = 5;

untypedActions$
.pipe(
ofType(multiply),
// Type is infered, even though untypedActions$ is Actions<Action>
map(update => update.by),
toArray()
)
.subscribe({
next(actual) {
expect(actual).toEqual([MULTYPLY_BY]);
},
});

// Unrelated Actions
actions.forEach(action => dispatcher.next({ type: action }));
// Action under test
dispatcher.next(multiply({ by: MULTYPLY_BY }));
dispatcher.complete();
});

it('should filter out multiple actions by action creator', () => {
const DIVIDE_BY = 3;
const MULTYPLY_BY = 5;
const expected = [DIVIDE_BY, MULTYPLY_BY];

actions$
.pipe(
ofType(divide, multiply),
// Both have 'by' property
map(update => update.by),
toArray()
)
.subscribe({
next(actual) {
expect(actual).toEqual(expected);
},
});

// Unrelated Actions
actions.forEach(action => dispatcher.next({ type: action }));
// Actions under test, in specific order
dispatcher.next(divide({ by: DIVIDE_BY }));
dispatcher.next(divide({ by: MULTYPLY_BY }));
dispatcher.complete();
});

it('should filter out actions by action creator and type string', () => {
const expected = [...actions.filter(type => type === ADD), square.type];

actions$
.pipe(
ofType(ADD, square),
map(update => update.type),
toArray()
)
.subscribe({
next(actual) {
expect(actual).toEqual(expected);
},
});

[...actions, square.type].forEach(action =>
dispatcher.next({ type: action })
);

dispatcher.complete();
});

it('should filter out actions by action creator and type string, with explicit type argument', () => {
const expected = [...actions.filter(type => type === ADD), square.type];

actions$
.pipe(
// Provided type overrides any inference from arguments
ofType<AddAction | ReturnType<typeof square>>(ADD, square),
map(update => update.type),
toArray()
)
.subscribe({
next(actual) {
expect(actual).toEqual(expected);
},
});

[...actions, square.type].forEach(action =>
dispatcher.next({ type: action })
);

dispatcher.complete();
});

it('should filter out up to 5 actions with type inference', () => {
// Mixing all of them, up to 5
const expected = [divide.type, ADD, square.type, SUBTRACT, multiply.type];

actions$
.pipe(
ofType(divide, ADD, square, SUBTRACT, multiply),
map(update => update.type),
toArray()
)
.subscribe({
next(actual) {
expect(actual).toEqual(expected);
},
});

// Actions under test, in specific order
dispatcher.next(divide({ by: 1 }));
dispatcher.next({ type: ADD });
dispatcher.next(square());
dispatcher.next({ type: SUBTRACT });
dispatcher.next(multiply({ by: 2 }));
dispatcher.complete();
});
});
87 changes: 58 additions & 29 deletions modules/effects/src/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Inject, Injectable } from '@angular/core';
import { Action, ScannedActionsSubject } from '@ngrx/store';
import {
Action,
ActionCreator,
Creator,
ScannedActionsSubject,
} from '@ngrx/store';
import { Observable, OperatorFunction, Operator } from 'rxjs';
import { filter } from 'rxjs/operators';

Expand All @@ -21,6 +26,12 @@ export class Actions<V = Action> extends Observable<V> {
}
}

// Module-private helper type
type ActionExtractor<
T extends string | AC,
AC extends ActionCreator<string, Creator>,
E
> = T extends string ? E : ReturnType<Extract<T, AC>>;
/**
* 'ofType' filters an Observable of Actions into an observable of the actions
* whose type strings are passed to it.
Expand All @@ -44,39 +55,49 @@ export class Actions<V = Action> extends Observable<V> {
* like `actions.ofType<AdditionAction>('add')`.
*/
export function ofType<
V extends Extract<U, { type: T1 }>,
T1 extends string = string,
U extends Action = Action
E extends Extract<U, { type: T1 }>,
AC extends ActionCreator<string, Creator>,
T1 extends string | AC,
U extends Action = Action,
V = T1 extends string ? E : ReturnType<Extract<T1, AC>>
>(t1: T1): OperatorFunction<U, V>;
export function ofType<
V extends Extract<U, { type: T1 | T2 }>,
T1 extends string = string,
T2 extends string = string,
U extends Action = Action
E extends Extract<U, { type: T1 | T2 }>,
AC extends ActionCreator<string, Creator>,
T1 extends string | AC,
T2 extends string | AC,
U extends Action = Action,
V = ActionExtractor<T1 | T2, AC, E>
>(t1: T1, t2: T2): OperatorFunction<U, V>;
export function ofType<
V extends Extract<U, { type: T1 | T2 | T3 }>,
T1 extends string = string,
T2 extends string = string,
T3 extends string = string,
U extends Action = Action
E extends Extract<U, { type: T1 | T2 | T3 }>,
AC extends ActionCreator<string, Creator>,
T1 extends string | AC,
T2 extends string | AC,
T3 extends string | AC,
U extends Action = Action,
V = ActionExtractor<T1 | T2 | T3, AC, E>
>(t1: T1, t2: T2, t3: T3): OperatorFunction<U, V>;
export function ofType<
V extends Extract<U, { type: T1 | T2 | T3 | T4 }>,
T1 extends string = string,
T2 extends string = string,
T3 extends string = string,
T4 extends string = string,
U extends Action = Action
E extends Extract<U, { type: T1 | T2 | T3 | T4 }>,
AC extends ActionCreator<string, Creator>,
T1 extends string | AC,
T2 extends string | AC,
T3 extends string | AC,
T4 extends string | AC,
U extends Action = Action,
V = ActionExtractor<T1 | T2 | T3 | T4, AC, E>
>(t1: T1, t2: T2, t3: T3, t4: T4): OperatorFunction<U, V>;
export function ofType<
V extends Extract<U, { type: T1 | T2 | T3 | T4 | T5 }>,
T1 extends string = string,
T2 extends string = string,
T3 extends string = string,
T4 extends string = string,
T5 extends string = string,
U extends Action = Action
E extends Extract<U, { type: T1 | T2 | T3 | T4 | T5 }>,
AC extends ActionCreator<string, Creator>,
T1 extends string | AC,
T2 extends string | AC,
T3 extends string | AC,
T4 extends string | AC,
T5 extends string | AC,
U extends Action = Action,
V = ActionExtractor<T1 | T2 | T3 | T4 | T5, AC, E>
>(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5): OperatorFunction<U, V>;
/**
* Fallback for more than 5 arguments.
Expand All @@ -87,12 +108,20 @@ export function ofType<
* arguments, to preserve backwards compatibility with old versions of ngrx.
*/
export function ofType<V extends Action>(
...allowedTypes: string[]
...allowedTypes: Array<string | ActionCreator<string, Creator>>
): OperatorFunction<Action, V>;
export function ofType(
...allowedTypes: string[]
...allowedTypes: Array<string | ActionCreator<string, Creator>>
): OperatorFunction<Action, Action> {
return filter((action: Action) =>
allowedTypes.some(type => type === action.type)
allowedTypes.some(typeOrActionCreator => {
if (typeof typeOrActionCreator === 'string') {
// Comparing the string to type
return typeOrActionCreator === action.type;
}

// We are filtering by ActionCreator
return typeOrActionCreator.type === action.type;
})
);
}