Skip to content

Commit 503e9d8

Browse files
timdeschryvermarkostanimirovic
authored andcommitted
feat(component-store): add selectSignal options
1 parent 755295f commit 503e9d8

File tree

2 files changed

+119
-9
lines changed

2 files changed

+119
-9
lines changed

modules/component-store/spec/component-store.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,41 @@ describe('Component Store', () => {
14011401
expect(projectorExecutionCount).toBe(2);
14021402
});
14031403

1404+
it('creates a signal from the provided state projector function with options', () => {
1405+
const store = new ComponentStore<{ arr: number[] }>({
1406+
arr: [10, 20, 30],
1407+
});
1408+
let projectorExecutionCount = 0;
1409+
1410+
const array = store.selectSignal(
1411+
(x) => {
1412+
projectorExecutionCount++;
1413+
return x.arr;
1414+
},
1415+
{
1416+
equal: (a, b) => a.length === b.length,
1417+
}
1418+
);
1419+
1420+
array();
1421+
const result1 = array();
1422+
expect(result1).toEqual([10, 20, 30]);
1423+
1424+
store.patchState({ arr: [30, 20, 10] });
1425+
1426+
// should be equal to the previous value because of the custom equality
1427+
array();
1428+
const result2 = array();
1429+
expect(result2).toEqual([10, 20, 30]);
1430+
expect(result2).toBe(result1);
1431+
expect(projectorExecutionCount).toBe(2);
1432+
1433+
store.patchState({ arr: [10] });
1434+
array();
1435+
expect(array()).toEqual([10]);
1436+
expect(projectorExecutionCount).toBe(3);
1437+
});
1438+
14041439
it('creates a signal by combining provided signals', () => {
14051440
const store = new ComponentStore<{ x: number; y: number; z: number }>({
14061441
x: 1,
@@ -1429,6 +1464,42 @@ describe('Component Store', () => {
14291464
expect(projectorExecutionCount).toBe(2);
14301465
});
14311466

1467+
it('creates a signal by combining provided signals with options', () => {
1468+
const store = new ComponentStore<{ x: number; y: number }>({
1469+
x: 1,
1470+
y: 10,
1471+
});
1472+
let projectorExecutionCount = 0;
1473+
1474+
const x = store.selectSignal((s) => s.x);
1475+
const y = store.selectSignal((s) => s.y);
1476+
const xPlusY = store.selectSignal(
1477+
x,
1478+
y,
1479+
(x, y) => {
1480+
projectorExecutionCount++;
1481+
return x + y;
1482+
},
1483+
{
1484+
equal: (a: number, b: number) => Math.round(a) === Math.round(b),
1485+
}
1486+
);
1487+
1488+
expect(xPlusY()).toBe(11);
1489+
1490+
store.patchState({ x: 1.2 });
1491+
xPlusY();
1492+
1493+
// should be equal to the previous value because of the custom equality
1494+
expect(xPlusY()).toBe(11);
1495+
expect(projectorExecutionCount).toBe(2);
1496+
1497+
store.patchState({ x: 1.8 });
1498+
xPlusY();
1499+
expect(xPlusY()).toBe(11.8);
1500+
expect(projectorExecutionCount).toBe(3);
1501+
});
1502+
14321503
it('throws an error when the signal is read before the state initialization', () => {
14331504
const store = new ComponentStore<{ foo: string }>();
14341505
const foo = store.selectSignal((s) => s.foo);

modules/component-store/src/component-store.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
isDevMode,
3535
Signal,
3636
computed,
37+
type ValueEqualityFn,
3738
} from '@angular/core';
3839
import { isOnStateInitDefined, isOnStoreInitDefined } from './lifecycle_hooks';
3940
import { toSignal } from './to-signal';
@@ -64,6 +65,13 @@ type SignalsProjector<Signals extends Signal<unknown>[], Result> = (
6465
}
6566
) => Result;
6667

68+
interface SelectSignalOptions<T> {
69+
/**
70+
* A comparison function which defines equality for select results.
71+
*/
72+
equal?: ValueEqualityFn<T>;
73+
}
74+
6775
@Injectable()
6876
export class ComponentStore<T extends object> implements OnDestroy {
6977
// Should be used only in ngOnDestroy.
@@ -301,38 +309,69 @@ export class ComponentStore<T extends object> implements OnDestroy {
301309
/**
302310
* Creates a signal from the provided state projector function.
303311
*/
304-
selectSignal<Result>(projector: (state: T) => Result): Signal<Result>;
312+
selectSignal<Result>(
313+
projector: (state: T) => Result,
314+
options?: SelectSignalOptions<Result>
315+
): Signal<Result>;
316+
/**
317+
* Creates a signal by combining provided signals.
318+
*/
319+
selectSignal<Signals extends Signal<unknown>[], Result>(
320+
...args: [...signals: Signals, projector: SignalsProjector<Signals, Result>]
321+
): Signal<Result>;
305322
/**
306323
* Creates a signal by combining provided signals.
307324
*/
308325
selectSignal<Signals extends Signal<unknown>[], Result>(
309-
...signalsWithProjector: [
310-
...selectors: Signals,
311-
projector: SignalsProjector<Signals, Result>
326+
...args: [
327+
...signals: Signals,
328+
projector: SignalsProjector<Signals, Result>,
329+
options: SelectSignalOptions<Result>
312330
]
313331
): Signal<Result>;
314332
selectSignal(
315333
...args:
316-
| [(state: T) => unknown]
334+
| [(state: T) => unknown, SelectSignalOptions<unknown>?]
317335
| [
318336
...signals: Signal<unknown>[],
319337
projector: (...values: unknown[]) => unknown
320338
]
339+
| [
340+
...signals: Signal<unknown>[],
341+
projector: (...values: unknown[]) => unknown,
342+
options: SelectSignalOptions<unknown>
343+
]
321344
): Signal<unknown> {
322345
if (args.length === 1) {
323346
const projector = args[0] as (state: T) => unknown;
324347
return computed(() => projector(this.state()));
325348
}
326349

327-
const signals = args.slice(0, -1) as Signal<unknown>[];
328-
const projector = args[args.length - 1] as (
350+
const optionsOrProjector = args[args.length - 1] as (
329351
...values: unknown[]
330-
) => unknown;
352+
) => unknown | SelectSignalOptions<unknown>;
353+
if (typeof optionsOrProjector === 'function') {
354+
const signals = args.slice(0, -1) as Signal<unknown>[];
355+
356+
return computed(() => {
357+
const values = signals.map((signal) => signal());
358+
return optionsOrProjector(...values);
359+
});
360+
}
331361

362+
if (args.length === 2) {
363+
const projector = args[0] as (state: T) => unknown;
364+
return computed(() => projector(this.state()), optionsOrProjector);
365+
}
366+
367+
const signals = args.slice(0, -2) as Signal<unknown>[];
368+
const projector = args[args.length - 2] as (
369+
...values: unknown[]
370+
) => unknown;
332371
return computed(() => {
333372
const values = signals.map((signal) => signal());
334373
return projector(...values);
335-
});
374+
}, optionsOrProjector);
336375
}
337376

338377
/**

0 commit comments

Comments
 (0)