Skip to content
This repository was archived by the owner on Jul 8, 2021. It is now read-only.

Commit 723a500

Browse files
authored
Merge pull request #14 from albyrock87/memoized-selectors
Added support for Selectors
2 parents b40ebfe + 11f8852 commit 723a500

14 files changed

+161
-61
lines changed

package-lock.json

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "dist/index.js",
66
"scripts": {
77
"build": "ng-packagr -p package.json",
8-
"test": "ngc -p tsconfig.json && jasmine dist/*.spec.js",
8+
"test": "ngc -p tsconfig.spec.json && jasmine dist/spec/*.spec.js",
99
"publish": "npm publish dist",
1010
"precommit": "lint-staged",
1111
"lint": "tslint -c tslint.json 'src/**/*.ts'",
@@ -51,7 +51,7 @@
5151
"rxjs": "^5.5.6",
5252
"tsickle": "^0.26.0",
5353
"tslint": "^5.8.0",
54-
"typescript": "~2.5.3"
54+
"typescript": "^2.6.2"
5555
},
5656
"ngPackage": {
5757
"lib": {

src/index.ts

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,2 @@
11
import 'reflect-metadata';
2-
export * from './module';
3-
export * from './action';
4-
export * from './of-action';
5-
export * from './store';
6-
export * from './select';
7-
export * from './symbols';
8-
export * from './factory';
2+
export * from './public_api';

src/internals.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Action } from '@ngrx/store';
1+
import { ActionType } from './symbols';
22

33
export interface ActionMeta {
4-
action: { new (...args: any[]): Action };
4+
action: ActionType;
55
fn: string;
66
type: string;
77
}

src/memoize.ts

-12
This file was deleted.

src/of-action.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { Action } from '@ngrx/store';
2-
import { Actions } from '@ngrx/effects';
32
import { filter } from 'rxjs/operators';
4-
import { Observable } from 'rxjs/Observable';
53
import { OperatorFunction } from 'rxjs/interfaces';
4+
import { ActionType } from './symbols';
65

7-
export function ofAction<T extends Action>(allowedType: { new (...args: any[]): T }): OperatorFunction<Action, T>;
8-
export function ofAction<T extends Action>(
9-
...allowedTypes: { new (...args: any[]): Action }[]
10-
): OperatorFunction<Action, Action>;
11-
export function ofAction(...allowedTypes: { new (...args: any[]): Action }[]): OperatorFunction<Action, Action> {
6+
export function ofAction<T extends Action>(allowedType: ActionType<T>): OperatorFunction<Action, T>;
7+
export function ofAction<T extends Action>(...allowedTypes: ActionType[]): OperatorFunction<Action, T>;
8+
export function ofAction(...allowedTypes: ActionType[]): OperatorFunction<Action, Action> {
129
const allowedMap = {};
1310
allowedTypes.forEach(klass => (allowedMap[new klass().type] = true));
1411
return filter((action: Action) => {

src/public_api.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export { NgrxActionsModule } from './module';
2+
export { Action } from './action';
3+
export { ofAction } from './of-action';
4+
export { Store } from './store';
5+
export { Select, NgrxSelect } from './select';
6+
export { createReducer } from './factory';

src/select.ts

+48-14
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,48 @@
11
import { Injectable } from '@angular/core';
2-
import { Store } from '@ngrx/store';
3-
import { Observable } from 'rxjs/Observable';
4-
import { memoize } from './memoize';
2+
import { Store, Selector } from '@ngrx/store';
53

64
@Injectable()
75
export class NgrxSelect {
8-
static store: Store<any> = undefined;
6+
static store: Store<any> | undefined = undefined;
97
connect(store: Store<any>) {
108
NgrxSelect.store = store;
119
}
1210
}
1311

14-
export function Select(path?: string): any {
15-
return function(target: any, name: string, descriptor: TypedPropertyDescriptor<any>): void {
12+
export function Select<TState = any, TValue = any>(
13+
selector: Selector<TState, TValue>
14+
): (target: any, name: string) => void;
15+
export function Select<TState = any, TValue = any>(
16+
selectorOrFeature?: string,
17+
...paths: string[]
18+
): (target: any, name: string) => void;
19+
export function Select<TState = any, TValue = any>(
20+
selectorOrFeature?: string | Selector<TState, TValue>,
21+
...paths: string[]
22+
) {
23+
return function(target: any, name: string): void {
24+
let fn: Selector<TState, TValue>;
25+
// Nothing here? Use propery name as selector
26+
if (!selectorOrFeature) {
27+
selectorOrFeature = name;
28+
}
29+
// Handle string vs Selector<TState, TValue>
30+
if (typeof selectorOrFeature === 'string') {
31+
const propsArray = paths.length ? [selectorOrFeature, ...paths] : selectorOrFeature.split('.');
32+
fn = fastPropGetter(propsArray);
33+
} else {
34+
fn = selectorOrFeature;
35+
}
36+
// Redefine property
1637
if (delete target[name]) {
1738
Object.defineProperty(target, name, {
1839
get: () => {
19-
if (!NgrxSelect.store) {
40+
// get connected store
41+
const store = NgrxSelect.store;
42+
if (!store) {
2043
throw new Error('NgrxSelect not connected to store!');
2144
}
22-
23-
const fn = memoize(state => getValue(state, path || name));
24-
return NgrxSelect.store.select(fn);
45+
return store.select(fn);
2546
},
2647
enumerable: true,
2748
configurable: true
@@ -30,9 +51,22 @@ export function Select(path?: string): any {
3051
};
3152
}
3253

33-
function getValue(state, prop: string) {
34-
if (prop) {
35-
return prop.split('.').reduce((acc, part) => acc && acc[part], state);
54+
/**
55+
* The generated function is faster than:
56+
* - pluck (Observable operator)
57+
* - memoize (old ngrx-actions implementation)
58+
* - MemoizedSelector (ngrx)
59+
* @param path
60+
*/
61+
export function fastPropGetter(paths: string[]): (x: any) => any {
62+
const segments = paths;
63+
let seg = 'store.' + segments[0],
64+
i = 0;
65+
const l = segments.length;
66+
let expr = seg;
67+
while (++i < l) {
68+
expr = expr + ' && ' + (seg = seg + '.' + segments[i]);
3669
}
37-
return state;
70+
const fn = new Function('store', 'return ' + expr + ';');
71+
return <(x: any) => any>fn;
3872
}

src/index.spec.ts src/spec/index.spec.ts

+76-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
import { Store, createReducer, Action, ofAction } from './index';
2-
import { of } from 'rxjs/Observable/of';
3-
import { Action as NgRxAction } from '@ngrx/store';
1+
import { Store, createReducer, Action, ofAction, Select, NgrxSelect } from '../index';
2+
import { Action as NgRxAction, createFeatureSelector, createSelector, Store as NgRxStore } from '@ngrx/store';
3+
import { Observable } from 'rxjs/Observable';
4+
import { of } from 'rxjs/observable/of';
45

56
describe('actions', () => {
67
interface FooState {
78
foo: boolean | null;
9+
bar?: {
10+
a?: {
11+
b?: any;
12+
};
13+
};
814
}
915

1016
it('has strict type checking working', () => {
@@ -20,9 +26,9 @@ describe('actions', () => {
2026
}
2127
}
2228

23-
const reducer = createReducer<FooState>(Bar);
29+
const reducer = createReducer<FooState | undefined>(Bar);
2430
const res = reducer(undefined, new MyAction());
25-
expect(res.foo).toBe(true);
31+
expect(res && res.foo).toBe(true);
2632
});
2733

2834
it('adds defaults', () => {
@@ -131,13 +137,72 @@ describe('actions', () => {
131137
readonly type = 'myaction2';
132138
}
133139

134-
const action = new MyAction('foo');
135-
const actions = of<NgRxAction>(action, new MyAction2());
136-
let tappedAction: NgRxAction;
137-
actions.pipe(ofAction(MyAction)).subscribe(a => {
138-
tappedAction = a;
140+
class MyAction3 implements NgRxAction {
141+
readonly type = 'myaction3';
142+
constructor(public foo: any, public bar: any) {}
143+
}
144+
145+
const action = new MyAction('foo'),
146+
action2 = new MyAction2(),
147+
action3 = new MyAction3('a', 0);
148+
const actions = of<NgRxAction>(action, action2, action3);
149+
const tappedActions: NgRxAction[] = [];
150+
actions.pipe(ofAction<MyAction | MyAction2>(MyAction, MyAction2)).subscribe(a => {
151+
tappedActions.push(a);
139152
});
140153

141-
expect(tappedAction).toBe(action);
154+
expect(tappedActions.length).toEqual(2);
155+
expect(tappedActions[0]).toBe(action);
156+
expect(tappedActions[1]).toBe(action2);
157+
});
158+
159+
it('selects sub state', () => {
160+
const globalState: {
161+
myFeature: FooState;
162+
} = {
163+
myFeature: {
164+
foo: true,
165+
bar: {
166+
a: {
167+
b: {
168+
c: {
169+
d: 'world'
170+
}
171+
}
172+
}
173+
}
174+
}
175+
};
176+
177+
const msFeature = createFeatureSelector<FooState>('myFeature');
178+
const msBar = createSelector(msFeature, state => state.bar);
179+
180+
class MyStateSelector {
181+
@Select('myFeature.bar.a.b.c.d') hello$: Observable<string>; // deeply nested props
182+
@Select() myFeature: Observable<FooState>; // implied by name
183+
@Select(msBar) bar$: Observable<any>; // using MemoizedSelector
184+
}
185+
186+
const store = new NgRxStore(of(globalState), undefined, undefined);
187+
188+
try {
189+
NgrxSelect.store = store;
190+
191+
const mss = new MyStateSelector();
192+
193+
mss.hello$.subscribe(n => {
194+
expect(n).toBe('world');
195+
});
196+
197+
mss.myFeature.subscribe(n => {
198+
expect(n).toBe(globalState.myFeature);
199+
});
200+
201+
mss.bar$.subscribe(n => {
202+
expect(n).toBe(globalState.myFeature.bar);
203+
});
204+
} finally {
205+
NgrxSelect.store = undefined;
206+
}
142207
});
143208
});

src/spec/tsconfig.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/* This file is for IDE only */
2+
{
3+
"extends": "../../tsconfig.spec.json"
4+
}

src/store.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { INITIAL_STATE_KEY } from './keys';
2-
import { Action } from '@ngrx/store';
32

43
export function Store<TState>(initialState?: TState): (target: Function) => void;
54
export function Store(initialState?: any): (target: Function) => void;

src/symbols.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import { Action } from '@ngrx/store';
22

3-
export type ActionType = { new (...args: any[]): Action };
3+
export type ActionType<T extends Action = Action> = { new (...args: any[]): T };

tsconfig.json

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
"rootDir": "src/",
1515
"skipDefaultLibCheck": true,
1616
"skipLibCheck": true,
17+
"noImplicitAny": false,
18+
"noUnusedLocals": true,
19+
"strict": true,
1720
"sourceMap": true,
1821
"target": "es5",
1922
"types": [
@@ -23,5 +26,8 @@
2326
},
2427
"include": [
2528
"src/**/*"
29+
],
30+
"exclude": [
31+
"src/spec/**/*"
2632
]
2733
}

tsconfig.spec.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"strictNullChecks": false
5+
},
6+
"files": ["src/spec/index.spec.ts"]
7+
}

0 commit comments

Comments
 (0)