Skip to content

Commit 53832a1

Browse files
timdeschryverbrandonroberts
authored andcommitted
feat(Store): createSelector allow props in selector
1 parent 22284ab commit 53832a1

File tree

5 files changed

+494
-20
lines changed

5 files changed

+494
-20
lines changed

modules/store/spec/integration.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
select,
66
Store,
77
StoreModule,
8+
createFeatureSelector,
9+
createSelector,
810
} from '@ngrx/store';
911
import { combineLatest } from 'rxjs';
1012
import { first } from 'rxjs/operators';
@@ -173,6 +175,42 @@ describe('ngRx Integration spec', () => {
173175

174176
expect(currentlyVisibleTodos.length).toBe(0);
175177
});
178+
179+
it('should use props to get a todo', () => {
180+
const getTodosState = createFeatureSelector<TodoAppSchema, Todo[]>(
181+
'todos'
182+
);
183+
const getTodos = createSelector(getTodosState, todos => todos);
184+
const getTodosById = createSelector(
185+
getTodos,
186+
(state: TodoAppSchema, id: number) => id,
187+
(todos, id) => todos.find(todo => todo.id === id)
188+
);
189+
190+
let testCase = 1;
191+
const todo$ = store.pipe(select(getTodosById, 2));
192+
todo$.subscribe(todo => {
193+
if (testCase === 1) {
194+
expect(todo).toEqual(undefined);
195+
} else if (testCase === 2) {
196+
expect(todo).toEqual({
197+
id: 2,
198+
text: 'second todo',
199+
completed: false,
200+
});
201+
} else if (testCase === 3) {
202+
expect(todo).toEqual({ id: 2, text: 'second todo', completed: true });
203+
}
204+
testCase++;
205+
});
206+
207+
store.dispatch({ type: ADD_TODO, payload: { text: 'first todo' } });
208+
store.dispatch({ type: ADD_TODO, payload: { text: 'second todo' } });
209+
store.dispatch({
210+
type: COMPLETE_TODO,
211+
payload: { id: 2 },
212+
});
213+
});
176214
});
177215

178216
describe('feature state', () => {

modules/store/spec/selector.spec.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,105 @@ describe('Selectors', () => {
125125
});
126126
});
127127

