Skip to content

Commit

Permalink
feat(effects): allow ofType to handle ActionCreator (#1676)
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-okrushko authored and brandonroberts committed Apr 1, 2019
1 parent 29e7885 commit a41d1d6
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 46 deletions.
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;
})
);
}

0 comments on commit a41d1d6

Please sign in to comment.