Skip to content

Commit

Permalink
feat(component-store): add selectSignal options
Browse files Browse the repository at this point in the history
  • Loading branch information
timdeschryver authored and markostanimirovic committed May 3, 2023
1 parent 755295f commit 503e9d8
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 9 deletions.
71 changes: 71 additions & 0 deletions modules/component-store/spec/component-store.spec.ts
Expand Up @@ -1401,6 +1401,41 @@ describe('Component Store', () => {
expect(projectorExecutionCount).toBe(2);
});

it('creates a signal from the provided state projector function with options', () => {
const store = new ComponentStore<{ arr: number[] }>({
arr: [10, 20, 30],
});
let projectorExecutionCount = 0;

const array = store.selectSignal(
(x) => {
projectorExecutionCount++;
return x.arr;
},
{
equal: (a, b) => a.length === b.length,
}
);

array();
const result1 = array();
expect(result1).toEqual([10, 20, 30]);

store.patchState({ arr: [30, 20, 10] });

// should be equal to the previous value because of the custom equality
array();
const result2 = array();
expect(result2).toEqual([10, 20, 30]);
expect(result2).toBe(result1);
expect(projectorExecutionCount).toBe(2);

store.patchState({ arr: [10] });
array();
expect(array()).toEqual([10]);
expect(projectorExecutionCount).toBe(3);
});

it('creates a signal by combining provided signals', () => {
const store = new ComponentStore<{ x: number; y: number; z: number }>({
x: 1,
Expand Down Expand Up @@ -1429,6 +1464,42 @@ describe('Component Store', () => {
expect(projectorExecutionCount).toBe(2);
});

it('creates a signal by combining provided signals with options', () => {
const store = new ComponentStore<{ x: number; y: number }>({
x: 1,
y: 10,
});
let projectorExecutionCount = 0;

const x = store.selectSignal((s) => s.x);
const y = store.selectSignal((s) => s.y);
const xPlusY = store.selectSignal(
x,
y,
(x, y) => {
projectorExecutionCount++;
return x + y;
},
{
equal: (a: number, b: number) => Math.round(a) === Math.round(b),
}
);

expect(xPlusY()).toBe(11);

store.patchState({ x: 1.2 });
xPlusY();

// should be equal to the previous value because of the custom equality
expect(xPlusY()).toBe(11);
expect(projectorExecutionCount).toBe(2);

store.patchState({ x: 1.8 });
xPlusY();
expect(xPlusY()).toBe(11.8);
expect(projectorExecutionCount).toBe(3);
});

it('throws an error when the signal is read before the state initialization', () => {
const store = new ComponentStore<{ foo: string }>();
const foo = store.selectSignal((s) => s.foo);
Expand Down
57 changes: 48 additions & 9 deletions modules/component-store/src/component-store.ts
Expand Up @@ -34,6 +34,7 @@ import {
isDevMode,
Signal,
computed,
type ValueEqualityFn,
} from '@angular/core';
import { isOnStateInitDefined, isOnStoreInitDefined } from './lifecycle_hooks';
import { toSignal } from './to-signal';
Expand Down Expand Up @@ -64,6 +65,13 @@ type SignalsProjector<Signals extends Signal<unknown>[], Result> = (
}
) => Result;

interface SelectSignalOptions<T> {
/**
* A comparison function which defines equality for select results.
*/
equal?: ValueEqualityFn<T>;
}

@Injectable()
export class ComponentStore<T extends object> implements OnDestroy {
// Should be used only in ngOnDestroy.
Expand Down Expand Up @@ -301,38 +309,69 @@ export class ComponentStore<T extends object> implements OnDestroy {
/**
* Creates a signal from the provided state projector function.
*/
selectSignal<Result>(projector: (state: T) => Result): Signal<Result>;
selectSignal<Result>(
projector: (state: T) => Result,
options?: SelectSignalOptions<Result>
): Signal<Result>;
/**
* Creates a signal by combining provided signals.
*/
selectSignal<Signals extends Signal<unknown>[], Result>(
...args: [...signals: Signals, projector: SignalsProjector<Signals, Result>]
): Signal<Result>;
/**
* Creates a signal by combining provided signals.
*/
selectSignal<Signals extends Signal<unknown>[], Result>(
...signalsWithProjector: [
...selectors: Signals,
projector: SignalsProjector<Signals, Result>
...args: [
...signals: Signals,
projector: SignalsProjector<Signals, Result>,
options: SelectSignalOptions<Result>
]
): Signal<Result>;
selectSignal(
...args:
| [(state: T) => unknown]
| [(state: T) => unknown, SelectSignalOptions<unknown>?]
| [
...signals: Signal<unknown>[],
projector: (...values: unknown[]) => unknown
]
| [
...signals: Signal<unknown>[],
projector: (...values: unknown[]) => unknown,
options: SelectSignalOptions<unknown>
]
): Signal<unknown> {
if (args.length === 1) {
const projector = args[0] as (state: T) => unknown;
return computed(() => projector(this.state()));
}

const signals = args.slice(0, -1) as Signal<unknown>[];
const projector = args[args.length - 1] as (
const optionsOrProjector = args[args.length - 1] as (
...values: unknown[]
) => unknown;
) => unknown | SelectSignalOptions<unknown>;
if (typeof optionsOrProjector === 'function') {
const signals = args.slice(0, -1) as Signal<unknown>[];

return computed(() => {
const values = signals.map((signal) => signal());
return optionsOrProjector(...values);
});
}

if (args.length === 2) {
const projector = args[0] as (state: T) => unknown;
return computed(() => projector(this.state()), optionsOrProjector);
}

const signals = args.slice(0, -2) as Signal<unknown>[];
const projector = args[args.length - 2] as (
...values: unknown[]
) => unknown;
return computed(() => {
const values = signals.map((signal) => signal());
return projector(...values);
});
}, optionsOrProjector);
}

/**
Expand Down

0 comments on commit 503e9d8

Please sign in to comment.