128+
describe('createSelector with props', () => {
129+
it('should deliver the value of selectors to the projection function', () => {
130+
const projectFn = jasmine.createSpy('projectionFn');
131+
132+
const selector = createSelector(
133+
incrementOne,
134+
incrementTwo,
135+
(state: any, props: any) => props.value,
136+
projectFn
137+
);
138+
139+
selector({}, { value: 47 });
140+
expect(projectFn).toHaveBeenCalledWith(countOne, countTwo, 47);
141+
});
142+
143+
it('should be possible to test a projector fn independent from the selectors it is composed of', () => {
144+
const projectFn = jasmine.createSpy('projectionFn');
145+
const selector = createSelector(
146+
incrementOne,
147+
incrementTwo,
148+
(state: any, props: any) => {
149+
fail(`Shouldn't be called`);
150+
return props.value;
151+
},
152+
projectFn
153+
);
154+
selector.projector('', '', 47);
155+
156+
expect(incrementOne).not.toHaveBeenCalled();
157+
expect(incrementTwo).not.toHaveBeenCalled();
158+
expect(projectFn).toHaveBeenCalledWith('', '', 47);
159+
});
160+
161+
it('should call the projector function when the state changes', () => {
162+
const projectFn = jasmine.createSpy('projectionFn');
163+
const selector = createSelector(
164+
incrementOne,
165+
(state: any, props: any) => props.value,
166+
projectFn
167+
);
168+
169+
const firstSate = { first: 'state' };
170+
const props = { foo: 'props' };
171+
selector(firstSate, props);
172+
selector(firstSate, props);
173+
expect(projectFn).toHaveBeenCalledTimes(1);
174+
175+
const secondState = { second: 'state' };
176+
selector(secondState, props);
177+
expect(projectFn).toHaveBeenCalledTimes(2);
178+
});
179+
180+
it('should memoize the function', () => {
181+
let counter = 0;
182+
183+
const firstState = { first: 'state' };
184+
const secondState = { second: 'state' };
185+
const props = { foo: 'props' };
186+
187+
const projectFn = jasmine.createSpy('projectionFn');
188+
const selector = createSelector(
189+
incrementOne,
190+
incrementTwo,
191+
(state: any, props: any) => {
192+
counter++;
193+
return props;
194+
},
195+
projectFn
196+
);
197+
198+
selector(firstState, props);
199+
selector(firstState, props);
200+
selector(firstState, props);
201+
selector(secondState, props);
202+
203+
expect(counter).toBe(2);
204+
expect(projectFn).toHaveBeenCalledTimes(2);
205+
});
206+
207+
it('should allow you to release memoized arguments', () => {
208+
const state = { first: 'state' };
209+
const props = { foo: 'props' };
210+
const projectFn = jasmine.createSpy('projectionFn');
211+
const selector = createSelector(
212+
incrementOne,
213+
(state: any, props: any) => props,
214+
projectFn
215+
);
216+
217+
selector(state, props);
218+
selector(state, props);
219+
selector.release();
220+
selector(state, props);
221+
selector(state, props);
222+
223+
expect(projectFn).toHaveBeenCalledTimes(2);
224+
});
225+
});
226+
128227
describe('createSelector with arrays', () => {
129228
it('should deliver the value of selectors to the projection function', () => {
130229
const projectFn = jasmine.createSpy('projectionFn');
@@ -211,6 +310,104 @@ describe('Selectors', () => {
211310
});
212311
});
213312

313+
describe('createSelector with arrays and props', () => {
314+
it('should deliver the value of selectors to the projection function', () => {
315+
const projectFn = jasmine.createSpy('projectionFn');
316+
const selector = createSelector(
317+
[incrementOne, incrementTwo, (state: any, props: any) => props.value],
318+
projectFn
319+
)({}, { value: 47 });
320+
321+
expect(projectFn).toHaveBeenCalledWith(countOne, countTwo, 47);
322+
});
323+
324+
it('should be possible to test a projector fn independent from the selectors it is composed of', () => {
325+
const projectFn = jasmine.createSpy('projectionFn');
326+
const selector = createSelector(
327+
[
328+
incrementOne,
329+
incrementTwo,
330+
(state: any, props: any) => {
331+
fail(`Shouldn't be called`);
332+
return props.value;
333+
},
334+
],
335+
projectFn
336+
);
337+
338+
selector.projector('', '', 47);
339+
340+
expect(incrementOne).not.toHaveBeenCalled();
341+
expect(incrementTwo).not.toHaveBeenCalled();
342+
expect(projectFn).toHaveBeenCalledWith('', '', 47);
343+
});
344+
345+
it('should call the projector function when the state changes', () => {
346+
const projectFn = jasmine.createSpy('projectionFn');
347+
const selector = createSelector(
348+
[incrementOne, (state: any, props: any) => props.value],
349+
projectFn
350+
);
351+
352+
const firstSate = { first: 'state' };
353+
const props = { foo: 'props' };
354+
selector(firstSate, props);
355+
selector(firstSate, props);
356+
expect(projectFn).toHaveBeenCalledTimes(1);
357+
358+
const secondState = { second: 'state' };
359+
selector(secondState, props);
360+
expect(projectFn).toHaveBeenCalledTimes(2);
361+
});
362+
363+
it('should memoize the function', () => {
364+
let counter = 0;
365+
366+
const firstState = { first: 'state' };
367+
const secondState = { second: 'state' };
368+
const props = { foo: 'props' };
369+
370+
const projectFn = jasmine.createSpy('projectionFn');
371+
const selector = createSelector(
372+
[
373+
incrementOne,
374+
incrementTwo,
375+
(state: any, props: any) => {
376+
counter++;
377+
return props;
378+
},
379+
],
380+
projectFn
381+
);
382+
383+
selector(firstState, props);
384+
selector(firstState, props);
385+
selector(firstState, props);
386+
selector(secondState, props);
387+
388+
expect(counter).toBe(2);
389+
expect(projectFn).toHaveBeenCalledTimes(2);
390+
});
391+
392+
it('should allow you to release memoized arguments', () => {
393+
const state = { first: 'state' };
394+
const props = { foo: 'props' };
395+
const projectFn = jasmine.createSpy('projectionFn');
396+
const selector = createSelector(
397+
[incrementOne, (state: any, props: any) => props],
398+
projectFn
399+
);
400+
401+
selector(state, props);
402+
selector(state, props);
403+
selector.release();
404+
selector(state, props);
405+
selector(state, props);
406+
407+
expect(projectFn).toHaveBeenCalledTimes(2);
408+
});
409+
});
410+
214411
describe('createFeatureSelector', () => {
215412
let featureName = '@ngrx/router-store';
216413
let featureSelector: (state: any) => number;

modules/store/src/models.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export interface StoreFeature<T, V extends Action = Action> {
3333
metaReducers?: MetaReducer<T, V>[];
3434
}
3535

36-
export interface Selector<T, V> {
37-
(state: T): V;
38-
}
36+
export type Selector<T, V> = (state: T) => V;
37+
38+
export type SelectorWithProps<State, Props, Result> = (
39+
state: State,
40+
props: Props
41+
) => Result;

0 commit comments

Comments
 (0)