Skip to content

Commit

Permalink
feat(store): support usingcreateSelector with selectors dictionary (#…
Browse files Browse the repository at this point in the history
…3703)

Closes #3677
  • Loading branch information
markostanimirovic authored Dec 9, 2022
1 parent 10f1146 commit 5c87dda
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 6 deletions.
31 changes: 31 additions & 0 deletions modules/store/spec/selector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
22 changes: 20 additions & 2 deletions modules/store/spec/types/selector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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<unknown, {}, never>');
});
});

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}
Expand Down
46 changes: 44 additions & 2 deletions modules/store/src/selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,18 @@ export function createSelector<State, S1, S2, S3, S4, S5, S6, S7, S8, Result>(
) => Result
): MemoizedSelector<State, Result, typeof projector>;

export function createSelector<
Selectors extends Record<string, Selector<State, unknown>>,
State = Selectors extends Record<string, Selector<infer S, unknown>>
? S
: never,
Result extends Record<string, unknown> = {
[Key in keyof Selectors]: Selectors[Key] extends Selector<State, infer R>
? R
: never;
}
>(selectors: Selectors): MemoizedSelector<State, Result, never>;

export function createSelector<State, Slices extends unknown[], Result>(
...args: [...slices: Selector<State, unknown>[], projector: unknown] &
[
Expand Down Expand Up @@ -632,8 +644,6 @@ export function createSelectorFactory<T = any, Props = any, V = any>(
* }
* );
* ```
*
*
*/
export function createSelectorFactory(
memoize: MemoizeFn,
Expand All @@ -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);
Expand Down Expand Up @@ -717,3 +729,33 @@ export function createFeatureSelector(
(featureState: any) => featureState
);
}

function isSelectorsDictionary(
selectors: unknown
): selectors is Record<string, Selector<unknown, unknown>> {
return (
!!selectors &&
typeof selectors === 'object' &&
Object.values(selectors).every((selector) => typeof selector === 'function')
);
}

function extractArgsFromSelectorsDictionary(
selectorsDictionary: Record<string, Selector<unknown, unknown>>
): [
...selectors: Selector<unknown, unknown>[],
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];
}
13 changes: 11 additions & 2 deletions projects/ngrx.io/content/guide/store/selectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ export const selectVisibleBooks = createSelector(
);
</code-example>

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

<div class="alert is-critical">
Expand Down Expand Up @@ -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

<code-example header="index.ts">
import { createSelector, createFeatureSelector } from '@ngrx/store';

Expand Down

0 comments on commit 5c87dda

Please sign in to comment.