From 5c87ddaabc19047c1e7fd2d2345d82f47f205bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Stanimirovi=C4=87?= Date: Fri, 9 Dec 2022 13:28:08 +0100 Subject: [PATCH] feat(store): support using`createSelector` with selectors dictionary (#3703) Closes #3677 --- modules/store/spec/selector.spec.ts | 31 +++++++++++++ modules/store/spec/types/selector.spec.ts | 22 ++++++++- modules/store/src/selector.ts | 46 ++++++++++++++++++- .../ngrx.io/content/guide/store/selectors.md | 13 +++++- 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/modules/store/spec/selector.spec.ts b/modules/store/spec/selector.spec.ts index b7d4cabc0c..38ed5324a8 100644 --- a/modules/store/spec/selector.spec.ts +++ b/modules/store/spec/selector.spec.ts @@ -168,6 +168,37 @@ describe('Selectors', () => { expect(grandparent.release).toHaveBeenCalled(); expect(parent.release).toHaveBeenCalled(); }); + + it('should create a selector from selectors dictionary', () => { + interface State { + x: number; + y: string; + } + + const selectX = (state: State) => state.x + 1; + const selectY = (state: State) => state.y; + + const selectDictionary = createSelector({ + s: selectX, + m: selectY, + }); + + expect(selectDictionary({ x: 1, y: 'ngrx' })).toEqual({ + s: 2, + m: 'ngrx', + }); + expect(selectDictionary({ x: 2, y: 'ngrx' })).toEqual({ + s: 3, + m: 'ngrx', + }); + }); + + it('should create a selector from empty dictionary', () => { + const selectDictionary = createSelector({}); + + expect(selectDictionary({ x: 1, y: 'ngrx' })).toEqual({}); + expect(selectDictionary({ x: 2, y: 'store' })).toEqual({}); + }); }); describe('createSelector with props', () => { diff --git a/modules/store/spec/types/selector.spec.ts b/modules/store/spec/types/selector.spec.ts index caf4457967..160520b440 100644 --- a/modules/store/spec/types/selector.spec.ts +++ b/modules/store/spec/types/selector.spec.ts @@ -4,7 +4,7 @@ import { compilerOptions } from './utils'; describe('createSelector()', () => { const expectSnippet = expecter( (code) => ` - import {createSelector} from '@ngrx/store'; + import { createSelector } from '@ngrx/store'; import { MemoizedSelector, DefaultProjectorFn } from '@ngrx/store'; ${code} @@ -38,12 +38,30 @@ describe('createSelector()', () => { `).toSucceed(); }); }); + + it('should create a selector from selectors dictionary', () => { + expectSnippet(` + const selectDictionary = createSelector({ + s: (state: { x: string }) => state.x, + m: (state: { y: number }) => state.y, + }); + `).toInfer( + 'selectDictionary', + 'MemoizedSelector<{ x: string; } & { y: number; }, { s: string; m: number; }, never>' + ); + }); + + it('should create a selector from empty dictionary', () => { + expectSnippet(` + const selectDictionary = createSelector({}); + `).toInfer('selectDictionary', 'MemoizedSelector'); + }); }); describe('createSelector() with props', () => { const expectSnippet = expecter( (code) => ` - import {createSelector} from '@ngrx/store'; + import { createSelector } from '@ngrx/store'; import { MemoizedSelectorWithProps, DefaultProjectorFn } from '@ngrx/store'; ${code} diff --git a/modules/store/src/selector.ts b/modules/store/src/selector.ts index 7275ed6190..7da40fdb63 100644 --- a/modules/store/src/selector.ts +++ b/modules/store/src/selector.ts @@ -193,6 +193,18 @@ export function createSelector( ) => Result ): MemoizedSelector; +export function createSelector< + Selectors extends Record>, + State = Selectors extends Record> + ? S + : never, + Result extends Record = { + [Key in keyof Selectors]: Selectors[Key] extends Selector + ? R + : never; + } +>(selectors: Selectors): MemoizedSelector; + export function createSelector( ...args: [...slices: Selector[], projector: unknown] & [ @@ -632,8 +644,6 @@ export function createSelectorFactory( * } * ); * ``` - * - * */ export function createSelectorFactory( memoize: MemoizeFn, @@ -648,6 +658,8 @@ export function createSelectorFactory( if (Array.isArray(args[0])) { const [head, ...tail] = args; args = [...head, ...tail]; + } else if (args.length === 1 && isSelectorsDictionary(args[0])) { + args = extractArgsFromSelectorsDictionary(args[0]); } const selectors = args.slice(0, args.length - 1); @@ -717,3 +729,33 @@ export function createFeatureSelector( (featureState: any) => featureState ); } + +function isSelectorsDictionary( + selectors: unknown +): selectors is Record> { + return ( + !!selectors && + typeof selectors === 'object' && + Object.values(selectors).every((selector) => typeof selector === 'function') + ); +} + +function extractArgsFromSelectorsDictionary( + selectorsDictionary: Record> +): [ + ...selectors: Selector[], + projector: (...selectorResults: unknown[]) => unknown +] { + const selectors = Object.values(selectorsDictionary); + const resultKeys = Object.keys(selectorsDictionary); + const projector = (...selectorResults: unknown[]) => + resultKeys.reduce( + (result, key, index) => ({ + ...result, + [key]: selectorResults[index], + }), + {} + ); + + return [...selectors, projector]; +} diff --git a/projects/ngrx.io/content/guide/store/selectors.md b/projects/ngrx.io/content/guide/store/selectors.md index de33737e26..bfdcc97ff9 100644 --- a/projects/ngrx.io/content/guide/store/selectors.md +++ b/projects/ngrx.io/content/guide/store/selectors.md @@ -80,6 +80,17 @@ export const selectVisibleBooks = createSelector( ); +The `createSelector` function also provides the ability to pass a dictionary of selectors without a projector. +In this case, `createSelector` will generate a projector function that maps the results of the input selectors to a dictionary. + +```ts +// result type - { books: Book[]; query: string } +const selectBooksPageViewModel = createSelector({ + books: selectBooks, // result type - Book[] + query: selectQuery, // result type - string +}); +``` + ### Using selectors with props
@@ -136,8 +147,6 @@ ngOnInit() { The `createFeatureSelector` is a convenience method for returning a top level feature state. It returns a typed selector function for a feature slice of state. -### Example - import { createSelector, createFeatureSelector } from '@ngrx/store';