@craft-ng/core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/craft-ng-core.mjs.map +1 -1
- package/package.json +1 -1
- package/types/craft-ng-core.d.ts +523 -523
package/types/craft-ng-core.d.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import * as _angular_core from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import { WritableSignal, Signal, Injector, InjectionToken, Provider, ValueEqualityFn, Type, EventEmitter, ResourceRef, ResourceOptions, ResourceStatus, ResourceLoaderParams, ResourceStreamingLoader } from '@angular/core';
|
|
3
3
|
import { CompatFieldState, FieldState, ReadonlyArrayLike, MaybeFieldTree, Subfields, FieldTree, SchemaPathTree, PathKind, SchemaPath, SchemaPathRules, ValidationError } from '@angular/forms/signals';
|
|
4
4
|
import { AbstractControl } from '@angular/forms';
|
|
5
5
|
|
|
6
|
-
type ReadonlySource<T> = Signal<T | undefined> & {
|
|
7
|
-
preserveLastValue: Signal<T | undefined>;
|
|
8
|
-
} & SourceBranded;
|
|
9
|
-
|
|
10
6
|
declare const SourceBranded: {
|
|
11
7
|
[SourceBrand]: true;
|
|
12
8
|
};
|
|
@@ -24,92 +20,67 @@ declare function createMethodHandlers<State>(methodsData: Record<string, ((...ar
|
|
|
24
20
|
onStateChange?: (newValue: State) => void;
|
|
25
21
|
}): Record<string, Function>;
|
|
26
22
|
|
|
27
|
-
type
|
|
28
|
-
set: (value: T) => void;
|
|
23
|
+
type ReadonlySource<T> = Signal<T | undefined> & {
|
|
29
24
|
preserveLastValue: Signal<T | undefined>;
|
|
30
25
|
} & SourceBranded;
|
|
26
|
+
|
|
31
27
|
/**
|
|
32
|
-
* Creates a source
|
|
28
|
+
* Creates a derived readonly source that transforms source emissions through a callback function.
|
|
33
29
|
*
|
|
34
|
-
*
|
|
35
|
-
* -
|
|
36
|
-
* -
|
|
37
|
-
* -
|
|
38
|
-
* -
|
|
39
|
-
* - Multi-listener support with independent subscription timing
|
|
30
|
+
* This function binds queries, mutations, and async methods to sources for automatic execution by:
|
|
31
|
+
* - Listening to source emissions and computing new values
|
|
32
|
+
* - Providing a readonly source suitable for method binding
|
|
33
|
+
* - Maintaining reactivity through Angular's effect system
|
|
34
|
+
* - Enabling source-based triggering patterns
|
|
40
35
|
*
|
|
41
36
|
* @remarks
|
|
42
|
-
* **
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* **Difference from Signals:**
|
|
50
|
-
* - **Signals**: Always have a value, recompute on access, continuous state
|
|
51
|
-
* - **Sources**: Emit on explicit set, lazy by default, discrete events
|
|
52
|
-
* - Sources are for events/actions, signals are for state
|
|
37
|
+
* **Primary Use Case:**
|
|
38
|
+
* Bind queries/mutations/async methods to sources for automatic execution:
|
|
39
|
+
* ```ts
|
|
40
|
+
* method: afterRecomputation(mySource, (data) => data)
|
|
41
|
+
* ```
|
|
42
|
+
* This pattern makes queries/mutations execute automatically when the source emits.
|
|
53
43
|
*
|
|
54
|
-
* **
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* - **Store inputs**: Triggering queries/mutations from components
|
|
60
|
-
* - **Event buses**: Decoupled event communication
|
|
44
|
+
* **Execution Flow:**
|
|
45
|
+
* 1. Source emits a value via `source.set(value)`
|
|
46
|
+
* 2. afterRecomputation callback transforms the value
|
|
47
|
+
* 3. Resulting readonly source emits the transformed value
|
|
48
|
+
* 4. Bound query/mutation/async method executes with the new value
|
|
61
49
|
*
|
|
62
|
-
* **
|
|
63
|
-
* -
|
|
64
|
-
* -
|
|
65
|
-
* -
|
|
50
|
+
* **Difference from computedSource:**
|
|
51
|
+
* - `afterRecomputation`: Designed for binding to method parameters
|
|
52
|
+
* - `computedSource`: General-purpose source transformation
|
|
53
|
+
* - Both transform source values, but afterRecomputation is optimized for method binding
|
|
66
54
|
*
|
|
67
|
-
* **
|
|
68
|
-
* - **
|
|
69
|
-
* - **
|
|
70
|
-
* -
|
|
55
|
+
* **Common Patterns:**
|
|
56
|
+
* - **Identity transformation**: `afterRecomputation(source, (x) => x)` - pass value through
|
|
57
|
+
* - **Field extraction**: `afterRecomputation(source, (data) => data.id)` - extract specific field
|
|
58
|
+
* - **Validation**: `afterRecomputation(source, (data) => validate(data))` - transform and validate
|
|
59
|
+
* - **Mapping**: `afterRecomputation(source, (data) => mapToDto(data))` - convert to different type
|
|
71
60
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* Understanding these three key limitations is important:
|
|
75
|
-
* - **Multiple sets in same cycle**: When a source is set multiple times during the same cycle
|
|
76
|
-
* (between the first set and the Change Detection that executes all consumer callbacks),
|
|
77
|
-
* consumers will only react once during CD and will only see the last set value.
|
|
78
|
-
* Intermediate values are discarded.
|
|
79
|
-
* - **Multiple sources order**: Within the same cycle, if multiple sources are triggered,
|
|
80
|
-
* consumers cannot determine the order in which the sources were set.
|
|
81
|
-
* The original emission sequence is not preserved.
|
|
82
|
-
* - **Consumer execution order**: When multiple sources are triggered in the same cycle,
|
|
83
|
-
* consumer callbacks are invoked in the order they were declared, not in the order
|
|
84
|
-
* their source producers were triggered.
|
|
85
|
-
* - **No synchronous intermediate value reactions**: Unlike observables, sources cannot react
|
|
86
|
-
* to each intermediate value synchronously. A mechanism similar to observables
|
|
87
|
-
* (or using native Observable API) without RxJS is being considered to enable
|
|
88
|
-
* synchronous reactions to intermediate values, matching the behavior currently
|
|
89
|
-
* offered by observables.
|
|
61
|
+
* @template State - The type of values produced by the callback
|
|
62
|
+
* @template SourceType - The type of values emitted by the origin source
|
|
90
63
|
*
|
|
91
|
-
* @
|
|
64
|
+
* @param _source - The source to listen to.
|
|
65
|
+
* When this source emits, the callback is invoked.
|
|
92
66
|
*
|
|
93
|
-
* @param
|
|
94
|
-
*
|
|
95
|
-
* - `debugName`: Name for debugging purposes
|
|
67
|
+
* @param callback - Function that transforms source values.
|
|
68
|
+
* Receives the emitted value and returns the transformed result.
|
|
96
69
|
*
|
|
97
|
-
* @returns A source
|
|
98
|
-
*
|
|
99
|
-
* - `set(value)`: Emit a value to all listeners
|
|
100
|
-
* - `preserveLastValue`: Alternative signal that returns last value immediately
|
|
70
|
+
* @returns A readonly source that emits transformed values.
|
|
71
|
+
* Can be used as the `method` parameter in queries, mutations, and async methods.
|
|
101
72
|
*
|
|
102
73
|
* @example
|
|
103
|
-
*
|
|
74
|
+
* Binding a query to a source for automatic execution
|
|
104
75
|
* ```ts
|
|
105
76
|
* const { injectCraft } = craft(
|
|
106
77
|
* { name: '', providedIn: 'root' },
|
|
107
78
|
* craftSources({
|
|
108
|
-
*
|
|
79
|
+
* userIdChange: source<string>(),
|
|
109
80
|
* }),
|
|
110
|
-
* craftQuery('user', ({
|
|
81
|
+
* craftQuery('user', ({ userIdChange }) =>
|
|
111
82
|
* query({
|
|
112
|
-
* method: afterRecomputation(
|
|
83
|
+
* method: afterRecomputation(userIdChange, (userId) => userId),
|
|
113
84
|
* loader: async ({ params }) => {
|
|
114
85
|
* const response = await fetch(`/api/users/${params}`);
|
|
115
86
|
* return response.json();
|
|
@@ -121,27 +92,24 @@ type SignalSource<T> = Signal<T | undefined> & {
|
|
|
121
92
|
* const store = injectCraft();
|
|
122
93
|
*
|
|
123
94
|
* // Query executes automatically when source emits
|
|
124
|
-
* store.
|
|
125
|
-
* // ->
|
|
126
|
-
* // -> user query executes with params 'user-123'
|
|
95
|
+
* store.setUserIdChange('user-123');
|
|
96
|
+
* // -> query loader executes with params 'user-123'
|
|
127
97
|
*
|
|
128
|
-
* store.
|
|
129
|
-
* // ->
|
|
98
|
+
* store.setUserIdChange('user-456');
|
|
99
|
+
* // -> query loader executes again with params 'user-456'
|
|
130
100
|
* ```
|
|
131
101
|
*
|
|
132
102
|
* @example
|
|
133
|
-
*
|
|
103
|
+
* Binding a mutation to a source
|
|
134
104
|
* ```ts
|
|
135
|
-
* type FormData = { name: string; email: string };
|
|
136
|
-
*
|
|
137
105
|
* const { injectCraft } = craft(
|
|
138
106
|
* { name: '', providedIn: 'root' },
|
|
139
107
|
* craftSources({
|
|
140
|
-
* submitForm: source<
|
|
108
|
+
* submitForm: source<{ name: string; email: string }>(),
|
|
141
109
|
* }),
|
|
142
110
|
* craftMutations(({ submitForm }) => ({
|
|
143
111
|
* submit: mutation({
|
|
144
|
-
* method: afterRecomputation(submitForm, (
|
|
112
|
+
* method: afterRecomputation(submitForm, (formData) => formData),
|
|
145
113
|
* loader: async ({ params }) => {
|
|
146
114
|
* const response = await fetch('/api/submit', {
|
|
147
115
|
* method: 'POST',
|
|
@@ -155,86 +123,60 @@ type SignalSource<T> = Signal<T | undefined> & {
|
|
|
155
123
|
*
|
|
156
124
|
* const store = injectCraft();
|
|
157
125
|
*
|
|
158
|
-
* //
|
|
159
|
-
*
|
|
160
|
-
* //
|
|
161
|
-
* //
|
|
162
|
-
* // </form>
|
|
163
|
-
*
|
|
164
|
-
* onSubmit() {
|
|
165
|
-
* // Mutation executes automatically
|
|
166
|
-
* this.store.setSubmitForm(this.formData);
|
|
167
|
-
* // -> submitForm source emits
|
|
168
|
-
* // -> submit mutation executes
|
|
169
|
-
* }
|
|
126
|
+
* // Mutation executes automatically when source emits
|
|
127
|
+
* store.setSubmitForm({ name: 'John', email: 'john@example.com' });
|
|
128
|
+
* // -> mutation loader executes with form data
|
|
129
|
+
* // Note: No store.mutateSubmit method exposed (source-based)
|
|
170
130
|
* ```
|
|
171
131
|
*
|
|
172
132
|
* @example
|
|
173
|
-
*
|
|
133
|
+
* Binding async method to a source
|
|
174
134
|
* ```ts
|
|
175
135
|
* const { injectCraft } = craft(
|
|
176
136
|
* { name: '', providedIn: 'root' },
|
|
177
137
|
* craftSources({
|
|
178
|
-
*
|
|
138
|
+
* searchInput: source<string>(),
|
|
179
139
|
* }),
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* },
|
|
140
|
+
* craftAsyncProcesses(({ searchInput }) => ({
|
|
141
|
+
* search: asyncProcess({
|
|
142
|
+
* method: afterRecomputation(searchInput, (term) => term),
|
|
143
|
+
* loader: async ({ params }) => {
|
|
144
|
+
* // Debounce at source level before setting
|
|
145
|
+
* const response = await fetch(`/api/search?q=${params}`);
|
|
146
|
+
* return response.json();
|
|
188
147
|
* },
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* )
|
|
148
|
+
* }),
|
|
149
|
+
* }))
|
|
192
150
|
* );
|
|
193
151
|
*
|
|
194
152
|
* const store = injectCraft();
|
|
195
153
|
*
|
|
196
|
-
* //
|
|
197
|
-
* store.
|
|
198
|
-
* // ->
|
|
199
|
-
* // -> query reloads
|
|
200
|
-
*
|
|
201
|
-
* // In component:
|
|
202
|
-
* // <button (click)="store.setReload()">Refresh</button>
|
|
154
|
+
* // Async method executes automatically
|
|
155
|
+
* store.setSearchInput('query');
|
|
156
|
+
* // -> search loader executes
|
|
203
157
|
* ```
|
|
204
158
|
*
|
|
205
159
|
* @example
|
|
206
|
-
*
|
|
160
|
+
* Extracting specific field from complex data
|
|
207
161
|
* ```ts
|
|
162
|
+
* type FormData = {
|
|
163
|
+
* user: { id: string; name: string };
|
|
164
|
+
* address: { city: string };
|
|
165
|
+
* };
|
|
166
|
+
*
|
|
208
167
|
* const { injectCraft } = craft(
|
|
209
168
|
* { name: '', providedIn: 'root' },
|
|
210
169
|
* craftSources({
|
|
211
|
-
*
|
|
212
|
-
* deleteTodo: source<string>(),
|
|
213
|
-
* toggleTodo: source<string>(),
|
|
170
|
+
* formSubmit: source<FormData>(),
|
|
214
171
|
* }),
|
|
215
|
-
* craftMutations(({
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* const response = await fetch('/api/todos', {
|
|
220
|
-
* method: 'POST',
|
|
221
|
-
* body: JSON.stringify(params),
|
|
222
|
-
* });
|
|
223
|
-
* return response.json();
|
|
224
|
-
* },
|
|
225
|
-
* }),
|
|
226
|
-
* delete: mutation({
|
|
227
|
-
* method: afterRecomputation(deleteTodo, (id) => id),
|
|
228
|
-
* loader: async ({ params }) => {
|
|
229
|
-
* await fetch(`/api/todos/${params}`, { method: 'DELETE' });
|
|
230
|
-
* return { deleted: true };
|
|
231
|
-
* },
|
|
232
|
-
* }),
|
|
233
|
-
* toggle: mutation({
|
|
234
|
-
* method: afterRecomputation(toggleTodo, (id) => id),
|
|
172
|
+
* craftMutations(({ formSubmit }) => ({
|
|
173
|
+
* updateUser: mutation({
|
|
174
|
+
* // Extract only user data
|
|
175
|
+
* method: afterRecomputation(formSubmit, (data) => data.user),
|
|
235
176
|
* loader: async ({ params }) => {
|
|
236
|
-
* const response = await fetch(`/api/
|
|
177
|
+
* const response = await fetch(`/api/users/${params.id}`, {
|
|
237
178
|
* method: 'PATCH',
|
|
179
|
+
* body: JSON.stringify(params),
|
|
238
180
|
* });
|
|
239
181
|
* return response.json();
|
|
240
182
|
* },
|
|
@@ -244,267 +186,136 @@ type SignalSource<T> = Signal<T | undefined> & {
|
|
|
244
186
|
*
|
|
245
187
|
* const store = injectCraft();
|
|
246
188
|
*
|
|
247
|
-
* //
|
|
248
|
-
* store.
|
|
249
|
-
*
|
|
250
|
-
*
|
|
189
|
+
* // Only user data is passed to mutation
|
|
190
|
+
* store.setFormSubmit({
|
|
191
|
+
* user: { id: 'user-1', name: 'John' },
|
|
192
|
+
* address: { city: 'NYC' },
|
|
193
|
+
* });
|
|
194
|
+
* // -> mutation receives only { id: 'user-1', name: 'John' }
|
|
251
195
|
* ```
|
|
252
196
|
*
|
|
253
197
|
* @example
|
|
254
|
-
*
|
|
198
|
+
* Transforming data before execution
|
|
255
199
|
* ```ts
|
|
256
|
-
* const
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
200
|
+
* const { injectCraft } = craft(
|
|
201
|
+
* { name: '', providedIn: 'root' },
|
|
202
|
+
* craftSources({
|
|
203
|
+
* searchParams: source<{ query: string; filters: string[] }>(),
|
|
204
|
+
* }),
|
|
205
|
+
* craftQuery('results', ({ searchParams }) =>
|
|
206
|
+
* query({
|
|
207
|
+
* method: afterRecomputation(searchParams, (params) => ({
|
|
208
|
+
* q: params.query.trim().toLowerCase(),
|
|
209
|
+
* f: params.filters.join(','),
|
|
210
|
+
* })),
|
|
211
|
+
* loader: async ({ params }) => {
|
|
212
|
+
* const query = new URLSearchParams(params);
|
|
213
|
+
* const response = await fetch(`/api/search?${query}`);
|
|
214
|
+
* return response.json();
|
|
215
|
+
* },
|
|
216
|
+
* })
|
|
217
|
+
* )
|
|
218
|
+
* );
|
|
261
219
|
*
|
|
262
|
-
*
|
|
263
|
-
* mySource.set('Hello');
|
|
264
|
-
* console.log(listener1()); // 'Hello'
|
|
220
|
+
* const store = injectCraft();
|
|
265
221
|
*
|
|
266
|
-
* //
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* mySource.set('World');
|
|
271
|
-
* console.log(listener1()); // 'World'
|
|
272
|
-
* console.log(listener2()); // 'World'
|
|
273
|
-
*
|
|
274
|
-
* // Using preserveLastValue for late listeners
|
|
275
|
-
* const listener3 = computed(() => mySource.preserveLastValue());
|
|
276
|
-
* console.log(listener3()); // 'World' (gets last value immediately)
|
|
277
|
-
* ```
|
|
278
|
-
*
|
|
279
|
-
* @example
|
|
280
|
-
* Custom equality to prevent duplicate emissions
|
|
281
|
-
* ```ts
|
|
282
|
-
* type Params = { id: string; timestamp: number };
|
|
283
|
-
*
|
|
284
|
-
* const paramsSource = source<Params>({
|
|
285
|
-
* equal: (a, b) => a?.id === b?.id, // Compare only by id
|
|
222
|
+
* // Data is transformed before query execution
|
|
223
|
+
* store.setSearchParams({
|
|
224
|
+
* query: ' Angular ',
|
|
225
|
+
* filters: ['tutorial', 'advanced'],
|
|
286
226
|
* });
|
|
287
|
-
*
|
|
288
|
-
* const listener = computed(() => paramsSource());
|
|
289
|
-
*
|
|
290
|
-
* paramsSource.set({ id: 'item-1', timestamp: Date.now() });
|
|
291
|
-
* // -> listener receives value
|
|
292
|
-
*
|
|
293
|
-
* paramsSource.set({ id: 'item-1', timestamp: Date.now() });
|
|
294
|
-
* // -> listener does NOT receive value (same id)
|
|
295
|
-
*
|
|
296
|
-
* paramsSource.set({ id: 'item-2', timestamp: Date.now() });
|
|
297
|
-
* // -> listener receives value (different id)
|
|
298
|
-
* ```
|
|
299
|
-
*
|
|
300
|
-
* @example
|
|
301
|
-
* Source for coordinating multiple components
|
|
302
|
-
* ```ts
|
|
303
|
-
* // Global source (outside component)
|
|
304
|
-
* const refreshAllSource = source<void>();
|
|
305
|
-
*
|
|
306
|
-
* // Component A
|
|
307
|
-
* @Component({
|
|
308
|
-
* selector: 'app-data-view',
|
|
309
|
-
* template: '...',
|
|
310
|
-
* })
|
|
311
|
-
* export class DataViewComponent {
|
|
312
|
-
* { injectCraft } = craft(
|
|
313
|
-
* { name: '', providedIn: 'root' },
|
|
314
|
-
* craftQuery('data', () =>
|
|
315
|
-
* query(
|
|
316
|
-
* {
|
|
317
|
-
* params: () => ({}),
|
|
318
|
-
* loader: async () => {
|
|
319
|
-
* const response = await fetch('/api/data');
|
|
320
|
-
* return response.json();
|
|
321
|
-
* },
|
|
322
|
-
* },
|
|
323
|
-
* insertReloadOnSource(refreshAllSource)
|
|
324
|
-
* )
|
|
325
|
-
* )
|
|
326
|
-
* );
|
|
327
|
-
*
|
|
328
|
-
* store = this.injectCraft();
|
|
329
|
-
* }
|
|
330
|
-
*
|
|
331
|
-
* // Component B
|
|
332
|
-
* @Component({
|
|
333
|
-
* selector: 'app-refresh-button',
|
|
334
|
-
* template: '<button (click)="refresh()">Refresh All</button>',
|
|
335
|
-
* })
|
|
336
|
-
* export class RefreshButtonComponent {
|
|
337
|
-
* refresh() {
|
|
338
|
-
* // Triggers refresh in all components listening to this source
|
|
339
|
-
* refreshAllSource.set();
|
|
340
|
-
* }
|
|
341
|
-
* }
|
|
227
|
+
* // -> query receives { q: 'angular', f: 'tutorial,advanced' }
|
|
342
228
|
* ```
|
|
343
229
|
*
|
|
344
230
|
* @example
|
|
345
|
-
*
|
|
231
|
+
* Validation and type narrowing
|
|
346
232
|
* ```ts
|
|
347
|
-
* type SearchParams = {
|
|
348
|
-
* query: string;
|
|
349
|
-
* filters: string[];
|
|
350
|
-
* page: number;
|
|
351
|
-
* };
|
|
352
|
-
*
|
|
353
233
|
* const { injectCraft } = craft(
|
|
354
234
|
* { name: '', providedIn: 'root' },
|
|
355
235
|
* craftSources({
|
|
356
|
-
*
|
|
236
|
+
* inputChange: source<string>(),
|
|
357
237
|
* }),
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
* method: afterRecomputation(
|
|
238
|
+
* craftAsyncProcesses(({ inputChange }) => ({
|
|
239
|
+
* validate: asyncProcess({
|
|
240
|
+
* method: afterRecomputation(inputChange, (input) => {
|
|
241
|
+
* // Only proceed if input is valid
|
|
242
|
+
* const trimmed = input.trim();
|
|
243
|
+
* if (trimmed.length < 3) {
|
|
244
|
+
* throw new Error('Input too short');
|
|
245
|
+
* }
|
|
246
|
+
* return trimmed;
|
|
247
|
+
* }),
|
|
361
248
|
* loader: async ({ params }) => {
|
|
362
|
-
* const
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
* page: String(params.page),
|
|
249
|
+
* const response = await fetch('/api/validate', {
|
|
250
|
+
* method: 'POST',
|
|
251
|
+
* body: JSON.stringify({ input: params }),
|
|
366
252
|
* });
|
|
367
|
-
* const response = await fetch(`/api/search?${queryString}`);
|
|
368
253
|
* return response.json();
|
|
369
254
|
* },
|
|
370
|
-
* })
|
|
371
|
-
* )
|
|
255
|
+
* }),
|
|
256
|
+
* }))
|
|
372
257
|
* );
|
|
373
258
|
*
|
|
374
259
|
* const store = injectCraft();
|
|
375
260
|
*
|
|
376
|
-
* //
|
|
377
|
-
* store.
|
|
378
|
-
* query: 'angular',
|
|
379
|
-
* filters: ['tutorial', 'advanced'],
|
|
380
|
-
* page: 1,
|
|
381
|
-
* });
|
|
382
|
-
* ```
|
|
383
|
-
*/
|
|
384
|
-
declare function signalSource<T>(options?: {
|
|
385
|
-
equal?: ValueEqualityFn<NoInfer<T> | undefined>;
|
|
386
|
-
debugName?: string;
|
|
387
|
-
}): SignalSource<T>;
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Creates a derived readonly source that transforms source emissions through a callback function.
|
|
391
|
-
*
|
|
392
|
-
* This function binds queries, mutations, and async methods to sources for automatic execution by:
|
|
393
|
-
* - Listening to source emissions and computing new values
|
|
394
|
-
* - Providing a readonly source suitable for method binding
|
|
395
|
-
* - Maintaining reactivity through Angular's effect system
|
|
396
|
-
* - Enabling source-based triggering patterns
|
|
261
|
+
* // Invalid input throws error in callback
|
|
262
|
+
* store.setInputChange('ab'); // Error: Input too short
|
|
397
263
|
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
* Bind queries/mutations/async methods to sources for automatic execution:
|
|
401
|
-
* ```ts
|
|
402
|
-
* method: afterRecomputation(mySource, (data) => data)
|
|
264
|
+
* // Valid input proceeds
|
|
265
|
+
* store.setInputChange('valid input'); // Validation executes
|
|
403
266
|
* ```
|
|
404
|
-
* This pattern makes queries/mutations execute automatically when the source emits.
|
|
405
|
-
*
|
|
406
|
-
* **Execution Flow:**
|
|
407
|
-
* 1. Source emits a value via `source.set(value)`
|
|
408
|
-
* 2. afterRecomputation callback transforms the value
|
|
409
|
-
* 3. Resulting readonly source emits the transformed value
|
|
410
|
-
* 4. Bound query/mutation/async method executes with the new value
|
|
411
|
-
*
|
|
412
|
-
* **Difference from computedSource:**
|
|
413
|
-
* - `afterRecomputation`: Designed for binding to method parameters
|
|
414
|
-
* - `computedSource`: General-purpose source transformation
|
|
415
|
-
* - Both transform source values, but afterRecomputation is optimized for method binding
|
|
416
|
-
*
|
|
417
|
-
* **Common Patterns:**
|
|
418
|
-
* - **Identity transformation**: `afterRecomputation(source, (x) => x)` - pass value through
|
|
419
|
-
* - **Field extraction**: `afterRecomputation(source, (data) => data.id)` - extract specific field
|
|
420
|
-
* - **Validation**: `afterRecomputation(source, (data) => validate(data))` - transform and validate
|
|
421
|
-
* - **Mapping**: `afterRecomputation(source, (data) => mapToDto(data))` - convert to different type
|
|
422
|
-
*
|
|
423
|
-
* @template State - The type of values produced by the callback
|
|
424
|
-
* @template SourceType - The type of values emitted by the origin source
|
|
425
|
-
*
|
|
426
|
-
* @param _source - The source to listen to.
|
|
427
|
-
* When this source emits, the callback is invoked.
|
|
428
|
-
*
|
|
429
|
-
* @param callback - Function that transforms source values.
|
|
430
|
-
* Receives the emitted value and returns the transformed result.
|
|
431
|
-
*
|
|
432
|
-
* @returns A readonly source that emits transformed values.
|
|
433
|
-
* Can be used as the `method` parameter in queries, mutations, and async methods.
|
|
434
267
|
*
|
|
435
268
|
* @example
|
|
436
|
-
*
|
|
269
|
+
* Multiple sources with different transformations
|
|
437
270
|
* ```ts
|
|
438
271
|
* const { injectCraft } = craft(
|
|
439
272
|
* { name: '', providedIn: 'root' },
|
|
440
273
|
* craftSources({
|
|
441
|
-
*
|
|
274
|
+
* quickSearch: source<string>(),
|
|
275
|
+
* advancedSearch: source<{ query: string; options: unknown }>(),
|
|
442
276
|
* }),
|
|
443
|
-
* craftQuery('
|
|
277
|
+
* craftQuery('searchResults', ({ quickSearch, advancedSearch }) =>
|
|
444
278
|
* query({
|
|
445
|
-
* method: afterRecomputation(
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* })
|
|
451
|
-
* )
|
|
452
|
-
* );
|
|
453
|
-
*
|
|
454
|
-
* const store = injectCraft();
|
|
455
|
-
*
|
|
456
|
-
* // Query executes automatically when source emits
|
|
457
|
-
* store.setUserIdChange('user-123');
|
|
458
|
-
* // -> query loader executes with params 'user-123'
|
|
459
|
-
*
|
|
460
|
-
* store.setUserIdChange('user-456');
|
|
461
|
-
* // -> query loader executes again with params 'user-456'
|
|
462
|
-
* ```
|
|
463
|
-
*
|
|
464
|
-
* @example
|
|
465
|
-
* Binding a mutation to a source
|
|
466
|
-
* ```ts
|
|
467
|
-
* const { injectCraft } = craft(
|
|
468
|
-
* { name: '', providedIn: 'root' },
|
|
469
|
-
* craftSources({
|
|
470
|
-
* submitForm: source<{ name: string; email: string }>(),
|
|
471
|
-
* }),
|
|
472
|
-
* craftMutations(({ submitForm }) => ({
|
|
473
|
-
* submit: mutation({
|
|
474
|
-
* method: afterRecomputation(submitForm, (formData) => formData),
|
|
279
|
+
* method: afterRecomputation(
|
|
280
|
+
* // Can combine sources at higher level
|
|
281
|
+
* quickSearch, // For this example, using one source
|
|
282
|
+
* (term) => ({ query: term, mode: 'quick' })
|
|
283
|
+
* ),
|
|
475
284
|
* loader: async ({ params }) => {
|
|
476
|
-
* const response = await fetch('/api/
|
|
285
|
+
* const response = await fetch('/api/search', {
|
|
477
286
|
* method: 'POST',
|
|
478
287
|
* body: JSON.stringify(params),
|
|
479
288
|
* });
|
|
480
289
|
* return response.json();
|
|
481
290
|
* },
|
|
482
|
-
* })
|
|
483
|
-
*
|
|
291
|
+
* })
|
|
292
|
+
* )
|
|
484
293
|
* );
|
|
485
294
|
*
|
|
486
295
|
* const store = injectCraft();
|
|
487
296
|
*
|
|
488
|
-
* //
|
|
489
|
-
* store.
|
|
490
|
-
* // ->
|
|
491
|
-
* // Note: No store.mutateSubmit method exposed (source-based)
|
|
297
|
+
* // Quick search with simple string
|
|
298
|
+
* store.setQuickSearch('angular');
|
|
299
|
+
* // -> query receives { query: 'angular', mode: 'quick' }
|
|
492
300
|
* ```
|
|
493
301
|
*
|
|
494
302
|
* @example
|
|
495
|
-
*
|
|
303
|
+
* Identity transformation (pass-through)
|
|
496
304
|
* ```ts
|
|
497
305
|
* const { injectCraft } = craft(
|
|
498
306
|
* { name: '', providedIn: 'root' },
|
|
499
307
|
* craftSources({
|
|
500
|
-
*
|
|
308
|
+
* dataUpdate: source<{ id: string; payload: unknown }>(),
|
|
501
309
|
* }),
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
310
|
+
* craftMutations(({ dataUpdate }) => ({
|
|
311
|
+
* update: mutation({
|
|
312
|
+
* // Pass data through unchanged
|
|
313
|
+
* method: afterRecomputation(dataUpdate, (data) => data),
|
|
505
314
|
* loader: async ({ params }) => {
|
|
506
|
-
*
|
|
507
|
-
*
|
|
315
|
+
* const response = await fetch(`/api/data/${params.id}`, {
|
|
316
|
+
* method: 'PUT',
|
|
317
|
+
* body: JSON.stringify(params.payload),
|
|
318
|
+
* });
|
|
508
319
|
* return response.json();
|
|
509
320
|
* },
|
|
510
321
|
* }),
|
|
@@ -513,185 +324,12 @@ declare function signalSource<T>(options?: {
|
|
|
513
324
|
*
|
|
514
325
|
* const store = injectCraft();
|
|
515
326
|
*
|
|
516
|
-
* //
|
|
517
|
-
* store.
|
|
518
|
-
* // ->
|
|
519
|
-
* ```
|
|
520
|
-
*
|
|
521
|
-
* @example
|
|
522
|
-
* Extracting specific field from complex data
|
|
523
|
-
* ```ts
|
|
524
|
-
* type FormData = {
|
|
525
|
-
* user: { id: string; name: string };
|
|
526
|
-
* address: { city: string };
|
|
527
|
-
* };
|
|
528
|
-
*
|
|
529
|
-
* const { injectCraft } = craft(
|
|
530
|
-
* { name: '', providedIn: 'root' },
|
|
531
|
-
* craftSources({
|
|
532
|
-
* formSubmit: source<FormData>(),
|
|
533
|
-
* }),
|
|
534
|
-
* craftMutations(({ formSubmit }) => ({
|
|
535
|
-
* updateUser: mutation({
|
|
536
|
-
* // Extract only user data
|
|
537
|
-
* method: afterRecomputation(formSubmit, (data) => data.user),
|
|
538
|
-
* loader: async ({ params }) => {
|
|
539
|
-
* const response = await fetch(`/api/users/${params.id}`, {
|
|
540
|
-
* method: 'PATCH',
|
|
541
|
-
* body: JSON.stringify(params),
|
|
542
|
-
* });
|
|
543
|
-
* return response.json();
|
|
544
|
-
* },
|
|
545
|
-
* }),
|
|
546
|
-
* }))
|
|
547
|
-
* );
|
|
548
|
-
*
|
|
549
|
-
* const store = injectCraft();
|
|
550
|
-
*
|
|
551
|
-
* // Only user data is passed to mutation
|
|
552
|
-
* store.setFormSubmit({
|
|
553
|
-
* user: { id: 'user-1', name: 'John' },
|
|
554
|
-
* address: { city: 'NYC' },
|
|
555
|
-
* });
|
|
556
|
-
* // -> mutation receives only { id: 'user-1', name: 'John' }
|
|
557
|
-
* ```
|
|
558
|
-
*
|
|
559
|
-
* @example
|
|
560
|
-
* Transforming data before execution
|
|
561
|
-
* ```ts
|
|
562
|
-
* const { injectCraft } = craft(
|
|
563
|
-
* { name: '', providedIn: 'root' },
|
|
564
|
-
* craftSources({
|
|
565
|
-
* searchParams: source<{ query: string; filters: string[] }>(),
|
|
566
|
-
* }),
|
|
567
|
-
* craftQuery('results', ({ searchParams }) =>
|
|
568
|
-
* query({
|
|
569
|
-
* method: afterRecomputation(searchParams, (params) => ({
|
|
570
|
-
* q: params.query.trim().toLowerCase(),
|
|
571
|
-
* f: params.filters.join(','),
|
|
572
|
-
* })),
|
|
573
|
-
* loader: async ({ params }) => {
|
|
574
|
-
* const query = new URLSearchParams(params);
|
|
575
|
-
* const response = await fetch(`/api/search?${query}`);
|
|
576
|
-
* return response.json();
|
|
577
|
-
* },
|
|
578
|
-
* })
|
|
579
|
-
* )
|
|
580
|
-
* );
|
|
581
|
-
*
|
|
582
|
-
* const store = injectCraft();
|
|
583
|
-
*
|
|
584
|
-
* // Data is transformed before query execution
|
|
585
|
-
* store.setSearchParams({
|
|
586
|
-
* query: ' Angular ',
|
|
587
|
-
* filters: ['tutorial', 'advanced'],
|
|
588
|
-
* });
|
|
589
|
-
* // -> query receives { q: 'angular', f: 'tutorial,advanced' }
|
|
590
|
-
* ```
|
|
591
|
-
*
|
|
592
|
-
* @example
|
|
593
|
-
* Validation and type narrowing
|
|
594
|
-
* ```ts
|
|
595
|
-
* const { injectCraft } = craft(
|
|
596
|
-
* { name: '', providedIn: 'root' },
|
|
597
|
-
* craftSources({
|
|
598
|
-
* inputChange: source<string>(),
|
|
599
|
-
* }),
|
|
600
|
-
* craftAsyncProcesses(({ inputChange }) => ({
|
|
601
|
-
* validate: asyncProcess({
|
|
602
|
-
* method: afterRecomputation(inputChange, (input) => {
|
|
603
|
-
* // Only proceed if input is valid
|
|
604
|
-
* const trimmed = input.trim();
|
|
605
|
-
* if (trimmed.length < 3) {
|
|
606
|
-
* throw new Error('Input too short');
|
|
607
|
-
* }
|
|
608
|
-
* return trimmed;
|
|
609
|
-
* }),
|
|
610
|
-
* loader: async ({ params }) => {
|
|
611
|
-
* const response = await fetch('/api/validate', {
|
|
612
|
-
* method: 'POST',
|
|
613
|
-
* body: JSON.stringify({ input: params }),
|
|
614
|
-
* });
|
|
615
|
-
* return response.json();
|
|
616
|
-
* },
|
|
617
|
-
* }),
|
|
618
|
-
* }))
|
|
619
|
-
* );
|
|
620
|
-
*
|
|
621
|
-
* const store = injectCraft();
|
|
622
|
-
*
|
|
623
|
-
* // Invalid input throws error in callback
|
|
624
|
-
* store.setInputChange('ab'); // Error: Input too short
|
|
625
|
-
*
|
|
626
|
-
* // Valid input proceeds
|
|
627
|
-
* store.setInputChange('valid input'); // Validation executes
|
|
628
|
-
* ```
|
|
629
|
-
*
|
|
630
|
-
* @example
|
|
631
|
-
* Multiple sources with different transformations
|
|
632
|
-
* ```ts
|
|
633
|
-
* const { injectCraft } = craft(
|
|
634
|
-
* { name: '', providedIn: 'root' },
|
|
635
|
-
* craftSources({
|
|
636
|
-
* quickSearch: source<string>(),
|
|
637
|
-
* advancedSearch: source<{ query: string; options: unknown }>(),
|
|
638
|
-
* }),
|
|
639
|
-
* craftQuery('searchResults', ({ quickSearch, advancedSearch }) =>
|
|
640
|
-
* query({
|
|
641
|
-
* method: afterRecomputation(
|
|
642
|
-
* // Can combine sources at higher level
|
|
643
|
-
* quickSearch, // For this example, using one source
|
|
644
|
-
* (term) => ({ query: term, mode: 'quick' })
|
|
645
|
-
* ),
|
|
646
|
-
* loader: async ({ params }) => {
|
|
647
|
-
* const response = await fetch('/api/search', {
|
|
648
|
-
* method: 'POST',
|
|
649
|
-
* body: JSON.stringify(params),
|
|
650
|
-
* });
|
|
651
|
-
* return response.json();
|
|
652
|
-
* },
|
|
653
|
-
* })
|
|
654
|
-
* )
|
|
655
|
-
* );
|
|
656
|
-
*
|
|
657
|
-
* const store = injectCraft();
|
|
658
|
-
*
|
|
659
|
-
* // Quick search with simple string
|
|
660
|
-
* store.setQuickSearch('angular');
|
|
661
|
-
* // -> query receives { query: 'angular', mode: 'quick' }
|
|
662
|
-
* ```
|
|
663
|
-
*
|
|
664
|
-
* @example
|
|
665
|
-
* Identity transformation (pass-through)
|
|
666
|
-
* ```ts
|
|
667
|
-
* const { injectCraft } = craft(
|
|
668
|
-
* { name: '', providedIn: 'root' },
|
|
669
|
-
* craftSources({
|
|
670
|
-
* dataUpdate: source<{ id: string; payload: unknown }>(),
|
|
671
|
-
* }),
|
|
672
|
-
* craftMutations(({ dataUpdate }) => ({
|
|
673
|
-
* update: mutation({
|
|
674
|
-
* // Pass data through unchanged
|
|
675
|
-
* method: afterRecomputation(dataUpdate, (data) => data),
|
|
676
|
-
* loader: async ({ params }) => {
|
|
677
|
-
* const response = await fetch(`/api/data/${params.id}`, {
|
|
678
|
-
* method: 'PUT',
|
|
679
|
-
* body: JSON.stringify(params.payload),
|
|
680
|
-
* });
|
|
681
|
-
* return response.json();
|
|
682
|
-
* },
|
|
683
|
-
* }),
|
|
684
|
-
* }))
|
|
685
|
-
* );
|
|
686
|
-
*
|
|
687
|
-
* const store = injectCraft();
|
|
688
|
-
*
|
|
689
|
-
* // Data passed through unchanged
|
|
690
|
-
* store.setDataUpdate({ id: 'item-1', payload: { value: 123 } });
|
|
691
|
-
* // -> mutation receives exact same object
|
|
327
|
+
* // Data passed through unchanged
|
|
328
|
+
* store.setDataUpdate({ id: 'item-1', payload: { value: 123 } });
|
|
329
|
+
* // -> mutation receives exact same object
|
|
692
330
|
* ```
|
|
693
331
|
*/
|
|
694
|
-
declare function afterRecomputation<State, SourceType>(_source:
|
|
332
|
+
declare function afterRecomputation<State, SourceType>(_source: ReadonlySource<SourceType>, callback: (source: SourceType) => State): ReadonlySource<State>;
|
|
695
333
|
|
|
696
334
|
type ContextConstraints = {
|
|
697
335
|
props: {};
|
|
@@ -2167,6 +1805,368 @@ declare function craft<outputs1 extends ContextConstraints, standaloneOutputs1 e
|
|
|
2167
1805
|
implements?: ToImplementContract;
|
|
2168
1806
|
}>;
|
|
2169
1807
|
|
|
1808
|
+
type SignalSource<T> = Signal<T | undefined> & {
|
|
1809
|
+
set: (value: T) => void;
|
|
1810
|
+
preserveLastValue: Signal<T | undefined>;
|
|
1811
|
+
} & SourceBranded;
|
|
1812
|
+
/**
|
|
1813
|
+
* Creates a source for event-driven communication with lazy emission semantics.
|
|
1814
|
+
*
|
|
1815
|
+
* Sources are the foundation of event-driven patterns in ng-craft, enabling:
|
|
1816
|
+
* - Discrete event emissions (unlike continuous signals)
|
|
1817
|
+
* - Lazy behavior (undefined until explicitly set)
|
|
1818
|
+
* - Decoupled communication between components and stores
|
|
1819
|
+
* - Automatic triggering of queries, mutations, and async methods
|
|
1820
|
+
* - Multi-listener support with independent subscription timing
|
|
1821
|
+
*
|
|
1822
|
+
* @remarks
|
|
1823
|
+
* **Core Concept:**
|
|
1824
|
+
* Sources implement event emitter pattern with reactive semantics:
|
|
1825
|
+
* - Emit only when explicitly set (not on every read like signals)
|
|
1826
|
+
* - Listeners receive `undefined` on first read (lazy semantics)
|
|
1827
|
+
* - New listeners don't receive previous emissions by default
|
|
1828
|
+
* - Use `preserveLastValue` to get the last emitted value immediately
|
|
1829
|
+
*
|
|
1830
|
+
* **Difference from Signals:**
|
|
1831
|
+
* - **Signals**: Always have a value, recompute on access, continuous state
|
|
1832
|
+
* - **Sources**: Emit on explicit set, lazy by default, discrete events
|
|
1833
|
+
* - Sources are for events/actions, signals are for state
|
|
1834
|
+
*
|
|
1835
|
+
* **Use Cases:**
|
|
1836
|
+
* - **User actions**: Button clicks, form submissions, custom events
|
|
1837
|
+
* - **Navigation events**: Route changes, tab switches
|
|
1838
|
+
* - **Data events**: Reload triggers, refresh requests
|
|
1839
|
+
* - **Coordination**: Communication between disconnected components
|
|
1840
|
+
* - **Store inputs**: Triggering queries/mutations from components
|
|
1841
|
+
* - **Event buses**: Decoupled event communication
|
|
1842
|
+
*
|
|
1843
|
+
* **Integration with Queries/Mutations:**
|
|
1844
|
+
* - Bind to method using `afterRecomputation(source, callback)`
|
|
1845
|
+
* - Query/mutation executes automatically when source emits
|
|
1846
|
+
* - No manual method exposed (source-based triggering)
|
|
1847
|
+
*
|
|
1848
|
+
* **Listener Semantics:**
|
|
1849
|
+
* - **Standard listener**: Returns `undefined` until source emits, then returns new values only
|
|
1850
|
+
* - **preserveLastValue**: Returns last emitted value immediately, then tracks new values
|
|
1851
|
+
* - Useful for late subscribers that need current state
|
|
1852
|
+
*
|
|
1853
|
+
* **Limitations:**
|
|
1854
|
+
* Sources are signals and behave differently from observables.
|
|
1855
|
+
* Understanding these three key limitations is important:
|
|
1856
|
+
* - **Multiple sets in same cycle**: When a source is set multiple times during the same cycle
|
|
1857
|
+
* (between the first set and the Change Detection that executes all consumer callbacks),
|
|
1858
|
+
* consumers will only react once during CD and will only see the last set value.
|
|
1859
|
+
* Intermediate values are discarded.
|
|
1860
|
+
* - **Multiple sources order**: Within the same cycle, if multiple sources are triggered,
|
|
1861
|
+
* consumers cannot determine the order in which the sources were set.
|
|
1862
|
+
* The original emission sequence is not preserved.
|
|
1863
|
+
* - **Consumer execution order**: When multiple sources are triggered in the same cycle,
|
|
1864
|
+
* consumer callbacks are invoked in the order they were declared, not in the order
|
|
1865
|
+
* their source producers were triggered.
|
|
1866
|
+
* - **No synchronous intermediate value reactions**: Unlike observables, sources cannot react
|
|
1867
|
+
* to each intermediate value synchronously. A mechanism similar to observables
|
|
1868
|
+
* (or using native Observable API) without RxJS is being considered to enable
|
|
1869
|
+
* synchronous reactions to intermediate values, matching the behavior currently
|
|
1870
|
+
* offered by observables.
|
|
1871
|
+
*
|
|
1872
|
+
* @template T - The type of values emitted by the source
|
|
1873
|
+
*
|
|
1874
|
+
* @param options - Optional configuration:
|
|
1875
|
+
* - `equal`: Custom equality function for change detection (prevents duplicate emissions)
|
|
1876
|
+
* - `debugName`: Name for debugging purposes
|
|
1877
|
+
*
|
|
1878
|
+
* @returns A source object with:
|
|
1879
|
+
* - `()`: Read current value (undefined until first emission)
|
|
1880
|
+
* - `set(value)`: Emit a value to all listeners
|
|
1881
|
+
* - `preserveLastValue`: Alternative signal that returns last value immediately
|
|
1882
|
+
*
|
|
1883
|
+
* @example
|
|
1884
|
+
* Basic source for user actions
|
|
1885
|
+
* ```ts
|
|
1886
|
+
* const { injectCraft } = craft(
|
|
1887
|
+
* { name: '', providedIn: 'root' },
|
|
1888
|
+
* craftSources({
|
|
1889
|
+
* loadUser: source<string>(),
|
|
1890
|
+
* }),
|
|
1891
|
+
* craftQuery('user', ({ loadUser }) =>
|
|
1892
|
+
* query({
|
|
1893
|
+
* method: afterRecomputation(loadUser, (userId) => userId),
|
|
1894
|
+
* loader: async ({ params }) => {
|
|
1895
|
+
* const response = await fetch(`/api/users/${params}`);
|
|
1896
|
+
* return response.json();
|
|
1897
|
+
* },
|
|
1898
|
+
* })
|
|
1899
|
+
* )
|
|
1900
|
+
* );
|
|
1901
|
+
*
|
|
1902
|
+
* const store = injectCraft();
|
|
1903
|
+
*
|
|
1904
|
+
* // Query executes automatically when source emits
|
|
1905
|
+
* store.setLoadUser('user-123');
|
|
1906
|
+
* // -> loadUser source emits 'user-123'
|
|
1907
|
+
* // -> user query executes with params 'user-123'
|
|
1908
|
+
*
|
|
1909
|
+
* store.setLoadUser('user-456');
|
|
1910
|
+
* // -> user query executes again with params 'user-456'
|
|
1911
|
+
* ```
|
|
1912
|
+
*
|
|
1913
|
+
* @example
|
|
1914
|
+
* Source for form submission
|
|
1915
|
+
* ```ts
|
|
1916
|
+
* type FormData = { name: string; email: string };
|
|
1917
|
+
*
|
|
1918
|
+
* const { injectCraft } = craft(
|
|
1919
|
+
* { name: '', providedIn: 'root' },
|
|
1920
|
+
* craftSources({
|
|
1921
|
+
* submitForm: source<FormData>(),
|
|
1922
|
+
* }),
|
|
1923
|
+
* craftMutations(({ submitForm }) => ({
|
|
1924
|
+
* submit: mutation({
|
|
1925
|
+
* method: afterRecomputation(submitForm, (data) => data),
|
|
1926
|
+
* loader: async ({ params }) => {
|
|
1927
|
+
* const response = await fetch('/api/submit', {
|
|
1928
|
+
* method: 'POST',
|
|
1929
|
+
* body: JSON.stringify(params),
|
|
1930
|
+
* });
|
|
1931
|
+
* return response.json();
|
|
1932
|
+
* },
|
|
1933
|
+
* }),
|
|
1934
|
+
* }))
|
|
1935
|
+
* );
|
|
1936
|
+
*
|
|
1937
|
+
* const store = injectCraft();
|
|
1938
|
+
*
|
|
1939
|
+
* // In component template:
|
|
1940
|
+
* // <form (submit)="onSubmit()">
|
|
1941
|
+
* // <input name="name" [(ngModel)]="formData.name" />
|
|
1942
|
+
* // <input name="email" [(ngModel)]="formData.email" />
|
|
1943
|
+
* // </form>
|
|
1944
|
+
*
|
|
1945
|
+
* onSubmit() {
|
|
1946
|
+
* // Mutation executes automatically
|
|
1947
|
+
* this.store.setSubmitForm(this.formData);
|
|
1948
|
+
* // -> submitForm source emits
|
|
1949
|
+
* // -> submit mutation executes
|
|
1950
|
+
* }
|
|
1951
|
+
* ```
|
|
1952
|
+
*
|
|
1953
|
+
* @example
|
|
1954
|
+
* Source for reload/refresh actions
|
|
1955
|
+
* ```ts
|
|
1956
|
+
* const { injectCraft } = craft(
|
|
1957
|
+
* { name: '', providedIn: 'root' },
|
|
1958
|
+
* craftSources({
|
|
1959
|
+
* reload: source<void>(),
|
|
1960
|
+
* }),
|
|
1961
|
+
* craftQuery('data', ({ reload }) =>
|
|
1962
|
+
* query(
|
|
1963
|
+
* {
|
|
1964
|
+
* params: () => ({}),
|
|
1965
|
+
* loader: async () => {
|
|
1966
|
+
* const response = await fetch('/api/data');
|
|
1967
|
+
* return response.json();
|
|
1968
|
+
* },
|
|
1969
|
+
* },
|
|
1970
|
+
* insertReloadOnSource(reload)
|
|
1971
|
+
* )
|
|
1972
|
+
* )
|
|
1973
|
+
* );
|
|
1974
|
+
*
|
|
1975
|
+
* const store = injectCraft();
|
|
1976
|
+
*
|
|
1977
|
+
* // Trigger reload from anywhere
|
|
1978
|
+
* store.setReload();
|
|
1979
|
+
* // -> reload source emits
|
|
1980
|
+
* // -> query reloads
|
|
1981
|
+
*
|
|
1982
|
+
* // In component:
|
|
1983
|
+
* // <button (click)="store.setReload()">Refresh</button>
|
|
1984
|
+
* ```
|
|
1985
|
+
*
|
|
1986
|
+
* @example
|
|
1987
|
+
* Multiple sources for different actions
|
|
1988
|
+
* ```ts
|
|
1989
|
+
* const { injectCraft } = craft(
|
|
1990
|
+
* { name: '', providedIn: 'root' },
|
|
1991
|
+
* craftSources({
|
|
1992
|
+
* addTodo: source<{ text: string }>(),
|
|
1993
|
+
* deleteTodo: source<string>(),
|
|
1994
|
+
* toggleTodo: source<string>(),
|
|
1995
|
+
* }),
|
|
1996
|
+
* craftMutations(({ addTodo, deleteTodo, toggleTodo }) => ({
|
|
1997
|
+
* create: mutation({
|
|
1998
|
+
* method: afterRecomputation(addTodo, (data) => data),
|
|
1999
|
+
* loader: async ({ params }) => {
|
|
2000
|
+
* const response = await fetch('/api/todos', {
|
|
2001
|
+
* method: 'POST',
|
|
2002
|
+
* body: JSON.stringify(params),
|
|
2003
|
+
* });
|
|
2004
|
+
* return response.json();
|
|
2005
|
+
* },
|
|
2006
|
+
* }),
|
|
2007
|
+
* delete: mutation({
|
|
2008
|
+
* method: afterRecomputation(deleteTodo, (id) => id),
|
|
2009
|
+
* loader: async ({ params }) => {
|
|
2010
|
+
* await fetch(`/api/todos/${params}`, { method: 'DELETE' });
|
|
2011
|
+
* return { deleted: true };
|
|
2012
|
+
* },
|
|
2013
|
+
* }),
|
|
2014
|
+
* toggle: mutation({
|
|
2015
|
+
* method: afterRecomputation(toggleTodo, (id) => id),
|
|
2016
|
+
* loader: async ({ params }) => {
|
|
2017
|
+
* const response = await fetch(`/api/todos/${params}/toggle`, {
|
|
2018
|
+
* method: 'PATCH',
|
|
2019
|
+
* });
|
|
2020
|
+
* return response.json();
|
|
2021
|
+
* },
|
|
2022
|
+
* }),
|
|
2023
|
+
* }))
|
|
2024
|
+
* );
|
|
2025
|
+
*
|
|
2026
|
+
* const store = injectCraft();
|
|
2027
|
+
*
|
|
2028
|
+
* // Different actions trigger different mutations
|
|
2029
|
+
* store.setAddTodo({ text: 'Buy milk' });
|
|
2030
|
+
* store.setToggleTodo('todo-123');
|
|
2031
|
+
* store.setDeleteTodo('todo-456');
|
|
2032
|
+
* ```
|
|
2033
|
+
*
|
|
2034
|
+
* @example
|
|
2035
|
+
* Late listener with preserveLastValue
|
|
2036
|
+
* ```ts
|
|
2037
|
+
* const mySource = source<string>();
|
|
2038
|
+
*
|
|
2039
|
+
* // Early listener
|
|
2040
|
+
* const listener1 = computed(() => mySource());
|
|
2041
|
+
* console.log(listener1()); // undefined
|
|
2042
|
+
*
|
|
2043
|
+
* // Emit value
|
|
2044
|
+
* mySource.set('Hello');
|
|
2045
|
+
* console.log(listener1()); // 'Hello'
|
|
2046
|
+
*
|
|
2047
|
+
* // Late listener (after emission)
|
|
2048
|
+
* const listener2 = computed(() => mySource());
|
|
2049
|
+
* console.log(listener2()); // undefined (doesn't get previous emission)
|
|
2050
|
+
*
|
|
2051
|
+
* mySource.set('World');
|
|
2052
|
+
* console.log(listener1()); // 'World'
|
|
2053
|
+
* console.log(listener2()); // 'World'
|
|
2054
|
+
*
|
|
2055
|
+
* // Using preserveLastValue for late listeners
|
|
2056
|
+
* const listener3 = computed(() => mySource.preserveLastValue());
|
|
2057
|
+
* console.log(listener3()); // 'World' (gets last value immediately)
|
|
2058
|
+
* ```
|
|
2059
|
+
*
|
|
2060
|
+
* @example
|
|
2061
|
+
* Custom equality to prevent duplicate emissions
|
|
2062
|
+
* ```ts
|
|
2063
|
+
* type Params = { id: string; timestamp: number };
|
|
2064
|
+
*
|
|
2065
|
+
* const paramsSource = source<Params>({
|
|
2066
|
+
* equal: (a, b) => a?.id === b?.id, // Compare only by id
|
|
2067
|
+
* });
|
|
2068
|
+
*
|
|
2069
|
+
* const listener = computed(() => paramsSource());
|
|
2070
|
+
*
|
|
2071
|
+
* paramsSource.set({ id: 'item-1', timestamp: Date.now() });
|
|
2072
|
+
* // -> listener receives value
|
|
2073
|
+
*
|
|
2074
|
+
* paramsSource.set({ id: 'item-1', timestamp: Date.now() });
|
|
2075
|
+
* // -> listener does NOT receive value (same id)
|
|
2076
|
+
*
|
|
2077
|
+
* paramsSource.set({ id: 'item-2', timestamp: Date.now() });
|
|
2078
|
+
* // -> listener receives value (different id)
|
|
2079
|
+
* ```
|
|
2080
|
+
*
|
|
2081
|
+
* @example
|
|
2082
|
+
* Source for coordinating multiple components
|
|
2083
|
+
* ```ts
|
|
2084
|
+
* // Global source (outside component)
|
|
2085
|
+
* const refreshAllSource = source<void>();
|
|
2086
|
+
*
|
|
2087
|
+
* // Component A
|
|
2088
|
+
* @Component({
|
|
2089
|
+
* selector: 'app-data-view',
|
|
2090
|
+
* template: '...',
|
|
2091
|
+
* })
|
|
2092
|
+
* export class DataViewComponent {
|
|
2093
|
+
* { injectCraft } = craft(
|
|
2094
|
+
* { name: '', providedIn: 'root' },
|
|
2095
|
+
* craftQuery('data', () =>
|
|
2096
|
+
* query(
|
|
2097
|
+
* {
|
|
2098
|
+
* params: () => ({}),
|
|
2099
|
+
* loader: async () => {
|
|
2100
|
+
* const response = await fetch('/api/data');
|
|
2101
|
+
* return response.json();
|
|
2102
|
+
* },
|
|
2103
|
+
* },
|
|
2104
|
+
* insertReloadOnSource(refreshAllSource)
|
|
2105
|
+
* )
|
|
2106
|
+
* )
|
|
2107
|
+
* );
|
|
2108
|
+
*
|
|
2109
|
+
* store = this.injectCraft();
|
|
2110
|
+
* }
|
|
2111
|
+
*
|
|
2112
|
+
* // Component B
|
|
2113
|
+
* @Component({
|
|
2114
|
+
* selector: 'app-refresh-button',
|
|
2115
|
+
* template: '<button (click)="refresh()">Refresh All</button>',
|
|
2116
|
+
* })
|
|
2117
|
+
* export class RefreshButtonComponent {
|
|
2118
|
+
* refresh() {
|
|
2119
|
+
* // Triggers refresh in all components listening to this source
|
|
2120
|
+
* refreshAllSource.set();
|
|
2121
|
+
* }
|
|
2122
|
+
* }
|
|
2123
|
+
* ```
|
|
2124
|
+
*
|
|
2125
|
+
* @example
|
|
2126
|
+
* Source with complex payload
|
|
2127
|
+
* ```ts
|
|
2128
|
+
* type SearchParams = {
|
|
2129
|
+
* query: string;
|
|
2130
|
+
* filters: string[];
|
|
2131
|
+
* page: number;
|
|
2132
|
+
* };
|
|
2133
|
+
*
|
|
2134
|
+
* const { injectCraft } = craft(
|
|
2135
|
+
* { name: '', providedIn: 'root' },
|
|
2136
|
+
* craftSources({
|
|
2137
|
+
* search: source<SearchParams>(),
|
|
2138
|
+
* }),
|
|
2139
|
+
* craftQuery('results', ({ search }) =>
|
|
2140
|
+
* query({
|
|
2141
|
+
* method: afterRecomputation(search, (params) => params),
|
|
2142
|
+
* loader: async ({ params }) => {
|
|
2143
|
+
* const queryString = new URLSearchParams({
|
|
2144
|
+
* q: params.query,
|
|
2145
|
+
* filters: params.filters.join(','),
|
|
2146
|
+
* page: String(params.page),
|
|
2147
|
+
* });
|
|
2148
|
+
* const response = await fetch(`/api/search?${queryString}`);
|
|
2149
|
+
* return response.json();
|
|
2150
|
+
* },
|
|
2151
|
+
* })
|
|
2152
|
+
* )
|
|
2153
|
+
* );
|
|
2154
|
+
*
|
|
2155
|
+
* const store = injectCraft();
|
|
2156
|
+
*
|
|
2157
|
+
* // Emit complex search parameters
|
|
2158
|
+
* store.setSearch({
|
|
2159
|
+
* query: 'angular',
|
|
2160
|
+
* filters: ['tutorial', 'advanced'],
|
|
2161
|
+
* page: 1,
|
|
2162
|
+
* });
|
|
2163
|
+
* ```
|
|
2164
|
+
*/
|
|
2165
|
+
declare function signalSource<T>(options?: {
|
|
2166
|
+
equal?: ValueEqualityFn<NoInfer<T> | undefined>;
|
|
2167
|
+
debugName?: string;
|
|
2168
|
+
}): SignalSource<T>;
|
|
2169
|
+
|
|
2170
2170
|
type ExtractSignalPropsAndMethods<State, StateKeysTuple, Acc extends {
|
|
2171
2171
|
props: {};
|
|
2172
2172
|
methods: Record<string, Function>;
|