Skip to content

Commit 7bc804e

Browse files
feat(signals): add withLinkedState
Generates and adds the properties of a `linkedSignal` to the store's state. ## Usage Notes: ```typescript const UserStore = signalStore( withState({ options: [1, 2, 3] }), withLinkedState(({ options }) => ({ selectOption: options()[0] ?? undefined })) ); ``` The resulting state is of type `{ options: number[], selectOption: number | undefined }`. Whenever the `options` signal changes, the `selectOption` will automatically update. For advanced use cases, `linkedSignal` can be called within `withLinkedState`: ```typescript const UserStore = signalStore( withState({ id: 1 }), withLinkedState(({ id }) => linkedSignal({ source: id, computation: () => ({ firstname: '', lastname: '' }) })) ) ```
1 parent 5f159a1 commit 7bc804e

File tree

5 files changed

+583
-0
lines changed

5 files changed

+583
-0
lines changed

modules/signals/spec/state-source.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,27 @@ describe('StateSource', () => {
293293
TestBed.flushEffects();
294294
expect(executionCount).toBe(2);
295295
});
296+
297+
it('does not support a dynamic type as state', () => {
298+
const Store = signalStore(
299+
{ providedIn: 'root' },
300+
withState<Record<number, number>>({}),
301+
withMethods((store) => ({
302+
addNumber(num: number): void {
303+
patchState(store, {
304+
[num]: num,
305+
});
306+
},
307+
}))
308+
);
309+
const store = TestBed.inject(Store);
310+
311+
store.addNumber(1);
312+
store.addNumber(2);
313+
store.addNumber(3);
314+
315+
expect(getState(store)).toEqual({});
316+
});
296317
});
297318
});
298319

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { expecter } from 'ts-snippet';
2+
import { compilerOptions } from './helpers';
3+
4+
describe('patchState', () => {
5+
const expectSnippet = expecter(
6+
(code) => `
7+
import { computed, inject, linkedSignal, Signal, signal } from '@angular/core';
8+
import {
9+
patchState,
10+
signalStore,
11+
withState,
12+
withLinkedState,
13+
withMethods
14+
} from '@ngrx/signals';
15+
16+
${code}
17+
`,
18+
compilerOptions()
19+
);
20+
21+
it('does not have access to methods', () => {
22+
const snippet = `
23+
signalStore(
24+
withMethods(() => ({
25+
foo: () => 'bar',
26+
})),
27+
withLinkedState(({ foo }) => ({ value: foo() }))
28+
);
29+
`;
30+
31+
expectSnippet(snippet).toFail(/Property 'foo' does not exist on type '{}'/);
32+
});
33+
34+
it('does not have access to STATE_SOURCE', () => {
35+
const snippet = `
36+
signalStore(
37+
withState({ foo: 'bar' }),
38+
withLinkedState((store) => {
39+
patchState(store, { foo: 'baz' });
40+
return { bar: 'foo' };
41+
})
42+
)
43+
`;
44+
45+
expectSnippet(snippet).toFail(
46+
/is not assignable to parameter of type 'WritableStateSource<object>'./
47+
);
48+
});
49+
50+
it('cannot return a primitive value', () => {
51+
const snippet = `
52+
signalStore(
53+
withLinkedState(() => 'foo')
54+
)
55+
`;
56+
57+
expectSnippet(snippet).toFail(
58+
/Type 'string' is not assignable to type 'object | WritableSignal<object>'./
59+
);
60+
});
61+
62+
it('resolves to a normal state signal with automatic linkedSignal', () => {
63+
const snippet = `
64+
const UserStore = signalStore(
65+
{ providedIn: 'root' },
66+
withLinkedState(() => ({ firstname: 'John', lastname: 'Doe' }))
67+
);
68+
69+
const userStore = new UserStore();
70+
71+
const firstname = userStore.firstname;
72+
const lastname = userStore.lastname;
73+
`;
74+
75+
expectSnippet(snippet).toSucceed();
76+
77+
expectSnippet(snippet).toInfer('firstname', 'Signal<string>');
78+
expectSnippet(snippet).toInfer('lastname', 'Signal<string>');
79+
});
80+
81+
it('resolves to a normal state signal with manual linkedSignal', () => {
82+
const snippet = `
83+
const UserStore = signalStore(
84+
{ providedIn: 'root' },
85+
withLinkedState(() =>
86+
linkedSignal(() => ({ firstname: 'John', lastname: 'Doe' }))
87+
)
88+
);
89+
90+
const userStore = new UserStore();
91+
92+
const firstname = userStore.firstname;
93+
const lastname = userStore.lastname;
94+
`;
95+
96+
expectSnippet(snippet).toSucceed();
97+
98+
expectSnippet(snippet).toInfer('firstname', 'Signal<string>');
99+
expectSnippet(snippet).toInfer('lastname', 'Signal<string>');
100+
});
101+
});

0 commit comments

Comments
 (0)