Skip to content

Commit a41d1d6

Browse files
alex-okrushkobrandonroberts
authored andcommitted
feat(effects): allow ofType to handle ActionCreator (#1676)
1 parent 29e7885 commit a41d1d6

File tree

2 files changed

+255
-46
lines changed

2 files changed

+255
-46
lines changed

modules/effects/spec/actions.spec.ts

Lines changed: 197 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Injector } from '@angular/core';
22
import {
33
Action,
4-
StoreModule,
4+
props,
55
ScannedActionsSubject,
66
ActionsSubject,
7+
createAction,
78
} from '@ngrx/store';
89
import { Actions, ofType } from '../';
910
import { map, toArray, switchMap } from 'rxjs/operators';
@@ -25,16 +26,12 @@ describe('Actions', function() {
2526
type: 'SUBTRACT';
2627
}
2728

28-
function reducer(state: number = 0, action: Action) {
29-
switch (action.type) {
30-
case ADD:
31-
return state + 1;
32-
case SUBTRACT:
33-
return state - 1;
34-
default:
35-
return state;
36-
}
37-
}
29+
const square = createAction('SQUARE');
30+
const multiply = createAction('MULTYPLY', props<{ by: number }>());
31+
const divide = createAction('DIVIDE', props<{ by: number }>());
32+
33+
// Class-based Action types
34+
const actions = [ADD, ADD, SUBTRACT, ADD, SUBTRACT];
3835

3936
beforeEach(function() {
4037
const injector = Injector.create([
@@ -69,12 +66,12 @@ describe('Actions', function() {
6966
});
7067

7168
actions.forEach(action => dispatcher.next(action));
69+
dispatcher.complete();
7270
});
7371

74-
const actions = [ADD, ADD, SUBTRACT, ADD, SUBTRACT];
75-
const expected = actions.filter(type => type === ADD);
72+
it('should filter out actions', () => {
73+
const expected = actions.filter(type => type === ADD);
7674

77-
it('should let you filter out actions', function() {
7875
actions$
7976
.pipe(
8077
ofType(ADD),
@@ -83,15 +80,17 @@ describe('Actions', function() {
8380
)
8481
.subscribe({
8582
next(actual) {
86-
expect(actual).toEqual(expected as any[]);
83+
expect(actual).toEqual(expected);
8784
},
8885
});
8986

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

94-
it('should let you filter out actions and ofType can take an explicit type argument', function() {
91+
it('should filter out actions and ofType can take an explicit type argument', () => {
92+
const expected = actions.filter(type => type === ADD);
93+
9594
actions$
9695
.pipe(
9796
ofType<AddAction>(ADD),
@@ -100,11 +99,192 @@ describe('Actions', function() {
10099
)
101100
.subscribe({
102101
next(actual) {
103-
expect(actual).toEqual(expected as any[]);
102+
expect(actual).toEqual(expected);
103+
},
104+
});
105+
106+
actions.forEach(action => dispatcher.next({ type: action }));
107+
dispatcher.complete();
108+
});
109+
110+
it('should let you filter out multiple action types with explicit type argument', () => {
111+
const expected = actions.filter(type => type === ADD || type === SUBTRACT);
112+
113+
actions$
114+
.pipe(
115+
ofType<AddAction | SubtractAction>(ADD, SUBTRACT),
116+
map(update => update.type),
117+
toArray()
118+
)
119+
.subscribe({
120+
next(actual) {
121+
expect(actual).toEqual(expected);
122+
},
123+
});
124+
125+
actions.forEach(action => dispatcher.next({ type: action }));
126+
dispatcher.complete();
127+
});
128+
129+
it('should filter out actions by action creator', () => {
130+
actions$
131+
.pipe(
132+
ofType(square),
133+
map(update => update.type),
134+
toArray()
135+
)
136+
.subscribe({
137+
next(actual) {
138+
expect(actual).toEqual(['SQUARE']);
139+
},
140+
});
141+
142+
[...actions, square.type].forEach(action =>
143+
dispatcher.next({ type: action })
144+
);
145+
dispatcher.complete();
146+
});
147+
148+
it('should infer the type for the action when it is filter by action creator with property', () => {
149+
const MULTYPLY_BY = 5;
150+
151+
actions$
152+
.pipe(
153+
ofType(multiply),
154+
map(update => update.by),
155+
toArray()
156+
)
157+
.subscribe({
158+
next(actual) {
159+
expect(actual).toEqual([MULTYPLY_BY]);
104160
},
105161
});
106162

163+
// Unrelated Actions
107164
actions.forEach(action => dispatcher.next({ type: action }));
165+
// Action under test
166+
dispatcher.next(multiply({ by: MULTYPLY_BY }));
167+
dispatcher.complete();
168+
});
169+
170+
it('should infer the type for the action when it is filter by action creator', () => {
171+
// Types are not provided for generic Actions
172+
const untypedActions$: Actions = actions$;
173+
const MULTYPLY_BY = 5;
174+
175+
untypedActions$
176+
.pipe(
177+
ofType(multiply),
178+
// Type is infered, even though untypedActions$ is Actions<Action>
179+
map(update => update.by),
180+
toArray()
181+
)
182+
.subscribe({
183+
next(actual) {
184+
expect(actual).toEqual([MULTYPLY_BY]);
185+
},
186+
});
187+
188+
// Unrelated Actions
189+
actions.forEach(action => dispatcher.next({ type: action }));
190+
// Action under test
191+
dispatcher.next(multiply({ by: MULTYPLY_BY }));
192+
dispatcher.complete();
193+
});
194+
195+
it('should filter out multiple actions by action creator', () => {
196+
const DIVIDE_BY = 3;
197+
const MULTYPLY_BY = 5;
198+
const expected = [DIVIDE_BY, MULTYPLY_BY];
199+
200+
actions$
201+
.pipe(
202+
ofType(divide, multiply),
203+
// Both have 'by' property
204+
map(update => update.by),
205+
toArray()
206+
)
207+
.subscribe({
208+
next(actual) {
209+
expect(actual).toEqual(expected);
210+
},
211+
});
212+
213+
// Unrelated Actions
214+
actions.forEach(action => dispatcher.next({ type: action }));
215+
// Actions under test, in specific order
216+
dispatcher.next(divide({ by: DIVIDE_BY }));
217+
dispatcher.next(divide({ by: MULTYPLY_BY }));
218+
dispatcher.complete();
219+
});
220+
221+
it('should filter out actions by action creator and type string', () => {
222+
const expected = [...actions.filter(type => type === ADD), square.type];
223+
224+
actions$
225+
.pipe(
226+
ofType(ADD, square),
227+
map(update => update.type),
228+
toArray()
229+
)
230+
.subscribe({
231+
next(actual) {
232+
expect(actual).toEqual(expected);
233+
},
234+
});
235+
236+
[...actions, square.type].forEach(action =>
237+
dispatcher.next({ type: action })
238+
);
239+
240+
dispatcher.complete();
241+
});
242+
243+
it('should filter out actions by action creator and type string, with explicit type argument', () => {
244+
const expected = [...actions.filter(type => type === ADD), square.type];
245+
246+
actions$
247+
.pipe(
248+
// Provided type overrides any inference from arguments
249+
ofType<AddAction | ReturnType<typeof square>>(ADD, square),
250+
map(update => update.type),
251+
toArray()
252+
)
253+
.subscribe({
254+
next(actual) {
255+
expect(actual).toEqual(expected);
256+
},
257+
});
258+
259+
[...actions, square.type].forEach(action =>
260+
dispatcher.next({ type: action })
261+
);
262+
263+
dispatcher.complete();
264+
});
265+
266+
it('should filter out up to 5 actions with type inference', () => {
267+
// Mixing all of them, up to 5
268+
const expected = [divide.type, ADD, square.type, SUBTRACT, multiply.type];
269+
270+
actions$
271+
.pipe(
272+
ofType(divide, ADD, square, SUBTRACT, multiply),
273+
map(update => update.type),
274+
toArray()
275+
)
276+
.subscribe({
277+
next(actual) {
278+
expect(actual).toEqual(expected);
279+
},
280+
});
281+
282+
// Actions under test, in specific order
283+
dispatcher.next(divide({ by: 1 }));
284+
dispatcher.next({ type: ADD });
285+
dispatcher.next(square());
286+
dispatcher.next({ type: SUBTRACT });
287+
dispatcher.next(multiply({ by: 2 }));
108288
dispatcher.complete();
109289
});
110290
});

modules/effects/src/actions.ts

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { Inject, Injectable } from '@angular/core';
2-
import { Action, ScannedActionsSubject } from '@ngrx/store';
2+
import {
3+
Action,
4+
ActionCreator,
5+
Creator,
6+
ScannedActionsSubject,
7+
} from '@ngrx/store';
38
import { Observable, OperatorFunction, Operator } from 'rxjs';
49
import { filter } from 'rxjs/operators';
510

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

29+
// Module-private helper type
30+
type ActionExtractor<
31+
T extends string | AC,
32+
AC extends ActionCreator<string, Creator>,
33+
E
34+
> = T extends string ? E : ReturnType<Extract<T, AC>>;
2435
/**
2536
* 'ofType' filters an Observable of Actions into an observable of the actions
2637
* whose type strings are passed to it.
@@ -44,39 +55,49 @@ export class Actions<V = Action> extends Observable<V> {
4455
* like `actions.ofType<AdditionAction>('add')`.
4556
*/
4657
export function ofType<
47-
V extends Extract<U, { type: T1 }>,
48-
T1 extends string = string,
49-
U extends Action = Action
58+
E extends Extract<U, { type: T1 }>,
59+
AC extends ActionCreator<string, Creator>,
60+
T1 extends string | AC,
61+
U extends Action = Action,
62+
V = T1 extends string ? E : ReturnType<Extract<T1, AC>>
5063
>(t1: T1): OperatorFunction<U, V>;
5164
export function ofType<
52-
V extends Extract<U, { type: T1 | T2 }>,
53-
T1 extends string = string,
54-
T2 extends string = string,
55-
U extends Action = Action
65+
E extends Extract<U, { type: T1 | T2 }>,
66+
AC extends ActionCreator<string, Creator>,
67+
T1 extends string | AC,
68+
T2 extends string | AC,
69+
U extends Action = Action,
70+
V = ActionExtractor<T1 | T2, AC, E>
5671
>(t1: T1, t2: T2): OperatorFunction<U, V>;
5772
export function ofType<
58-
V extends Extract<U, { type: T1 | T2 | T3 }>,
59-
T1 extends string = string,
60-
T2 extends string = string,
61-
T3 extends string = string,
62-
U extends Action = Action
73+
E extends Extract<U, { type: T1 | T2 | T3 }>,
74+
AC extends ActionCreator<string, Creator>,
75+
T1 extends string | AC,
76+
T2 extends string | AC,
77+
T3 extends string | AC,
78+
U extends Action = Action,
79+
V = ActionExtractor<T1 | T2 | T3, AC, E>
6380
>(t1: T1, t2: T2, t3: T3): OperatorFunction<U, V>;
6481
export function ofType<
65-
V extends Extract<U, { type: T1 | T2 | T3 | T4 }>,
66-
T1 extends string = string,
67-
T2 extends string = string,
68-
T3 extends string = string,
69-
T4 extends string = string,
70-
U extends Action = Action
82+
E extends Extract<U, { type: T1 | T2 | T3 | T4 }>,
83+
AC extends ActionCreator<string, Creator>,
84+
T1 extends string | AC,
85+
T2 extends string | AC,
86+
T3 extends string | AC,
87+
T4 extends string | AC,
88+
U extends Action = Action,
89+
V = ActionExtractor<T1 | T2 | T3 | T4, AC, E>
7190
>(t1: T1, t2: T2, t3: T3, t4: T4): OperatorFunction<U, V>;
7291
export function ofType<
73-
V extends Extract<U, { type: T1 | T2 | T3 | T4 | T5 }>,
74-
T1 extends string = string,
75-
T2 extends string = string,
76-
T3 extends string = string,
77-
T4 extends string = string,
78-
T5 extends string = string,
79-
U extends Action = Action
92+
E extends Extract<U, { type: T1 | T2 | T3 | T4 | T5 }>,
93+
AC extends ActionCreator<string, Creator>,
94+
T1 extends string | AC,
95+
T2 extends string | AC,
96+
T3 extends string | AC,
97+
T4 extends string | AC,
98+
T5 extends string | AC,
99+
U extends Action = Action,
100+
V = ActionExtractor<T1 | T2 | T3 | T4 | T5, AC, E>
80101
>(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5): OperatorFunction<U, V>;
81102
/**
82103
* Fallback for more than 5 arguments.
@@ -87,12 +108,20 @@ export function ofType<
87108
* arguments, to preserve backwards compatibility with old versions of ngrx.
88109
*/
89110
export function ofType<V extends Action>(
90-
...allowedTypes: string[]
111+
...allowedTypes: Array<string | ActionCreator<string, Creator>>
91112
): OperatorFunction<Action, V>;
92113
export function ofType(
93-
...allowedTypes: string[]
114+
...allowedTypes: Array<string | ActionCreator<string, Creator>>
94115
): OperatorFunction<Action, Action> {
95116
return filter((action: Action) =>
96-
allowedTypes.some(type => type === action.type)
117+
allowedTypes.some(typeOrActionCreator => {
118+
if (typeof typeOrActionCreator === 'string') {
119+
// Comparing the string to type
120+
return typeOrActionCreator === action.type;
121+
}
122+
123+
// We are filtering by ActionCreator
124+
return typeOrActionCreator.type === action.type;
125+
})
97126
);
98127
}

0 commit comments

Comments
 (0)