Skip to content

Commit

Permalink
feat(component-store): add selectSignal signature that combines provi…
Browse files Browse the repository at this point in the history
…ded signals (#3863)
  • Loading branch information
markostanimirovic authored Apr 29, 2023
1 parent d604695 commit 07ba3fa
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 12 deletions.
56 changes: 56 additions & 0 deletions modules/component-store/spec/component-store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,62 @@ describe('Component Store', () => {
});
});

describe('selectSignal', () => {
it('creates a signal from the provided state projector function', () => {
const store = new ComponentStore<{ foo: string }>({ foo: 'bar' });
let projectorExecutionCount = 0;

const foo = store.selectSignal((state) => {
projectorExecutionCount++;
return state.foo;
});

expect(foo()).toBe('bar');

foo();
store.patchState({ foo: 'baz' });
foo();

expect(foo()).toBe('baz');
expect(projectorExecutionCount).toBe(2);
});

it('creates a signal by combining provided signals', () => {
const store = new ComponentStore<{ x: number; y: number; z: number }>({
x: 1,
y: 10,
z: 100,
});
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;
});

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

// projector should not be executed
store.patchState({ z: 1000 });
xPlusY();

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

expect(xPlusY()).toBe(20);
expect(projectorExecutionCount).toBe(2);
});

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);

expect(() => foo()).toThrowError();
});
});

describe('effect', () => {
let componentStore: ComponentStore<object>;

Expand Down
58 changes: 46 additions & 12 deletions modules/component-store/src/component-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ export type Projector<Selectors extends Observable<unknown>[], Result> = (
...args: SelectorResults<Selectors>
) => Result;

type SignalsProjector<Signals extends Signal<unknown>[], Result> = (
...values: {
[Key in keyof Signals]: Signals[Key] extends Signal<infer Value>
? Value
: never;
}
) => Result;

@Injectable()
export class ComponentStore<T extends object> implements OnDestroy {
// Should be used only in ngOnDestroy.
Expand All @@ -67,10 +75,12 @@ export class ComponentStore<T extends object> implements OnDestroy {
private isInitialized = false;
// Needs to be after destroy$ is declared because it's used in select.
readonly state$: Observable<T> = this.select((s) => s);
private ɵhasProvider = false;

// Signal of state$
readonly state: Signal<T>;
readonly state: Signal<T> = toSignal(
this.state$.pipe(takeUntil(this.destroy$)),
{ requireSync: false, manualCleanup: true }
);
private ɵhasProvider = false;

constructor(@Optional() @Inject(INITIAL_STATE_TOKEN) defaultState?: T) {
// State can be initialized either through constructor or setState.
Expand All @@ -79,10 +89,6 @@ export class ComponentStore<T extends object> implements OnDestroy {
}

this.checkProviderForHooks();
this.state = toSignal(this.stateSubject$.pipe(takeUntil(this.destroy$)), {
requireSync: false,
manualCleanup: true,
});
}

/** Completes all relevant Observable streams. */
Expand Down Expand Up @@ -293,12 +299,40 @@ export class ComponentStore<T extends object> implements OnDestroy {
}

/**
* Returns a signal of the provided projector function.
*
* @param projector projector function
* Creates a signal from the provided state projector function.
*/
selectSignal<Result>(projector: (state: T) => Result): Signal<Result>;
/**
* Creates a signal by combining provided signals.
*/
selectSignal<K>(projector: (state: T) => K): Signal<K> {
return computed(() => projector(this.state()));
selectSignal<Signals extends Signal<unknown>[], Result>(
...signalsWithProjector: [
...selectors: Signals,
projector: SignalsProjector<Signals, Result>
]
): Signal<Result>;
selectSignal(
...args:
| [(state: T) => unknown]
| [
...signals: Signal<unknown>[],
projector: (...values: unknown[]) => 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 (
...values: unknown[]
) => unknown;

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

/**
Expand Down

0 comments on commit 07ba3fa

Please sign in to comment.