Skip to content

Commit b1b5370

Browse files
committed
feat(state): add keyCompareMap option to select's keys+fn overload
1 parent 14bfc5b commit b1b5370

File tree

6 files changed

+87
-14
lines changed

6 files changed

+87
-14
lines changed

libs/state/selections/spec/select.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,43 @@ describe('select', () => {
145145
});
146146
});
147147

148+
it('should accept array of strings keyof T, map function and key compare map', () => {
149+
testScheduler.run(({ cold, expectObservable }) => {
150+
const state: PrimitiveState & NestedState = {
151+
bol: true,
152+
str: 'string',
153+
num: 42,
154+
obj: {
155+
key1: {
156+
key11: {
157+
key111: 'foo',
158+
},
159+
},
160+
},
161+
};
162+
const source: Observable<PrimitiveState & NestedState> = cold('abcde|', {
163+
a: state,
164+
b: { ...state, bol: false },
165+
c: { ...state, num: 69 },
166+
d: { ...state, num: 69, obj: { key1: { key11: { key111: 'foo' } } } },
167+
e: { ...state, num: 69, obj: { key1: { key11: { key111: 'bar' } } } },
168+
});
169+
expectObservable(
170+
source.pipe(
171+
select(
172+
['num', 'obj'],
173+
({ num, obj }) => `${num}: ${obj.key1.key11.key111}`,
174+
{ obj: (a, b) => a.key1.key11.key111 === b.key1.key11.key111 }
175+
)
176+
)
177+
).toBe('a-c-e|', {
178+
a: '42: foo',
179+
c: '69: foo',
180+
e: '69: bar',
181+
});
182+
});
183+
});
184+
148185
it('should accept one operator', () => {
149186
testScheduler.run(({ cold, expectObservable }) => {
150187
const source: Observable<PrimitiveState> = cold('a|', {

libs/state/selections/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export {
1414
isObjectGuard,
1515
isOperateFnArrayGuard,
1616
isStringAndFunctionTupleGuard,
17+
isStringArrayFunctionAndOptionalObjectTupleGuard,
1718
isStringArrayGuard,
18-
isStringsArrayAndFunctionTupleGuard,
1919
} from './lib/utils/guards';
2020
export { pipeFromArray } from './lib/utils/pipe-from-array';
2121
export { safePluck } from './lib/utils/safe-pluck';

libs/state/selections/src/lib/operators/select.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { MonoTypeOperatorFunction, Observable, OperatorFunction } from 'rxjs';
22
import { map, pluck } from 'rxjs/operators';
3-
import { PickSlice } from '../interfaces';
3+
import { KeyCompareMap, PickSlice } from '../interfaces';
44
import {
55
isOperateFnArrayGuard,
66
isStringAndFunctionTupleGuard,
7+
isStringArrayFunctionAndOptionalObjectTupleGuard,
78
isStringArrayGuard,
8-
isStringsArrayAndFunctionTupleGuard,
99
} from '../utils/guards';
1010
import { pipeFromArray } from '../utils/pipe-from-array';
1111
import { selectSlice } from './selectSlice';
@@ -99,7 +99,8 @@ export function select<T, A, B, C, D, E>(
9999
*/
100100
export function select<T extends object, K extends keyof T, R>(
101101
keys: K[],
102-
fn: (slice: PickSlice<T, K>) => R
102+
fn: (slice: PickSlice<T, K>) => R,
103+
keyCompareMap?: KeyCompareMap<Pick<T, K>>
103104
): OperatorFunction<T, R>;
104105

105106
/**
@@ -205,17 +206,24 @@ export function select<T extends Record<string, unknown>>(
205206
...opOrMapFn:
206207
| OperatorFunction<T, unknown>[]
207208
| string[]
208-
| [string, (val: unknown) => unknown]
209-
| [string[], (slice: unknown) => unknown]
209+
| [k: string, fn: (val: unknown) => unknown]
210+
| [
211+
keys: string[],
212+
fn: (slice: unknown) => unknown,
213+
keyCompareMap?: KeyCompareMap<T>
214+
]
210215
): OperatorFunction<T, unknown> {
211216
return (state$: Observable<T>) => {
212217
if (!opOrMapFn || opOrMapFn.length === 0) {
213218
return state$.pipe(stateful());
214219
} else if (isStringAndFunctionTupleGuard(opOrMapFn)) {
215220
return state$.pipe(stateful(map((s) => opOrMapFn[1](s[opOrMapFn[0]]))));
216-
} else if (isStringsArrayAndFunctionTupleGuard(opOrMapFn)) {
221+
} else if (isStringArrayFunctionAndOptionalObjectTupleGuard(opOrMapFn)) {
217222
return state$.pipe(
218-
selectSlice<T & object, keyof T>(opOrMapFn[0] as (keyof T)[]),
223+
selectSlice<T & object, keyof T>(
224+
opOrMapFn[0] as (keyof T)[],
225+
opOrMapFn[2]
226+
),
219227
stateful(map(opOrMapFn[1]))
220228
);
221229
} else if (isStringArrayGuard(opOrMapFn)) {

libs/state/selections/src/lib/utils/guards.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,12 @@ export function isStringAndFunctionTupleGuard<R>(
6060
return typeof op[0] === 'string' && typeof op[1] === 'function';
6161
}
6262

63-
export function isStringsArrayAndFunctionTupleGuard<R>(
63+
export function isStringArrayFunctionAndOptionalObjectTupleGuard<R>(
6464
op: unknown[]
65-
): op is [string[], (val: any) => R] {
66-
return isStringArrayGuard(op[0] as any) && typeof op[1] === 'function';
65+
): op is [strs: string[], fn: (val: any) => R, obj?: object] {
66+
return (
67+
isStringArrayGuard(op[0] as any) &&
68+
typeof op[1] === 'function' &&
69+
(op[2] === undefined || typeof op[2] === 'object')
70+
);
6771
}

libs/state/spec/rx-state.service.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { fakeAsync, TestBed } from '@angular/core/testing';
22
import { RxState } from '@rx-angular/state';
33
import { select } from '@rx-angular/state/selections';
44
import {
5+
initialNestedState,
56
initialPrimitiveState,
67
jestMatcher,
78
PrimitiveState,
@@ -251,6 +252,23 @@ describe('RxStateService', () => {
251252
});
252253
});
253254
});
255+
256+
it('should return mapped slice on select with keys, function and key compare map', () => {
257+
testScheduler.run(({ expectObservable }) => {
258+
const state = setupState({
259+
initialState: { ...initialPrimitiveState, ...initialNestedState },
260+
});
261+
expectObservable(
262+
state.select(
263+
['num', 'obj'],
264+
({ num, obj }) => `${num}: ${obj.key1.key11.key111}`,
265+
{ obj: (a, b) => a.key1.key11.key111 === b.key1.key11.key111 }
266+
)
267+
).toBe('s', {
268+
s: `${initialPrimitiveState.num}: ${initialNestedState.obj.key1.key11.key111}`,
269+
});
270+
});
271+
});
254272
});
255273
});
256274

libs/state/src/lib/rx-state.service.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
createAccumulationObservable,
66
createSideEffectObservable,
77
isKeyOf,
8+
KeyCompareMap,
89
PickSlice,
910
safePluck,
1011
select,
@@ -488,7 +489,8 @@ export class RxState<T extends object> implements OnDestroy, Subscribable<T> {
488489
*/
489490
select<K extends keyof T, V>(
490491
keys: K[],
491-
fn: (slice: PickSlice<T, K>) => V
492+
fn: (slice: PickSlice<T, K>) => V,
493+
keyCompareMap?: KeyCompareMap<Pick<T, K>>
492494
): Observable<V>;
493495
/**
494496
* @description
@@ -578,8 +580,12 @@ export class RxState<T extends object> implements OnDestroy, Subscribable<T> {
578580
...args:
579581
| OperatorFunction<T, unknown>[]
580582
| string[]
581-
| [string, (val: unknown) => unknown]
582-
| [string[], (slice: unknown) => unknown]
583+
| [k: string, fn: (val: unknown) => unknown]
584+
| [
585+
keys: string[],
586+
fn: (slice: unknown) => unknown,
587+
keyCompareMap?: KeyCompareMap<T>
588+
]
583589
): Observable<T | R> {
584590
return this.accumulator.state$.pipe(
585591
select(...(args as Parameters<typeof select>))

0 commit comments

Comments
 (0)