From a3662b74ebd41ce445569f99872ce22417543d98 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Thu, 19 Jul 2018 18:39:38 +0200 Subject: [PATCH] feat: allow properties in selectors --- modules/store/spec/selector.spec.ts | 199 ++++++++++++++++++ modules/store/src/models.ts | 9 +- modules/store/src/selector.ts | 303 ++++++++++++++++++++++++---- 3 files changed, 471 insertions(+), 40 deletions(-) diff --git a/modules/store/spec/selector.spec.ts b/modules/store/spec/selector.spec.ts index 7793a92990..1dc564d893 100644 --- a/modules/store/spec/selector.spec.ts +++ b/modules/store/spec/selector.spec.ts @@ -123,6 +123,106 @@ describe('Selectors', () => { }); }); + describe('createSelector with props', () => { + it('should deliver the value of selectors to the projection function', () => { + const projectFn = jasmine.createSpy('projectionFn'); + + const selector = createSelector( + incrementOne, + incrementTwo, + (state: any, props: any) => props.value, + projectFn + ); + + selector({}, { value: 47 }); + expect(projectFn).toHaveBeenCalledWith(countOne, countTwo, 47); + }); + + it('should be possible to test a projector fn independent from the selectors it is composed of', () => { + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector( + incrementOne, + incrementTwo, + (state: any, props: any) => { + fail(`Shouldn't be called`); + return props.value; + }, + projectFn + ); + selector.projector('', '', 47); + + expect(incrementOne).not.toHaveBeenCalled(); + expect(incrementTwo).not.toHaveBeenCalled(); + expect(projectFn).toHaveBeenCalledWith('', '', 47); + }); + + it('should call the projector function when the property changes', () => { + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector( + incrementOne, + (state: any, props: any) => props.value, + projectFn + ); + + const state1 = { foo: 'bar' }; + const props1 = { foo: 'bar' }; + selector(state1, props1); + selector(state1, props1); + expect(projectFn).toHaveBeenCalledTimes(1); + + const props2 = { foo: 'bar2' }; + selector(state1, props2); + expect(projectFn).toHaveBeenCalledTimes(2); + }); + + it('should memoize the function', () => { + let counter = 0; + + const firstState = { first: 'state' }; + const firstProps = { foo: 'first' }; + const secondProps = { foo: 'second' }; + + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector( + incrementOne, + incrementTwo, + (state: any, props: any) => { + counter++; + return props; + }, + projectFn + ); + + selector(firstState, firstProps); + selector(firstState, firstProps); + selector(firstState, firstProps); + selector(firstState, secondProps); + selector(firstState, secondProps); + + expect(counter).toBe(2); + expect(projectFn).toHaveBeenCalledTimes(2); + }); + + it('should allow you to release memoized arguments', () => { + const state = { first: 'state' }; + const props = { first: 'props' }; + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector( + incrementOne, + (state: any, props: any) => props, + projectFn + ); + + selector(state, props); + selector(state, props); + selector.release(); + selector(state, props); + selector(state, props); + + expect(projectFn).toHaveBeenCalledTimes(2); + }); + }); + describe('createSelector with arrays', () => { it('should deliver the value of selectors to the projection function', () => { const projectFn = jasmine.createSpy('projectionFn'); @@ -209,6 +309,105 @@ describe('Selectors', () => { }); }); + describe('createSelector with arrays and props', () => { + it('should deliver the value of selectors to the projection function', () => { + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector( + [incrementOne, incrementTwo, (state: any, props: any) => props.value], + projectFn + )({}, { value: 47 }); + + expect(projectFn).toHaveBeenCalledWith(countOne, countTwo, 47); + }); + + it('should be possible to test a projector fn independent from the selectors it is composed of', () => { + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector( + [ + incrementOne, + incrementTwo, + (state: any, props: any) => { + fail(`Shouldn't be called`); + return props.value; + }, + ], + projectFn + ); + + selector.projector('', '', 47); + + expect(incrementOne).not.toHaveBeenCalled(); + expect(incrementTwo).not.toHaveBeenCalled(); + expect(projectFn).toHaveBeenCalledWith('', '', 47); + }); + + it('should call the projector function when the property changes', () => { + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector( + [incrementOne, (state: any, props: any) => props.value], + projectFn + ); + + const state1 = { foo: 'bar' }; + const props1 = { foo: 'bar' }; + selector(state1, props1); + selector(state1, props1); + expect(projectFn).toHaveBeenCalledTimes(1); + + const props2 = { foo: 'bar2' }; + selector(state1, props2); + expect(projectFn).toHaveBeenCalledTimes(2); + }); + + it('should memoize the function', () => { + let counter = 0; + + const firstState = { first: 'state' }; + const firstProps = { foo: 'first' }; + const secondProps = { foo: 'second' }; + + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector( + [ + incrementOne, + incrementTwo, + (state: any, props: any) => { + counter++; + return props; + }, + ], + projectFn + ); + + selector(firstState, firstProps); + selector(firstState, firstProps); + selector(firstState, firstProps); + selector(firstState, secondProps); + selector(firstState, secondProps); + + expect(counter).toBe(2); + expect(projectFn).toHaveBeenCalledTimes(2); + }); + + it('should allow you to release memoized arguments', () => { + const state = { first: 'state' }; + const props = { first: 'props' }; + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector( + [incrementOne, (state: any, props: any) => props], + projectFn + ); + + selector(state, props); + selector(state, props); + selector.release(); + selector(state, props); + selector(state, props); + + expect(projectFn).toHaveBeenCalledTimes(2); + }); + }); + describe('createFeatureSelector', () => { let featureName = '@ngrx/router-store'; let featureSelector: (state: any) => number; diff --git a/modules/store/src/models.ts b/modules/store/src/models.ts index ef6acfbd78..bc110257a5 100644 --- a/modules/store/src/models.ts +++ b/modules/store/src/models.ts @@ -33,6 +33,9 @@ export interface StoreFeature { metaReducers?: MetaReducer[]; } -export interface Selector { - (state: T): V; -} +export type Selector = (state: State) => Result; + +export type SelectorWithProperty = ( + state: State, + props: Property +) => Result; diff --git a/modules/store/src/selector.ts b/modules/store/src/selector.ts index 1acafe52e6..06a7c87d36 100644 --- a/modules/store/src/selector.ts +++ b/modules/store/src/selector.ts @@ -1,4 +1,4 @@ -import { Selector } from './models'; +import { Selector, SelectorWithProperty } from './models'; export type AnyFn = (...args: any[]) => any; @@ -12,6 +12,12 @@ export interface MemoizedSelector projector: AnyFn; } +export interface MemoizedSelectorWithProperty + extends SelectorWithProperty { + release(): void; + projector: AnyFn; +} + export function isEqualCheck(a: any, b: any): boolean { return a === b; } @@ -55,36 +61,80 @@ export function createSelector( s1: Selector, projector: (S1: S1) => Result ): MemoizedSelector; +export function createSelector( + s1: SelectorWithProperty, + projector: (S1: S1) => Result +): MemoizedSelectorWithProperty; export function createSelector( selectors: [Selector], - projector: (s1: S1) => Result + projector: (S1: S1) => Result ): MemoizedSelector; +export function createSelector( + selectors: [SelectorWithProperty], + projector: (S1: S1) => Result +): MemoizedSelectorWithProperty; + export function createSelector( s1: Selector, s2: Selector, - projector: (s1: S1, s2: S2) => Result + projector: (S1: S1, S2: S2) => Result ): MemoizedSelector; +export function createSelector( + s1: SelectorWithProperty, + s2: SelectorWithProperty, + projector: (S1: S1, S2: S2) => Result +): MemoizedSelectorWithProperty; export function createSelector( selectors: [Selector, Selector], - projector: (s1: S1, s2: S2) => Result + projector: (S1: S1, S2: S2) => Result ): MemoizedSelector; +export function createSelector( + selectors: [ + SelectorWithProperty, + SelectorWithProperty + ], + projector: (S1: S1, S2: S2) => Result +): MemoizedSelectorWithProperty; + export function createSelector( s1: Selector, s2: Selector, s3: Selector, - projector: (s1: S1, s2: S2, s3: S3) => Result + projector: (S1: S1, S2: S2, S3: S3) => Result ): MemoizedSelector; +export function createSelector( + s1: SelectorWithProperty, + s2: SelectorWithProperty, + s3: SelectorWithProperty, + projector: (S1: S1, S2: S2, S3: S3) => Result +): MemoizedSelectorWithProperty; export function createSelector( selectors: [Selector, Selector, Selector], - projector: (s1: S1, s2: S2, s3: S3) => Result + projector: (S1: S1, S2: S2, S3: S3) => Result ): MemoizedSelector; +export function createSelector( + selectors: [ + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty + ], + projector: (S1: S1, S2: S2, S3: S3) => Result +): MemoizedSelectorWithProperty; + export function createSelector( s1: Selector, s2: Selector, s3: Selector, s4: Selector, - projector: (s1: S1, s2: S2, s3: S3, s4: S4) => Result + projector: (S1: S1, S2: S2, S3: S3, S4: S4) => Result ): MemoizedSelector; +export function createSelector( + s1: SelectorWithProperty, + s2: SelectorWithProperty, + s3: SelectorWithProperty, + s4: SelectorWithProperty, + projector: (S1: S1, S2: S2, S3: S3, S4: S4) => Result +): MemoizedSelectorWithProperty; export function createSelector( selectors: [ Selector, @@ -92,16 +142,34 @@ export function createSelector( Selector, Selector ], - projector: (s1: S1, s2: S2, s3: S3, s4: S4) => Result + projector: (S1: S1, S2: S2, S3: S3, S4: S4) => Result ): MemoizedSelector; +export function createSelector( + selectors: [ + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty + ], + projector: (S1: S1, S2: S2, S3: S3, S4: S4) => Result +): MemoizedSelectorWithProperty; + export function createSelector( s1: Selector, s2: Selector, s3: Selector, s4: Selector, s5: Selector, - projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5) => Result + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5) => Result ): MemoizedSelector; +export function createSelector( + s1: SelectorWithProperty, + s2: SelectorWithProperty, + s3: SelectorWithProperty, + s4: SelectorWithProperty, + s5: SelectorWithProperty, + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5) => Result +): MemoizedSelectorWithProperty; export function createSelector( selectors: [ Selector, @@ -110,8 +178,19 @@ export function createSelector( Selector, Selector ], - projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5) => Result + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5) => Result ): MemoizedSelector; +export function createSelector( + selectors: [ + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty + ], + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5) => Result +): MemoizedSelectorWithProperty; + export function createSelector( s1: Selector, s2: Selector, @@ -119,8 +198,17 @@ export function createSelector( s4: Selector, s5: Selector, s6: Selector, - projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6) => Result + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5, S6: S6) => Result ): MemoizedSelector; +export function createSelector( + s1: SelectorWithProperty, + s2: SelectorWithProperty, + s3: SelectorWithProperty, + s4: SelectorWithProperty, + s5: SelectorWithProperty, + s6: SelectorWithProperty, + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5, S6: S6) => Result +): MemoizedSelectorWithProperty; export function createSelector( selectors: [ Selector, @@ -130,8 +218,20 @@ export function createSelector( Selector, Selector ], - projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6) => Result + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5, S6: S6) => Result ): MemoizedSelector; +export function createSelector( + selectors: [ + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty + ], + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5, S6: S6) => Result +): MemoizedSelectorWithProperty; + export function createSelector( s1: Selector, s2: Selector, @@ -140,8 +240,29 @@ export function createSelector( s5: Selector, s6: Selector, s7: Selector, - projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6, s7: S7) => Result + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5, S6: S6, S7: S7) => Result ): MemoizedSelector; +export function createSelector< + State, + Property, + S1, + S2, + S3, + S4, + S5, + S6, + S7, + Result +>( + s1: SelectorWithProperty, + s2: SelectorWithProperty, + s3: SelectorWithProperty, + s4: SelectorWithProperty, + s5: SelectorWithProperty, + s6: SelectorWithProperty, + s7: SelectorWithProperty, + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5, S6: S6, S7: S7) => Result +): MemoizedSelectorWithProperty; export function createSelector( selectors: [ Selector, @@ -152,8 +273,32 @@ export function createSelector( Selector, Selector ], - projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6, s7: S7) => Result + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5, S6: S6, S7: S7) => Result ): MemoizedSelector; +export function createSelector< + State, + Property, + S1, + S2, + S3, + S4, + S5, + S6, + S7, + Result +>( + selectors: [ + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty + ], + projector: (S1: S1, S2: S2, S3: S3, S4: S4, S5: S5, S6: S6, S7: S7) => Result +): MemoizedSelectorWithProperty; + export function createSelector( s1: Selector, s2: Selector, @@ -164,16 +309,48 @@ export function createSelector( s7: Selector, s8: Selector, projector: ( - s1: S1, - s2: S2, - s3: S3, - s4: S4, - s5: S5, - s6: S6, - s7: S7, - s8: S8 + S1: S1, + S2: S2, + S3: S3, + S4: S4, + S5: S5, + S6: S6, + S7: S7, + S8: S8 ) => Result ): MemoizedSelector; +export function createSelector< + State, + Property, + S1, + S2, + S3, + S4, + S5, + S6, + S7, + S8, + Result +>( + s1: SelectorWithProperty, + s2: SelectorWithProperty, + s3: SelectorWithProperty, + s4: SelectorWithProperty, + s5: SelectorWithProperty, + s6: SelectorWithProperty, + s7: SelectorWithProperty, + s8: SelectorWithProperty, + projector: ( + S1: S1, + S2: S2, + S3: S3, + S4: S4, + S5: S5, + S6: S6, + S7: S7, + S8: S8 + ) => Result +): MemoizedSelectorWithProperty; export function createSelector( selectors: [ Selector, @@ -186,33 +363,71 @@ export function createSelector( Selector ], projector: ( - s1: S1, - s2: S2, - s3: S3, - s4: S4, - s5: S5, - s6: S6, - s7: S7, - s8: S8 + S1: S1, + S2: S2, + S3: S3, + S4: S4, + S5: S5, + S6: S6, + S7: S7, + S8: S8 ) => Result ): MemoizedSelector; -export function createSelector(...input: any[]) { +export function createSelector< + State, + Property, + S1, + S2, + S3, + S4, + S5, + S6, + S7, + S8, + Result +>( + selectors: [ + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty, + SelectorWithProperty + ], + projector: ( + S1: S1, + S2: S2, + S3: S3, + S4: S4, + S5: S5, + S6: S6, + S7: S7, + S8: S8 + ) => Result +): MemoizedSelectorWithProperty; + +export function createSelector( + ...input: any[] +): Selector | SelectorWithProperty { return createSelectorFactory(defaultMemoize)(...input); } export function defaultStateFn( state: any, - selectors: Selector[], + props: any, + selectors: any[], memoizedProjector: MemoizedProjection ): any { - const args = selectors.map(fn => fn(state)); - + const args = selectors.map(fn => fn(state, props)); return memoizedProjector.memoized.apply(null, args); } export type SelectorFactoryConfig = { stateFn: ( state: T, + props: any, selectors: Selector[], memoizedProjector: MemoizedProjection ) => V; @@ -225,13 +440,22 @@ export function createSelectorFactory( memoize: MemoizeFn, options: SelectorFactoryConfig ): (...input: any[]) => Selector; +export function createSelectorFactory( + memoize: MemoizeFn +): (...input: any[]) => SelectorWithProperty; +export function createSelectorFactory( + memoize: MemoizeFn, + options: SelectorFactoryConfig +): (...input: any[]) => SelectorWithProperty; export function createSelectorFactory( memoize: MemoizeFn, options: SelectorFactoryConfig = { stateFn: defaultStateFn, } ) { - return function(...input: any[]): Selector { + return function( + ...input: any[] + ): Selector | SelectorWithProperty { let args = input; if (Array.isArray(args[0])) { const [head, ...tail] = args; @@ -249,8 +473,13 @@ export function createSelectorFactory( return projector.apply(null, selectors); }); - const memoizedState = defaultMemoize(function(state: any) { - return options.stateFn.apply(null, [state, selectors, memoizedProjector]); + const memoizedState = defaultMemoize(function(state: any, props: any) { + return options.stateFn.apply(null, [ + state, + props, + selectors, + memoizedProjector, + ]); }); function release() {