@dvirus-js/angular-signals 0.0.1

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.
@@ -0,0 +1,2192 @@
1
+ import { signal, isSignal, inject, Injector, DestroyRef, effect, untracked, computed } from '@angular/core';
2
+ import { FormGroup, FormArray } from '@angular/forms';
3
+ import { startWith } from 'rxjs';
4
+
5
+ /**
6
+ * Converts a plain object into a writable signal object.
7
+ * Each property of the source object becomes a WritableSignal initialized with the property's value.
8
+ *
9
+ * @param src - The source object to convert.
10
+ * @returns An object with the same keys as the source, but with values wrapped in WritableSignals.
11
+ */
12
+ function toSignalObj(src) {
13
+ return Object.entries(src).reduce((prev, [key, value]) => ({ ...prev, [key]: signal(value) }), {});
14
+ }
15
+ /**
16
+ * Converts a signal object into a plain object.
17
+ * Each property of the signal object becomes a value from the signal.
18
+ *
19
+ * @param src - The signal object to convert.
20
+ * @returns An object with the same keys as the source, but with values from the signals.
21
+ */
22
+ function fromSignalObj(src) {
23
+ return Object.entries(src).reduce((prev, [key, $value]) => ({ ...prev, [key]: signalOrValue($value) }), {});
24
+ }
25
+ /**
26
+ * Unwraps a value that might be a Signal.
27
+ * If the input is a Signal, it returns the signal's value.
28
+ * If the input is a raw value, it returns the value itself.
29
+ *
30
+ * @param value - The signal or value to unwrap.
31
+ * @returns The raw value of type T.
32
+ */
33
+ function signalOrValue(value) {
34
+ return isSignal(value) ? value() : value;
35
+ }
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ function signalOrFunction(value, ...fnArgs) {
38
+ let res = value;
39
+ if (isSignal(res)) {
40
+ res = res();
41
+ }
42
+ if (typeof res == 'function') {
43
+ res = res(...fnArgs);
44
+ }
45
+ return res;
46
+ }
47
+ // function main() {
48
+ // const signalOrValue1: SignalOrValue<number> = signal(42);
49
+ // const signalOrValue2: SignalOrValue<string> = 'Hello, World!';
50
+ // console.log(signalOrValue(signalOrValue1)); // 42
51
+ // console.log(signalOrValue(signalOrValue2)); // "Hello, World!"
52
+ // // #############################################################
53
+ // const signalOrFunction1: SignalOrValue<number> | (() => number) = signal(100);
54
+ // const signalOrFunction2: SignalOrValue<number> | (() => number) = () => 200;
55
+ // const signalOrFunction3: SignalOrValue<number> | (() => number) = 300;
56
+ // const signalOrFunction4: SignalOrValue<number> | ((a: number, b: number) => number) = (a, b) => {
57
+ // return a + b;
58
+ // };
59
+ // console.log(signalOrFunction(signalOrFunction1)); // 100
60
+ // console.log(signalOrFunction(signalOrFunction2)); // 200
61
+ // console.log(signalOrFunction(signalOrFunction3)); // 300
62
+ // console.log(signalOrFunction(signalOrFunction4, 5, 10)); // 15
63
+ // // #############################################################
64
+ // const signalObj1: SignalObj<{ a: number; b: string }> = {
65
+ // a: signal(10),
66
+ // b: computed(() => 'Test'),
67
+ // };
68
+ // console.log(signalObj1); // { a: Signal<number>, b: Signal<string> }
69
+ // const signalObjWritable1: SignalObjWritable<{ a: number; b: string }> = toSignalObj({
70
+ // a: 10,
71
+ // b: 'Test',
72
+ // });
73
+ // console.log(signalObjWritable1); // { a: WritableSignal<number>, b: WritableSignal<string> }
74
+ // const plainObj1 = fromSignalObj(signalObj1);
75
+ // console.log(plainObj1); // { a: 10, b: 'Test' }
76
+ // const plainObj2 = fromSignalObj(signalObjWritable1);
77
+ // console.log(plainObj2); // { a: 10, b: 'Test' }
78
+ // }
79
+
80
+ /**
81
+ * Normalizes a validation error object by filtering out empty/null values.
82
+ *
83
+ * Converts a raw validation result into a clean error map containing only
84
+ * non-empty string messages. Filters out undefined, null, and empty strings.
85
+ *
86
+ * @param error - Raw validation error object or null
87
+ * @returns Clean error map with only valid error messages
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * normalizeValidationError({ required: 'Error', empty: '', invalid: null });
92
+ * // Returns: { required: 'Error' }
93
+ * ```
94
+ */
95
+ function normalizeValidationError(error) {
96
+ if (!error)
97
+ return {};
98
+ return Object.entries(error).reduce((acc, [key, val]) => {
99
+ if (typeof val === 'string' && val.length > 0) {
100
+ acc[key] = val;
101
+ }
102
+ return acc;
103
+ }, {});
104
+ }
105
+ /**
106
+ * Executes an array of validators and collects all error messages.
107
+ *
108
+ * Runs each validator function with the provided context and merges all
109
+ * error messages into a single error map. Empty/null results are filtered out.
110
+ *
111
+ * @template TControls - Object type defining available sibling controls
112
+ * @template TValue - The type of value being validated
113
+ *
114
+ * @param validators - Array of validator functions to execute
115
+ * @param ctx - Validation context with current value and control accessor
116
+ * @returns Combined error map from all validators
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * const errors = collectValidationErrors(
121
+ * [signalFormValidators.required, signalFormValidators.minLength(3)],
122
+ * { item: { value: '' }, getControl: () => {} }
123
+ * );
124
+ * // Returns: { required: 'This field is required' }
125
+ * ```
126
+ */
127
+ function collectValidationErrors(validators, ctx) {
128
+ if (!validators?.length)
129
+ return {};
130
+ return validators.reduce((acc, validator) => {
131
+ const normalized = normalizeValidationError(validator(ctx));
132
+ Object.entries(normalized).forEach(([key, val]) => {
133
+ acc[key] = val;
134
+ });
135
+ return acc;
136
+ }, {});
137
+ }
138
+ /**
139
+ * Checks if an error map contains any errors.
140
+ *
141
+ * Simple utility to determine if a control has validation errors by
142
+ * checking if the error map object has any keys.
143
+ *
144
+ * @param errorMap - Error map to check
145
+ * @returns True if there are any errors, false if empty
146
+ *
147
+ * @example
148
+ * ```typescript
149
+ * hasErrors({ required: 'Error' }); // true
150
+ * hasErrors({}); // false
151
+ * ```
152
+ */
153
+ function hasErrors(errorMap) {
154
+ return Object.keys(errorMap).length > 0;
155
+ }
156
+ /**
157
+ * Recursively checks if an error tree structure is completely empty.
158
+ *
159
+ * Traverses nested error structures (objects and arrays) to determine if
160
+ * there are any actual error messages anywhere in the tree. Returns true
161
+ * only when the entire structure contains no errors.
162
+ *
163
+ * @param value - Error tree to check (can be nested objects/arrays)
164
+ * @returns True if no errors exist anywhere in the tree
165
+ *
166
+ * @example
167
+ * ```typescript
168
+ * isEmptyErrorTree({ name: undefined, address: { street: undefined } }); // true
169
+ * isEmptyErrorTree({ name: { required: 'Error' } }); // false
170
+ * isEmptyErrorTree([undefined, undefined]); // true
171
+ * ```
172
+ */
173
+ function isEmptyErrorTree(value) {
174
+ if (value === null || value === undefined)
175
+ return true;
176
+ if (Array.isArray(value)) {
177
+ return value.every(isEmptyErrorTree);
178
+ }
179
+ if (typeof value === 'object') {
180
+ return Object.values(value).every(isEmptyErrorTree);
181
+ }
182
+ return false;
183
+ }
184
+
185
+ /**
186
+ * Creates a signal-based notifier function.
187
+ *
188
+ * Each call to the returned function returns the current notification count.
189
+ * Calling `notify()` increments the count, triggering any listeners.
190
+ *
191
+ * @returns {SignalNotifier} A notifier function with a `notify` method.
192
+ *
193
+ * @example
194
+ * const notifier = signalNotifier();
195
+ * effect(() => {
196
+ * notifier(); // Reacts to notifications
197
+ * });
198
+ * notifier.notify(); // Triggers the effect
199
+ */
200
+ function signalNotifier() {
201
+ const _signal = signal(0, ...(ngDevMode ? [{ debugName: "_signal" }] : []));
202
+ function get() {
203
+ return _signal();
204
+ }
205
+ get.notify = () => _signal.update((n) => n + 1);
206
+ return get;
207
+ }
208
+
209
+ /**
210
+ * Main wrapper function to handle promise with try-catch
211
+ * @template T - Type of the successful result
212
+ * @template E - Type of the error, defaults to Error
213
+ * @param {()=> T} fn - The function to handle
214
+ * @returns {TryResult<T, E>} - A tuple with either the result or the error
215
+ */
216
+ function tryCatch(fn) {
217
+ try {
218
+ const val = fn();
219
+ return [val, null];
220
+ }
221
+ catch (error) {
222
+ return [null, error];
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Creates a debounced writable signal.
228
+ *
229
+ * The returned signal can be written to instantly via its `WritableSignal` interface
230
+ * **or** through `setDebounced(value)` which delays the commit by `debounceTime` ms.
231
+ * While a debounced write is pending, `isLoading()` returns `true`.
232
+ *
233
+ * If a reactive `params` function is supplied, the signal will also track that
234
+ * source and debounce upstream changes (requires an injection context).
235
+ *
236
+ * @typeParam T - The type of the signal's value.
237
+ * @param options - Configuration object.
238
+ * @param options.params - Optional reactive source function whose return value
239
+ * is tracked and debounced into the signal.
240
+ * @param options.debounceTime - Delay in milliseconds before a debounced value
241
+ * is committed.
242
+ * @param options.initialValue - Optional initial value for the signal.
243
+ * @param options.injector - Optional Angular `Injector` to use for setting up
244
+ * reactive tracking. Required if `params` is provided and this function is called
245
+ * outside of an injection context.
246
+ * @returns A `SignalDebounce<T>` instance.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * // Simple debounced signal with an initial value
251
+ * const search = signalDebounce<string>({ debounceTime: 300, initialValue: '' });
252
+ * search.setDebounced('hello'); // commits after 300 ms
253
+ *
254
+ * // Tracking a reactive source
255
+ * const query = signal('angular');
256
+ * const debounced = signalDebounce({ params: () => query(), debounceTime: 500 });
257
+ * ```
258
+ */
259
+ function signalDebounce(options) {
260
+ const timeout = signal(null, ...(ngDevMode ? [{ debugName: "timeout" }] : []));
261
+ const isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
262
+ const _sig = signal(options.initialValue);
263
+ const scheduleDebounce = (value) => {
264
+ clearPendingTimeout(timeout);
265
+ isLoading.set(true);
266
+ timeout.set(setTimeout(() => {
267
+ isLoading.set(false);
268
+ _sig.set(value);
269
+ }, options.debounceTime));
270
+ };
271
+ if (options.params) {
272
+ trackSource({
273
+ source: options.params,
274
+ scheduleDebounce,
275
+ timeout,
276
+ injector: options.injector,
277
+ });
278
+ }
279
+ return Object.assign(_sig, {
280
+ setDebounced: scheduleDebounce,
281
+ isLoading: isLoading.asReadonly(),
282
+ });
283
+ }
284
+ /**
285
+ * Clears a pending debounce timeout if one exists.
286
+ * @internal
287
+ */
288
+ function clearPendingTimeout(timeout) {
289
+ const t = timeout();
290
+ if (t)
291
+ clearTimeout(t);
292
+ }
293
+ /**
294
+ * Sets up an effect that tracks a reactive source and debounces its changes.
295
+ *
296
+ * Requires an Angular injection context. If called outside one, logs a warning
297
+ * and returns without setting up tracking (manual `setDebounced` still works).
298
+ *
299
+ * @typeParam T - The type of the source value.
300
+ * @param source - Reactive function to track.
301
+ * @param scheduleDebounce - Callback that schedules a debounced write.
302
+ * @param timeout - Shared timeout handle for cleanup on destroy.
303
+ * @internal
304
+ */
305
+ function trackSource({ source, scheduleDebounce, timeout, injector }) {
306
+ if (!isInInjectionContext() && !injector) {
307
+ console.error('Warning: signalDebounce is being used outside of an injection context. The debounced signal will not update based on the provided signal.\n\n', "Still can be used with manual value updates via setDebounced, but won't react to changes in the provided signal.");
308
+ return;
309
+ }
310
+ const _injector = injector ?? inject(Injector);
311
+ const destroyRef = _injector.get(DestroyRef) ?? inject(DestroyRef);
312
+ effect(() => {
313
+ const val = source();
314
+ untracked(() => scheduleDebounce(val));
315
+ }, { injector: _injector });
316
+ destroyRef.onDestroy(() => clearPendingTimeout(timeout));
317
+ }
318
+ /**
319
+ * Checks if the current execution context has access to Angular's dependency injection.
320
+ * @returns `true` when inside an injection context, `false` otherwise.
321
+ * @internal
322
+ */
323
+ function isInInjectionContext() {
324
+ const [, error] = tryCatch(() => inject(DestroyRef));
325
+ return !error;
326
+ }
327
+
328
+ function writableSignal(computationOrOptions, maybeOptions) {
329
+ // Determine if input is a function or options object
330
+ const isFunction = typeof computationOrOptions === 'function';
331
+ if (isFunction) {
332
+ const computation = computationOrOptions;
333
+ const opts = maybeOptions ?? {};
334
+ // Create a fresh token whenever the source recomputes, even if the value is equal.
335
+ const sourceState = computed(() => ({ value: computation() }), { ...(ngDevMode ? { debugName: "sourceState" } : {}), equal: (a, b) => opts.equal ? opts.equal(a.value, b.value) : a.value === b.value });
336
+ // Manual overrides are only valid for the source-state token they were set against.
337
+ const override = signal(undefined, ...(ngDevMode ? [{ debugName: "override" }] : []));
338
+ const result = computed(() => {
339
+ const state = sourceState();
340
+ const currentOverride = override();
341
+ if (currentOverride !== undefined && currentOverride.state === state) {
342
+ return currentOverride.value;
343
+ }
344
+ return state.value;
345
+ }, { ...(ngDevMode ? { debugName: "result" } : {}), equal: opts.equal });
346
+ return Object.assign(result, {
347
+ set(v) {
348
+ override.set({ state: untracked(sourceState), value: v });
349
+ },
350
+ update(updater) {
351
+ override.set({
352
+ state: untracked(sourceState),
353
+ value: updater(untracked(result)),
354
+ });
355
+ },
356
+ asReadonly() {
357
+ return result;
358
+ },
359
+ });
360
+ }
361
+ else {
362
+ const opts = computationOrOptions;
363
+ let previousState;
364
+ const sourceState = computed(() => ({ value: opts.source() }), ...(ngDevMode ? [{ debugName: "sourceState" }] : []));
365
+ const override = signal(undefined, ...(ngDevMode ? [{ debugName: "override" }] : []));
366
+ const result = computed(() => {
367
+ const state = sourceState();
368
+ const currentOverride = override();
369
+ if (currentOverride !== undefined &&
370
+ currentOverride.state === state) {
371
+ // Update previous state for next computation
372
+ previousState = { source: state.value, value: currentOverride.value };
373
+ return currentOverride.value;
374
+ }
375
+ const computedValue = opts.computation(state.value, previousState);
376
+ // Update previous state for next computation
377
+ previousState = { source: state.value, value: computedValue };
378
+ return computedValue;
379
+ }, { ...(ngDevMode ? { debugName: "result" } : {}), equal: opts.equal });
380
+ return Object.assign(result, {
381
+ set(v) {
382
+ override.set({
383
+ state: untracked(sourceState),
384
+ value: v,
385
+ });
386
+ },
387
+ update(updater) {
388
+ override.set({
389
+ state: untracked(sourceState),
390
+ value: updater(untracked(result)),
391
+ });
392
+ },
393
+ asReadonly() {
394
+ return result;
395
+ },
396
+ });
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Creates a reactive `SignalSet` backed by Angular signals.
402
+ *
403
+ * Every mutation creates a new `Set`, so Angular's signal equality check triggers updates.
404
+ *
405
+ * @template T The element type. Defaults to `number`.
406
+ * @param initialValue An optional iterable to seed the set with.
407
+ * @returns A {@link SignalSet} instance.
408
+ *
409
+ * @example
410
+ * ```ts
411
+ * const selected = signalSet<number>();
412
+ * selected.add(1);
413
+ * selected.toggle(2);
414
+ * console.log(selected.toArray()); // [1, 2]
415
+ * selected.toggle(1);
416
+ * console.log(selected.has(1)); // false
417
+ * ```
418
+ */
419
+ function signalSet(initialValue = new Set()) {
420
+ // writableSignal = linkedSignal
421
+ const setSignal = writableSignal(() => new Set(typeof initialValue === 'function' ? initialValue() : initialValue));
422
+ const toArray = computed(() => Array.from(setSignal()), ...(ngDevMode ? [{ debugName: "toArray" }] : []));
423
+ return Object.assign(() => setSignal(), {
424
+ toArray,
425
+ size: computed(() => setSignal().size),
426
+ toggle: (id) => {
427
+ setSignal.update((set) => {
428
+ const next = new Set(set);
429
+ if (next.has(id))
430
+ next.delete(id);
431
+ else
432
+ next.add(id);
433
+ return next;
434
+ });
435
+ },
436
+ has: (id) => setSignal().has(id),
437
+ add: (id) => {
438
+ setSignal.update((set) => {
439
+ const next = new Set(set);
440
+ next.add(id);
441
+ return next;
442
+ });
443
+ },
444
+ delete: (id) => {
445
+ setSignal.update((set) => {
446
+ const next = new Set(set);
447
+ next.delete(id);
448
+ return next;
449
+ });
450
+ },
451
+ clear: (values) => {
452
+ setSignal.set(new Set(values));
453
+ },
454
+ toString: () => {
455
+ return `SignalSet(${toArray().join(', ')})`;
456
+ },
457
+ toJSON: () => {
458
+ return toArray();
459
+ },
460
+ });
461
+ }
462
+
463
+ /**
464
+ * if an object have Symbol.iterator, treat it as Iterable<[K, V]>
465
+ * @param value Iterable<[K, V]> or Record<ToKey<K>, V>
466
+ * @returns Iterable<[K, V]>
467
+ */
468
+ function getIterable(value) {
469
+ if (Symbol.iterator in Object(value)) {
470
+ return value;
471
+ }
472
+ else {
473
+ return Object.entries(value);
474
+ }
475
+ }
476
+ /**
477
+ * Creates a reactive `SignalMap` backed by Angular signals.
478
+ *
479
+ * Every mutation creates a new `Map`, so Angular's signal equality check triggers updates.
480
+ * Accepts either an iterable of `[key, value]` pairs or a plain object as the initial value.
481
+ *
482
+ * @template K The key type. Defaults to `string`.
483
+ * @template V The value type. Defaults to `unknown`.
484
+ * @param initialValue An optional iterable of entries or a plain object to seed the map.
485
+ * @returns A {@link SignalMap} instance.
486
+ *
487
+ * @example
488
+ * ```ts
489
+ * const cache = signalMap<string, number>({ a: 1, b: 2 });
490
+ * cache.set('c', 3);
491
+ * console.log(cache.keys()); // ['a', 'b', 'c']
492
+ * cache.delete('a'); // true
493
+ * console.log(cache.toJSON()); // {b:2,c:3}
494
+ * ```
495
+ */
496
+ function signalMap(initialValue = new Map()) {
497
+ // writableSignal = linkedSignal
498
+ const mapSignal = writableSignal(() => {
499
+ const _val = typeof initialValue === 'function' ? initialValue() : initialValue;
500
+ const init = getIterable(_val);
501
+ return new Map(init);
502
+ });
503
+ const entries = computed(() => Array.from(mapSignal().entries()), ...(ngDevMode ? [{ debugName: "entries" }] : []));
504
+ const keys = computed(() => Array.from(mapSignal().keys()), ...(ngDevMode ? [{ debugName: "keys" }] : []));
505
+ const values = computed(() => Array.from(mapSignal().values()), ...(ngDevMode ? [{ debugName: "values" }] : []));
506
+ const size = computed(() => mapSignal().size, ...(ngDevMode ? [{ debugName: "size" }] : []));
507
+ return Object.assign(() => mapSignal(), {
508
+ entries,
509
+ keys,
510
+ values,
511
+ size,
512
+ get: (key) => mapSignal().get(key),
513
+ has: (key) => mapSignal().has(key),
514
+ set: (key, value) => {
515
+ mapSignal.update((map) => {
516
+ const next = new Map(map);
517
+ next.set(key, value);
518
+ return next;
519
+ });
520
+ return mapSignal();
521
+ },
522
+ delete: (key) => {
523
+ let deleted = false;
524
+ mapSignal.update((map) => {
525
+ const next = new Map(map);
526
+ deleted = next.delete(key);
527
+ return next;
528
+ });
529
+ return deleted;
530
+ },
531
+ clear: (values) => {
532
+ mapSignal.set(new Map(values ? getIterable(values) : undefined));
533
+ },
534
+ toString: () => {
535
+ return `SignalMap(${entries()
536
+ .map(([k, v]) => `${k} => ${v}`)
537
+ .join(', ')})`;
538
+ },
539
+ toJSON: () => {
540
+ return Object.fromEntries(entries());
541
+ },
542
+ toObject: () => {
543
+ return Object.fromEntries(entries());
544
+ },
545
+ });
546
+ }
547
+
548
+ const SIGNAL_OBJECT = Symbol('SignalObject');
549
+ /**
550
+ * A reactive object backed by Angular signals.
551
+ * Each property is stored as a WritableSignal, so reads are tracked
552
+ * by Angular's reactive system (templates, computed, effect).
553
+ *
554
+ * Usage:
555
+ * const person = signalObject({ name: 'dvirus', age: 30 });
556
+ * person.name; // reads the signal → 'dvirus' (tracked)
557
+ * person['name'] = 'new'; // sets the signal (triggers reactivity)
558
+ * person.age; // reads the signal → 30 (tracked)
559
+ */
560
+ class _SignalObject {
561
+ [SIGNAL_OBJECT] = true;
562
+ #signals = new Map();
563
+ #version = signal(0, ...(ngDevMode ? [{ debugName: "#version" }] : []));
564
+ #boundCache = new Map();
565
+ constructor(initialValue) {
566
+ for (const [key, value] of Object.entries(initialValue)) {
567
+ this.#signals.set(key, signal(value));
568
+ }
569
+ // Use a function as the proxy target so the `apply` trap works (makes it callable).
570
+ // All traps delegate to `self` (the real class instance) for property/signal access.
571
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
572
+ const self = this;
573
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
574
+ const callable = (() => { });
575
+ return new Proxy(callable, {
576
+ apply() {
577
+ return self.$snapshot();
578
+ },
579
+ get(_, prop) {
580
+ // Symbols & class own members go through normally
581
+ if (typeof prop === 'symbol' || prop in self) {
582
+ const value = Reflect.get(self, prop, self);
583
+ // Bind methods to the real target so private fields (#signals) work.
584
+ // Cache the bound function so the same reference is returned each time
585
+ // (important for Angular signal identity tracking on $snapshot).
586
+ if (typeof value === 'function') {
587
+ let bound = self.#boundCache.get(prop);
588
+ if (!bound) {
589
+ bound = value.bind(self);
590
+ self.#boundCache.set(prop, bound);
591
+ }
592
+ return bound;
593
+ }
594
+ return value;
595
+ }
596
+ // Read the signal — this is tracked by Angular's reactive context
597
+ return self.#signals.get(prop)?.();
598
+ },
599
+ set(_, prop, value) {
600
+ if (typeof prop === 'symbol' || prop in self) {
601
+ return Reflect.set(self, prop, value);
602
+ }
603
+ const existing = self.#signals.get(prop);
604
+ if (existing) {
605
+ existing.set(value);
606
+ }
607
+ else {
608
+ // Dynamic property — create a new signal and bump version
609
+ self.#signals.set(prop, signal(value));
610
+ self.#version.update((v) => v + 1);
611
+ }
612
+ return true;
613
+ },
614
+ has(_, prop) {
615
+ if (typeof prop === 'string' && self.#signals.has(prop)) {
616
+ return true;
617
+ }
618
+ return Reflect.has(self, prop);
619
+ },
620
+ ownKeys() {
621
+ return Array.from(self.#signals.keys());
622
+ },
623
+ getOwnPropertyDescriptor(_, prop) {
624
+ if (typeof prop === 'string' && self.#signals.has(prop)) {
625
+ return {
626
+ configurable: true,
627
+ enumerable: true,
628
+ writable: true,
629
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
630
+ value: self.#signals.get(prop)(),
631
+ };
632
+ }
633
+ return Reflect.getOwnPropertyDescriptor(self, prop);
634
+ },
635
+ });
636
+ }
637
+ /**
638
+ * A computed signal that returns a plain snapshot of all properties.
639
+ * Reading this tracks ALL properties — so any property change triggers reactivity.
640
+ *
641
+ * Usage:
642
+ * effect(() => console.log(person.$snapshot()));
643
+ * // logs whenever ANY property changes
644
+ */
645
+ $snapshot = computed(() => {
646
+ this.#version(); // track structural changes (new/removed props)
647
+ return this.toJSON();
648
+ }, ...(ngDevMode ? [{ debugName: "$snapshot" }] : []));
649
+ /**
650
+ * Spreads one or more objects (plain or reactive) into this SignalObject.
651
+ * Mimics `Object.assign(this, ...sources)` / `{ ...this, ...a, ...b }`.
652
+ *
653
+ * - Existing keys → updates the signal (triggers reactivity)
654
+ * - New keys → creates a new signal and bumps the version
655
+ * - Accepts plain objects and ReactiveObjects interchangeably
656
+ *
657
+ * @example
658
+ * const person = signalObject({ name: 'dvirus' });
659
+ * person.$assign({ age: 30 }, otherSignalObj);
660
+ */
661
+ $assign(...sources) {
662
+ for (const source of sources) {
663
+ const entries = isSignalObject(source)
664
+ ? Object.entries(source.$snapshot())
665
+ : Object.entries(source);
666
+ for (const [key, value] of entries) {
667
+ const existing = this.#signals.get(key);
668
+ if (existing) {
669
+ existing.set(value);
670
+ }
671
+ else {
672
+ this.#signals.set(key, signal(value));
673
+ }
674
+ }
675
+ }
676
+ this.#version.update((v) => v + 1);
677
+ }
678
+ toJSON() {
679
+ const result = {};
680
+ for (const [key, sig] of this.#signals) {
681
+ result[key] = sig();
682
+ }
683
+ return result;
684
+ }
685
+ toString() {
686
+ return JSON.stringify(this.toJSON());
687
+ }
688
+ }
689
+ /**
690
+ * Creates a reactive object backed by Angular signals.
691
+ *
692
+ * Every property is stored as a `WritableSignal`. Reading a property
693
+ * (e.g. `obj.name` or `obj['name']`) calls the signal — so it's
694
+ * automatically tracked in templates, `computed()`, and `effect()`.
695
+ * Setting a property calls `signal.set()`, which triggers reactivity.
696
+ *
697
+ * @param initialValue - The plain object to make reactive
698
+ * @returns A `SignalObject<T>` proxy with reactive property access, `$snapshot`, and `$assign`
699
+ *
700
+ * @example
701
+ * // In a component:
702
+ * protected person = signalObject({ name: 'dvirus', age: 30 });
703
+ *
704
+ * // In the template (reactive — updates automatically):
705
+ * // {{ person.name }}
706
+ *
707
+ * // In the class:
708
+ * person.name = 'changed'; // triggers re-render
709
+ * person['age'] = 31; // triggers re-render
710
+ *
711
+ * // Spread (plain snapshot):
712
+ * const copy = { ...person }; // { name: 'changed', age: 31 }
713
+ *
714
+ * // Track all properties reactively:
715
+ * effect(() => console.log(person.$snapshot()));
716
+ */
717
+ function signalObject(initialValue) {
718
+ return new _SignalObject(initialValue);
719
+ }
720
+ /**
721
+ * Type guard — checks if a value is a reactive SignalObject proxy.
722
+ *
723
+ * @example
724
+ * isSignalObject(signalObject({ a: 1 })); // true
725
+ * isSignalObject({ a: 1 }); // false
726
+ * isSignalObject({ ...signalObject({ a: 1 }) }); // false (spread = plain copy)
727
+ */
728
+ function isSignalObject(value) {
729
+ return (typeof value === 'object' &&
730
+ value !== null &&
731
+ value[SIGNAL_OBJECT] === true);
732
+ }
733
+ /**
734
+ * Reactively merges multiple SignalObjects (like `{ ...a, ...b }`).
735
+ * Returns a `Signal` that re-evaluates whenever any source property changes.
736
+ * Later sources win on key conflicts, just like spread.
737
+ *
738
+ * same as `computed(() => ({ ...a, ...b }))`
739
+ * but with proper tracking of nested properties and support for non-reactive objects as sources.
740
+ *
741
+ * @example
742
+ * const objA = signalObject({ name: 'dvirus', role: 'dev' });
743
+ * const objB = signalObject({ age: 30, role: 'admin' });
744
+ * const merged = mergeSignalObjects(objA, objB);
745
+ * merged(); // { name: 'dvirus', role: 'admin', age: 30 }
746
+ */
747
+ function mergeSignalObjects(...sources) {
748
+ return computed(() => {
749
+ let result = {};
750
+ for (const source of sources) {
751
+ // Spreading a SignalObject triggers its Proxy traps (ownKeys + getOwnPropertyDescriptor),
752
+ // which reads every signal — so it's automatically tracked by Angular's reactive context.
753
+ // Plain objects are just spread normally.
754
+ result = { ...result, ...source };
755
+ }
756
+ return result;
757
+ });
758
+ }
759
+
760
+ /**
761
+ * Creates a {@link ControlSignal} that mirrors an `AbstractControl`'s
762
+ * reactive state as Angular signals.
763
+ *
764
+ * **Important:** If called outside an injection context, you must manually handle
765
+ * un-subscription by either:
766
+ * - Calling the `unsubscribe()` method on the returned {@link ControlSignal}, or
767
+ * - Passing a `DestroyRef` via the `options` parameter for automatic cleanup.
768
+ *
769
+ * If called within an injection context without providing a `DestroyRef`, the
770
+ * subscriptions will be automatically cleaned up on component destruction.
771
+ *
772
+ * @template T - The value type of the form control.
773
+ * @param control - The form control or a signal wrapping one.
774
+ * @param options - Optional configuration object containing:
775
+ * - `destroyRef`: A `DestroyRef` used for automatic cleanup on destruction.
776
+ * If not provided, the function will attempt to inject one from the current
777
+ * injection context. If neither is available, manual un-subscription is required.
778
+ * @returns A {@link ControlSignal} exposing the control's state as signals.
779
+ * @throws If `control` is a signal and no `Injector` is available.
780
+ */
781
+ function controlSignal(control, options) {
782
+ const destroyRef = options?.destroyRef ??
783
+ tryCatch(() => inject(DestroyRef, { optional: true }))[0];
784
+ let subscriptions = [];
785
+ const unsubscribe = () => {
786
+ subscriptions.forEach((s) => s.unsubscribe());
787
+ subscriptions = [];
788
+ };
789
+ // cleanUp
790
+ unsubscribe();
791
+ destroyRef?.onDestroy(() => unsubscribe());
792
+ const $value = signal(undefined, ...(ngDevMode ? [{ debugName: "$value" }] : []));
793
+ const $status = signal(undefined, ...(ngDevMode ? [{ debugName: "$status" }] : []));
794
+ const _events = signal(null, ...(ngDevMode ? [{ debugName: "_events" }] : []));
795
+ const $events = writableSignal(() => {
796
+ $value();
797
+ $status();
798
+ return _events();
799
+ });
800
+ subscriptions.push(control.valueChanges
801
+ .pipe(startWith(control.value))
802
+ .subscribe((x) => untracked(() => $value.set(x))), control.statusChanges
803
+ .pipe(startWith(control.status))
804
+ .subscribe((x) => untracked(() => $status.set(x))));
805
+ // Subscribe to control.events if available (Angular v18+)
806
+ const ctrl = control;
807
+ if (ctrl.events && typeof ctrl.events.subscribe === 'function') {
808
+ subscriptions.push(ctrl.events.subscribe((x) => untracked(() => {
809
+ _events.set(x);
810
+ })));
811
+ }
812
+ const invalid = computed(() => ($status(), control.invalid), ...(ngDevMode ? [{ debugName: "invalid" }] : []));
813
+ const touched = computed(() => ($events(), control.touched), ...(ngDevMode ? [{ debugName: "touched" }] : []));
814
+ const errors = computed(() => ($events(), control.errors), ...(ngDevMode ? [{ debugName: "errors" }] : []));
815
+ return {
816
+ control: control,
817
+ value: computed(() => {
818
+ $value();
819
+ $events();
820
+ return control.value;
821
+ }),
822
+ status: $status.asReadonly(),
823
+ events: $events.asReadonly(),
824
+ invalid: invalid,
825
+ errors: errors,
826
+ touched: touched,
827
+ valid: computed(() => ($status(), control.valid)),
828
+ dirty: computed(() => ($events(), control.dirty)),
829
+ disabled: computed(() => ($status(), control.disabled)),
830
+ touchedAndInvalid: computed(() => touched() && invalid()),
831
+ firstErrorKey: computed(() => Object.keys(errors() ?? {})[0] ?? null),
832
+ validators: computed(() => ($events(), control.validator?.({}) ?? null)),
833
+ unsubscribe: () => unsubscribe(),
834
+ };
835
+ }
836
+ /**
837
+ * Creates a {@link FormGroupSignal} for a `FormGroup`. Child controls are
838
+ * converted recursively, so nested `FormGroup` and `FormArray` structures
839
+ * are supported.
840
+ *
841
+ * @template TControls - A typed map of control names to `AbstractControl` instances.
842
+ * @param formGroup - The `FormGroup` to wrap.
843
+ * @param options - Optional `DestroyRef` and `Injector` forwarded to
844
+ * each underlying {@link controlSignal} call.
845
+ * @returns A {@link FormGroupSignal} with both group-level and per-control signals.
846
+ */
847
+ function formGroupSignal(formGroup, options) {
848
+ const destroyRef = options?.destroyRef ??
849
+ tryCatch(() => inject(DestroyRef, { optional: true }))[0];
850
+ const controls = Object.fromEntries(Object.entries(formGroup.controls).map(([key, ctrl]) => {
851
+ return [key, nestedControlSignal(ctrl, { destroyRef })];
852
+ }));
853
+ const groupSignal = controlSignal(formGroup);
854
+ const selfUnsubscribe = groupSignal.unsubscribe.bind(groupSignal);
855
+ return Object.assign(groupSignal, {
856
+ controls,
857
+ unsubscribe: () => {
858
+ selfUnsubscribe();
859
+ Object.values(controls).forEach((c) => c.unsubscribe());
860
+ },
861
+ });
862
+ }
863
+ /**
864
+ * Creates a {@link FormArraySignal} for a `FormArray`. Child controls are
865
+ * converted recursively, so arrays of `FormGroup`, arrays of `FormArray`,
866
+ * and arrays of `FormControl` are all supported.
867
+ */
868
+ function formArraySignal(formArray, options) {
869
+ const destroyRef = options?.destroyRef ??
870
+ tryCatch(() => inject(DestroyRef, { optional: true }))[0];
871
+ const controls = formArray.controls.map((ctrl) => nestedControlSignal(ctrl, { destroyRef }));
872
+ const arraySignal = controlSignal(formArray);
873
+ const selfUnsubscribe = arraySignal.unsubscribe.bind(arraySignal);
874
+ return Object.assign(arraySignal, {
875
+ controls,
876
+ unsubscribe: () => {
877
+ selfUnsubscribe();
878
+ controls.forEach((c) => c.unsubscribe());
879
+ },
880
+ });
881
+ }
882
+ function nestedControlSignal(control, options) {
883
+ if (control instanceof FormGroup) {
884
+ return formGroupSignal(control, options);
885
+ }
886
+ if (control instanceof FormArray) {
887
+ return formArraySignal(control, options);
888
+ }
889
+ return controlSignal(control, options);
890
+ }
891
+
892
+ /**
893
+ * Shared empty error map object to avoid creating new objects.
894
+ *
895
+ * @internal
896
+ */
897
+ const EMPTY_ERROR_MAP = {};
898
+ /**
899
+ * Checks if a value is a plain JavaScript object (not array, not null, not class instance).
900
+ *
901
+ * @internal
902
+ * @param value - Value to check
903
+ * @returns True if value is a plain object
904
+ */
905
+ function isPlainObject(value) {
906
+ if (!value || typeof value !== 'object')
907
+ return false;
908
+ const proto = Object.getPrototypeOf(value);
909
+ return proto === Object.prototype || proto === null;
910
+ }
911
+ /**
912
+ * Checks if an object contains any Angular signal values.
913
+ *
914
+ * @internal
915
+ * @param obj - Object to check for signal values
916
+ * @returns True if any property value is a signal
917
+ */
918
+ function hasSignalValues(obj) {
919
+ return Object.values(obj).some((value) => isSignal(value));
920
+ }
921
+ /**
922
+ * Resolves a SignalOrValue to its actual value, handling nested signal objects.
923
+ *
924
+ * If the resolved value is a plain object containing signals, it converts
925
+ * all signal properties to their values using fromSignalObj.
926
+ *
927
+ * @internal
928
+ * @template TValue - The type of value to resolve
929
+ * @param value - Signal or static value to resolve
930
+ * @returns The resolved value
931
+ */
932
+ function resolveControlValue(value) {
933
+ const resolved = signalOrValue(value);
934
+ if (isPlainObject(resolved) && hasSignalValues(resolved)) {
935
+ return fromSignalObj(resolved);
936
+ }
937
+ return resolved;
938
+ }
939
+ /**
940
+ * Normalizes various control input formats to a standard SignalFormControlConfig.
941
+ *
942
+ * Handles three input types:
943
+ * - SignalFormControl: extracts current value
944
+ * - Config object with 'value' property: uses as-is
945
+ * - Raw value: wraps in config object
946
+ *
947
+ * @internal
948
+ * @template TValue - The type of control value
949
+ * @template TControls - Object type defining available sibling controls
950
+ * @param input - Input in any accepted format
951
+ * @returns Normalized control configuration
952
+ */
953
+ function normalizeControlInput(input) {
954
+ if (isSignalFormControl(input)) {
955
+ return { value: input.value() };
956
+ }
957
+ if (typeof input === 'object' && input !== null && 'value' in input) {
958
+ return input;
959
+ }
960
+ return { value: input };
961
+ }
962
+ /**
963
+ * Type guard to check if an object is a SignalFormControl.
964
+ *
965
+ * @template TValue - The type of value the control manages
966
+ * @param obj - Object to check
967
+ * @returns True if obj is a SignalFormControl
968
+ *
969
+ * @example
970
+ * ```typescript
971
+ * if (isSignalFormControl(value)) {
972
+ * console.log(value.value()); // TypeScript knows this is a control
973
+ * }
974
+ * ```
975
+ */
976
+ function isSignalFormControl(obj) {
977
+ return (!!obj &&
978
+ typeof obj === 'object' &&
979
+ obj.kind === 'control');
980
+ }
981
+ /**
982
+ * Creates a reactive form control with signal-based state management.
983
+ *
984
+ * Builds a control that wraps a primitive value (string, number, boolean, etc.)
985
+ * with validation, state tracking, and reactive updates using Angular signals.
986
+ *
987
+ * Features:
988
+ * - Reactive value updates through signals
989
+ * - Validators for errors (mark control as invalid)
990
+ * - Warnings (validation messages without invalidating)
991
+ * - Dynamic disabled state based on form context
992
+ * - State tracking (touched, dirty)
993
+ * - Manual error/warning management
994
+ *
995
+ * @template TControls - Object type defining available sibling controls for cross-field validation
996
+ * @template TValue - The type of value this control manages
997
+ *
998
+ * @param input - Control input (raw value, config object, or existing control)
999
+ * @param getControl - Optional accessor function for sibling controls
1000
+ * @returns Fully configured SignalFormControl instance
1001
+ *
1002
+ * @example
1003
+ * ```typescript
1004
+ * // Simple control
1005
+ * const nameControl = createSignalFormControl('John');
1006
+ *
1007
+ * // With validators
1008
+ * const ageControl = createSignalFormControl({
1009
+ * value: 25,
1010
+ * validators: [signalFormValidators.required, signalFormValidators.min(0)],
1011
+ * warnings: [signalFormValidators.max(120)]
1012
+ * });
1013
+ *
1014
+ * // With dynamic disabled
1015
+ * const emailControl = createSignalFormControl({
1016
+ * value: '',
1017
+ * disabled: (ctx) => ctx.getControl('accountType').value() === 'guest'
1018
+ * });
1019
+ * ```
1020
+ */
1021
+ function createSignalFormControl(input, getControl) {
1022
+ if (isSignalFormControl(input)) {
1023
+ return input;
1024
+ }
1025
+ const config = normalizeControlInput(input);
1026
+ const accessControl = getControl ??
1027
+ (() => {
1028
+ throw new Error('getControl is not available for this control.');
1029
+ });
1030
+ const initialValue = resolveControlValue(config.value);
1031
+ const value = writableSignal(() => resolveControlValue(config.value));
1032
+ const manualErrors = signal({}, ...(ngDevMode ? [{ debugName: "manualErrors" }] : []));
1033
+ const manualWarnings = signal({}, ...(ngDevMode ? [{ debugName: "manualWarnings" }] : []));
1034
+ const selfTouched = signal(false, ...(ngDevMode ? [{ debugName: "selfTouched" }] : []));
1035
+ const selfDirty = signal(false, ...(ngDevMode ? [{ debugName: "selfDirty" }] : []));
1036
+ const selfDisabled = signal(false, ...(ngDevMode ? [{ debugName: "selfDisabled" }] : []));
1037
+ const disabledResolver = config.disabled;
1038
+ const disabled = computed(() => {
1039
+ const derived = typeof disabledResolver === 'function'
1040
+ ? disabledResolver({
1041
+ item: { value: value() },
1042
+ getControl: accessControl,
1043
+ })
1044
+ : disabledResolver
1045
+ ? signalOrValue(disabledResolver)
1046
+ : false;
1047
+ return selfDisabled() || derived;
1048
+ }, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
1049
+ const errors = computed(() => {
1050
+ if (disabled())
1051
+ return EMPTY_ERROR_MAP;
1052
+ const ctx = {
1053
+ item: { value: value() },
1054
+ getControl: accessControl,
1055
+ };
1056
+ return {
1057
+ ...collectValidationErrors(config.validators, ctx),
1058
+ ...manualErrors(),
1059
+ };
1060
+ }, ...(ngDevMode ? [{ debugName: "errors" }] : []));
1061
+ const warnings = computed(() => {
1062
+ if (disabled())
1063
+ return EMPTY_ERROR_MAP;
1064
+ const ctx = {
1065
+ item: { value: value() },
1066
+ getControl: accessControl,
1067
+ };
1068
+ return {
1069
+ ...collectValidationErrors(config.warnings, ctx),
1070
+ ...manualWarnings(),
1071
+ };
1072
+ }, ...(ngDevMode ? [{ debugName: "warnings" }] : []));
1073
+ const invalid = computed(() => !disabled() && hasErrors(errors()), ...(ngDevMode ? [{ debugName: "invalid" }] : []));
1074
+ const valid = computed(() => !invalid(), ...(ngDevMode ? [{ debugName: "valid" }] : []));
1075
+ const firstError = computed(() => {
1076
+ const entries = Object.entries(errors());
1077
+ if (!entries.length)
1078
+ return undefined;
1079
+ const [name, message] = entries[0] ?? ['', ''];
1080
+ return { name, message, type: 'error' };
1081
+ }, ...(ngDevMode ? [{ debugName: "firstError" }] : []));
1082
+ const firstWarning = computed(() => {
1083
+ const entries = Object.entries(warnings());
1084
+ if (!entries.length)
1085
+ return undefined;
1086
+ const [name, message] = entries[0] ?? ['', ''];
1087
+ return { name, message, type: 'warning' };
1088
+ }, ...(ngDevMode ? [{ debugName: "firstWarning" }] : []));
1089
+ const firstErrorOrWarning = computed(() => {
1090
+ return firstError() ?? firstWarning();
1091
+ }, ...(ngDevMode ? [{ debugName: "firstErrorOrWarning" }] : []));
1092
+ const setValue = (next, options) => {
1093
+ value.set(next);
1094
+ if (options?.markDirty ?? true)
1095
+ selfDirty.set(true);
1096
+ if (options?.markTouched)
1097
+ selfTouched.set(true);
1098
+ };
1099
+ const reset = (next) => {
1100
+ value.set(next ?? initialValue);
1101
+ selfDirty.set(false);
1102
+ selfTouched.set(false);
1103
+ manualErrors.set({});
1104
+ manualWarnings.set({});
1105
+ };
1106
+ return {
1107
+ kind: 'control',
1108
+ value,
1109
+ disabled,
1110
+ touched: computed(() => selfTouched()),
1111
+ dirty: computed(() => selfDirty()),
1112
+ errors,
1113
+ warnings,
1114
+ selfErrors: errors,
1115
+ selfWarnings: warnings,
1116
+ invalid,
1117
+ valid,
1118
+ firstError,
1119
+ firstWarning,
1120
+ firstErrorOrWarning,
1121
+ setValue,
1122
+ reset,
1123
+ markTouched: () => selfTouched.set(true),
1124
+ markUntouched: () => selfTouched.set(false),
1125
+ markDirty: () => selfDirty.set(true),
1126
+ markPristine: () => selfDirty.set(false),
1127
+ setDisabled: (next) => selfDisabled.set(next),
1128
+ setError: (key, message) => manualErrors.update((current) => ({ ...current, [key]: message })),
1129
+ clearError: (key) => manualErrors.update((current) => {
1130
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1131
+ const { [key]: _removed, ...rest } = current;
1132
+ return rest;
1133
+ }),
1134
+ clearErrors: () => manualErrors.set({}),
1135
+ setWarning: (key, message) => manualWarnings.update((current) => ({ ...current, [key]: message })),
1136
+ clearWarning: (key) => manualWarnings.update((current) => {
1137
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1138
+ const { [key]: _removed, ...rest } = current;
1139
+ return rest;
1140
+ }),
1141
+ clearWarnings: () => manualWarnings.set({}),
1142
+ };
1143
+ }
1144
+
1145
+ /**
1146
+ * Set of valid keys for control configuration objects.
1147
+ * Used to distinguish config objects from plain value objects.
1148
+ *
1149
+ * @internal
1150
+ */
1151
+ const CONTROL_CONFIG_KEYS$1 = new Set([
1152
+ 'value',
1153
+ 'validators',
1154
+ 'warnings',
1155
+ 'disabled',
1156
+ ]);
1157
+ /**
1158
+ * Checks if a value is a control configuration object.
1159
+ *
1160
+ * Determines if the value has a 'value' property and only contains
1161
+ * valid control config keys (value, validators, warnings, disabled).
1162
+ *
1163
+ * @internal
1164
+ * @param value - Value to check
1165
+ * @returns True if value is a control config object
1166
+ */
1167
+ function isControlConfig$1(value) {
1168
+ if (!value || typeof value !== 'object')
1169
+ return false;
1170
+ if (!('value' in value))
1171
+ return false;
1172
+ return Object.keys(value).every(key => CONTROL_CONFIG_KEYS$1.has(key));
1173
+ }
1174
+ /**
1175
+ * Creates the appropriate control node from various input types.
1176
+ *
1177
+ * Recursively determines and creates the correct control type:
1178
+ * - Existing controls are returned as-is
1179
+ * - Arrays become SignalFormArray
1180
+ * - Objects (non-config) become SignalForm (group)
1181
+ * - Primitives/configs become SignalFormControl
1182
+ *
1183
+ * @internal
1184
+ * @template TValue - The type of value to create a control for
1185
+ * @template TControls - Object type defining available sibling controls
1186
+ * @param input - Input value in any accepted format
1187
+ * @param getControl - Accessor for sibling controls
1188
+ * @returns Appropriate control type for the input
1189
+ */
1190
+ function createNodeFromInput$1(input, getControl) {
1191
+ if (isSignalFormControl(input)) {
1192
+ return input;
1193
+ }
1194
+ if (isSignalFormGroup(input)) {
1195
+ return input;
1196
+ }
1197
+ if (isSignalFormArray(input)) {
1198
+ return input;
1199
+ }
1200
+ if (Array.isArray(input)) {
1201
+ return createSignalFormArray(input, getControl);
1202
+ }
1203
+ if (typeof input === 'object' && input !== null && !isControlConfig$1(input)) {
1204
+ return createSignalFormGroup(input);
1205
+ }
1206
+ return createSignalFormControl(input, getControl);
1207
+ }
1208
+ /**
1209
+ * Type guard to check if an object is a SignalForm (form group).
1210
+ *
1211
+ * @template TData - Object type defining the form structure
1212
+ * @param obj - Object to check
1213
+ * @returns True if obj is a SignalForm
1214
+ *
1215
+ * @example
1216
+ * ```typescript
1217
+ * if (isSignalFormGroup(value)) {
1218
+ * console.log(value.controls.name); // TypeScript knows this is a form group
1219
+ * }
1220
+ * ```
1221
+ */
1222
+ function isSignalFormGroup(obj) {
1223
+ return (!!obj &&
1224
+ typeof obj === 'object' &&
1225
+ obj.kind === 'group');
1226
+ }
1227
+ /**
1228
+ * Creates a reactive form group for managing structured form data.
1229
+ *
1230
+ * Builds a typed form container that holds multiple named controls, groups, or arrays.
1231
+ * Each property in the input object becomes a control with full signal-based reactivity.
1232
+ * Provides type-safe access to controls and tracks collective validation state.
1233
+ *
1234
+ * Features:
1235
+ * - Type-safe control access via `.controls` property
1236
+ * - Reactive value and error tracking across all controls
1237
+ * - Collective state management (touched, dirty, valid)
1238
+ * - Manual error/warning management at group level
1239
+ * - Support for nested groups and arrays
1240
+ * - Cross-field validation via getControl accessor
1241
+ *
1242
+ * @template TData - Object type defining the structure and types of all controls
1243
+ *
1244
+ * @param inputs - Object mapping property names to their control inputs
1245
+ * @returns Fully configured SignalForm instance
1246
+ *
1247
+ * @example
1248
+ * ```typescript
1249
+ * // Simple form
1250
+ * const form = createSignalFormGroup({
1251
+ * name: 'John',
1252
+ * age: 25
1253
+ * });
1254
+ *
1255
+ * // With validators and nested structure
1256
+ * const form = createSignalFormGroup<User>({
1257
+ * email: {
1258
+ * value: '',
1259
+ * validators: [signalFormValidators.required, signalFormValidators.email]
1260
+ * },
1261
+ * age: {
1262
+ * value: 25,
1263
+ * validators: [signalFormValidators.min(0)],
1264
+ * warnings: [signalFormValidators.max(120)]
1265
+ * },
1266
+ * address: {
1267
+ * street: '123 Main St',
1268
+ * city: 'NYC'
1269
+ * },
1270
+ * hobbies: ['coding', 'gaming']
1271
+ * });
1272
+ *
1273
+ * // Access controls
1274
+ * form.controls.email.value(); // Type-safe access
1275
+ * form.getControl('age').setValue(30);
1276
+ * ```
1277
+ */
1278
+ function createSignalFormGroup(inputs) {
1279
+ const controls = {};
1280
+ const getControl = key => controls[key];
1281
+ Object.entries(inputs).forEach(([key, value]) => {
1282
+ controls[key] = createNodeFromInput$1(value, getControl);
1283
+ });
1284
+ const selfDirty = signal(false, ...(ngDevMode ? [{ debugName: "selfDirty" }] : []));
1285
+ const selfTouched = signal(false, ...(ngDevMode ? [{ debugName: "selfTouched" }] : []));
1286
+ const selfDisabled = signal(false, ...(ngDevMode ? [{ debugName: "selfDisabled" }] : []));
1287
+ const manualErrors = signal({}, ...(ngDevMode ? [{ debugName: "manualErrors" }] : []));
1288
+ const manualWarnings = signal({}, ...(ngDevMode ? [{ debugName: "manualWarnings" }] : []));
1289
+ const value = computed(() => {
1290
+ return Object.entries(controls).reduce((acc, [key, control]) => {
1291
+ acc[key] = control.value();
1292
+ return acc;
1293
+ }, {});
1294
+ }, ...(ngDevMode ? [{ debugName: "value" }] : []));
1295
+ const errors = computed(() => {
1296
+ const all = {};
1297
+ Object.entries(controls).forEach(([key, control]) => {
1298
+ if (control.kind === 'control') {
1299
+ const map = control.errors();
1300
+ all[key] = (hasErrors(map) ? map : undefined);
1301
+ return;
1302
+ }
1303
+ const childErrors = control.errors();
1304
+ all[key] = (isEmptyErrorTree(childErrors) ? undefined : childErrors);
1305
+ });
1306
+ return all;
1307
+ }, ...(ngDevMode ? [{ debugName: "errors" }] : []));
1308
+ const warnings = computed(() => {
1309
+ const all = {};
1310
+ Object.entries(controls).forEach(([key, control]) => {
1311
+ if (control.kind === 'control') {
1312
+ const map = control.warnings();
1313
+ all[key] = (hasErrors(map) ? map : undefined);
1314
+ return;
1315
+ }
1316
+ const childWarnings = control.warnings();
1317
+ all[key] = (isEmptyErrorTree(childWarnings) ? undefined : childWarnings);
1318
+ });
1319
+ return all;
1320
+ }, ...(ngDevMode ? [{ debugName: "warnings" }] : []));
1321
+ const selfErrors = computed(() => {
1322
+ if (selfDisabled())
1323
+ return {};
1324
+ return { ...manualErrors() };
1325
+ }, ...(ngDevMode ? [{ debugName: "selfErrors" }] : []));
1326
+ const selfWarnings = computed(() => {
1327
+ if (selfDisabled())
1328
+ return {};
1329
+ return { ...manualWarnings() };
1330
+ }, ...(ngDevMode ? [{ debugName: "selfWarnings" }] : []));
1331
+ const invalid = computed(() => {
1332
+ if (selfDisabled())
1333
+ return false;
1334
+ return (hasErrors(selfErrors()) ||
1335
+ Object.values(controls).some(control => control.invalid()));
1336
+ }, ...(ngDevMode ? [{ debugName: "invalid" }] : []));
1337
+ const valid = computed(() => !invalid(), ...(ngDevMode ? [{ debugName: "valid" }] : []));
1338
+ const touched = computed(() => selfTouched() ||
1339
+ Object.values(controls).some(control => control.touched()), ...(ngDevMode ? [{ debugName: "touched" }] : []));
1340
+ const dirty = computed(() => selfDirty() ||
1341
+ Object.values(controls).some(control => control.dirty()), ...(ngDevMode ? [{ debugName: "dirty" }] : []));
1342
+ const setValue = (nextValues, options) => {
1343
+ Object.entries(nextValues).forEach(([key, nextValue]) => {
1344
+ const control = controls[key];
1345
+ control?.setValue(nextValue, options);
1346
+ });
1347
+ if (options?.markDirty ?? true)
1348
+ selfDirty.set(true);
1349
+ if (options?.markTouched)
1350
+ selfTouched.set(true);
1351
+ };
1352
+ const reset = (nextValues) => {
1353
+ if (nextValues) {
1354
+ setValue(nextValues, { markDirty: false });
1355
+ }
1356
+ else {
1357
+ Object.values(controls).forEach(control => control.reset());
1358
+ }
1359
+ selfDirty.set(false);
1360
+ selfTouched.set(false);
1361
+ manualErrors.set({});
1362
+ manualWarnings.set({});
1363
+ };
1364
+ const setDisabled = (disabled, options) => {
1365
+ selfDisabled.set(disabled);
1366
+ if (!options?.onlySelf) {
1367
+ Object.values(controls).forEach(control => control.setDisabled(disabled));
1368
+ }
1369
+ };
1370
+ return {
1371
+ kind: 'group',
1372
+ controls,
1373
+ value,
1374
+ errors,
1375
+ warnings,
1376
+ selfErrors,
1377
+ selfWarnings,
1378
+ disabled: computed(() => selfDisabled()),
1379
+ touched,
1380
+ dirty,
1381
+ invalid,
1382
+ valid,
1383
+ getControl,
1384
+ setValue,
1385
+ reset,
1386
+ markTouched: () => selfTouched.set(true),
1387
+ markUntouched: () => selfTouched.set(false),
1388
+ markDirty: () => selfDirty.set(true),
1389
+ markPristine: () => selfDirty.set(false),
1390
+ markAllTouched: () => Object.values(controls).forEach(control => control.markTouched()),
1391
+ setDisabled,
1392
+ setError: (key, message) => manualErrors.update(current => ({ ...current, [key]: message })),
1393
+ clearError: (key) => manualErrors.update(current => {
1394
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1395
+ const { [key]: _removed, ...rest } = current;
1396
+ return rest;
1397
+ }),
1398
+ clearErrors: () => manualErrors.set({}),
1399
+ setWarning: (key, message) => manualWarnings.update(current => ({ ...current, [key]: message })),
1400
+ clearWarning: (key) => manualWarnings.update(current => {
1401
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1402
+ const { [key]: _removed, ...rest } = current;
1403
+ return rest;
1404
+ }),
1405
+ clearWarnings: () => manualWarnings.set({}),
1406
+ };
1407
+ }
1408
+ /**
1409
+ * Helper function to create a standalone form control.
1410
+ *
1411
+ * Creates a control without sibling control access. Useful for creating
1412
+ * individual controls outside of a form group context.
1413
+ *
1414
+ * @template TValue - The type of value the control manages
1415
+ * @param input - Control input (raw value, config object, or existing control)
1416
+ * @returns SignalFormControl instance
1417
+ *
1418
+ * @example
1419
+ * ```typescript
1420
+ * const nameControl = formControl('John');
1421
+ * const ageControl = formControl({
1422
+ * value: 25,
1423
+ * validators: [signalFormValidators.min(0)]
1424
+ * });
1425
+ * ```
1426
+ */
1427
+ function formControl(input) {
1428
+ return createSignalFormControl(input, undefined);
1429
+ }
1430
+ /**
1431
+ * Helper function to create a standalone form array.
1432
+ *
1433
+ * Creates an array without sibling control access. Useful for creating
1434
+ * array controls outside of a form group context.
1435
+ *
1436
+ * @template TValue - The type of each item in the array
1437
+ * @param input - Array of initial items
1438
+ * @returns SignalFormArray instance
1439
+ *
1440
+ * @example
1441
+ * ```typescript
1442
+ * const tagsArray = formArray(['tag1', 'tag2', 'tag3']);
1443
+ * const addressesArray = formArray<Address>([
1444
+ * { street: '123 Main', city: 'NYC' }
1445
+ * ]);
1446
+ * ```
1447
+ */
1448
+ function formArray(input) {
1449
+ return createSignalFormArray(input, undefined);
1450
+ }
1451
+ /**
1452
+ * Helper function to create a form group.
1453
+ *
1454
+ * Alias for createSignalFormGroup. Creates a typed form with multiple controls.
1455
+ *
1456
+ * @template TData - Object type defining the form structure
1457
+ * @param input - Object mapping property names to control inputs
1458
+ * @returns SignalForm instance
1459
+ *
1460
+ * @example
1461
+ * ```typescript
1462
+ * const form = formGroup({
1463
+ * name: 'John',
1464
+ * email: {
1465
+ * value: 'john@example.com',
1466
+ * validators: [signalFormValidators.email]
1467
+ * }
1468
+ * });
1469
+ * ```
1470
+ */
1471
+ function formGroup(input) {
1472
+ return createSignalFormGroup(input);
1473
+ }
1474
+ /**
1475
+ * Primary API for creating signal-based reactive forms.
1476
+ *
1477
+ * Alias for `formGroup`. This is the main entry point for creating forms.
1478
+ * Provides type-safe, signal-based form state management with built-in validation.
1479
+ *
1480
+ * @example
1481
+ * ```typescript
1482
+ * // Basic form
1483
+ * const form = signalForm({ name: 'John', age: 25 });
1484
+ *
1485
+ * // Complex form with validation
1486
+ * const form = signalForm<Person>({
1487
+ * name: {
1488
+ * value: '',
1489
+ * validators: [signalFormValidators.required, signalFormValidators.minLength(2)]
1490
+ * },
1491
+ * age: {
1492
+ * value: 30,
1493
+ * validators: [signalFormValidators.min(0)],
1494
+ * warnings: [signalFormValidators.max(120)],
1495
+ * disabled: (ctx) => ctx.getControl('name').value() === 'admin'
1496
+ * },
1497
+ * address: {
1498
+ * street: '123 Main St',
1499
+ * city: 'NYC'
1500
+ * },
1501
+ * hobbies: ['coding', 'gaming']
1502
+ * });
1503
+ *
1504
+ * // Access form state
1505
+ * console.log(form.value()); // { name: '', age: 30, address: {...}, hobbies: [...] }
1506
+ * console.log(form.valid()); // boolean
1507
+ * console.log(form.controls.name.errors()); // { required: 'This field is required' }
1508
+ * ```
1509
+ */
1510
+ const signalForm = formGroup;
1511
+
1512
+ /**
1513
+ * Set of valid keys for control configuration objects.
1514
+ * Used to distinguish config objects from plain value objects.
1515
+ *
1516
+ * @internal
1517
+ */
1518
+ const CONTROL_CONFIG_KEYS = new Set([
1519
+ 'value',
1520
+ 'validators',
1521
+ 'warnings',
1522
+ 'disabled',
1523
+ ]);
1524
+ /**
1525
+ * Checks if a value is a control configuration object.
1526
+ *
1527
+ * Determines if the value has a 'value' property and only contains
1528
+ * valid control config keys (value, validators, warnings, disabled).
1529
+ *
1530
+ * @internal
1531
+ * @param value - Value to check
1532
+ * @returns True if value is a control config object
1533
+ */
1534
+ function isControlConfig(value) {
1535
+ if (!value || typeof value !== 'object')
1536
+ return false;
1537
+ if (!('value' in value))
1538
+ return false;
1539
+ return Object.keys(value).every(key => CONTROL_CONFIG_KEYS.has(key));
1540
+ }
1541
+ /**
1542
+ * Creates the appropriate control node from various input types.
1543
+ *
1544
+ * Recursively determines and creates the correct control type:
1545
+ * - Existing controls are returned as-is
1546
+ * - Arrays become SignalFormArray
1547
+ * - Objects (non-config) become SignalForm (group)
1548
+ * - Primitives/configs become SignalFormControl
1549
+ *
1550
+ * @internal
1551
+ * @template TItem - The type of item to create a control for
1552
+ * @template TControls - Object type defining available sibling controls
1553
+ * @param input - Input value in any accepted format
1554
+ * @param getControl - Accessor for sibling controls
1555
+ * @returns Appropriate control type for the input
1556
+ */
1557
+ function createNodeFromInput(input, getControl) {
1558
+ if (isSignalFormControl(input)) {
1559
+ return input;
1560
+ }
1561
+ if (isSignalFormGroup(input)) {
1562
+ return input;
1563
+ }
1564
+ if (isSignalFormArray(input)) {
1565
+ return input;
1566
+ }
1567
+ if (Array.isArray(input)) {
1568
+ return createSignalFormArray(input, getControl);
1569
+ }
1570
+ if (typeof input === 'object' && input !== null && !isControlConfig(input)) {
1571
+ return createSignalFormGroup(input);
1572
+ }
1573
+ return createSignalFormControl(input, getControl);
1574
+ }
1575
+ /**
1576
+ * Type guard to check if an object is a SignalFormArray.
1577
+ *
1578
+ * @template TValue - The type of items in the array
1579
+ * @param obj - Object to check
1580
+ * @returns True if obj is a SignalFormArray
1581
+ *
1582
+ * @example
1583
+ * ```typescript
1584
+ * if (isSignalFormArray(value)) {
1585
+ * console.log(value.controls().length); // TypeScript knows this is an array
1586
+ * }
1587
+ * ```
1588
+ */
1589
+ function isSignalFormArray(obj) {
1590
+ return (!!obj &&
1591
+ typeof obj === 'object' &&
1592
+ obj.kind === 'array');
1593
+ }
1594
+ /**
1595
+ * Creates a reactive form array for managing dynamic collections of controls.
1596
+ *
1597
+ * Builds an array container that can hold multiple controls (primitives, groups, or nested arrays)
1598
+ * with full signal-based reactivity. Provides methods for dynamic addition/removal of items
1599
+ * and tracks collective validation state.
1600
+ *
1601
+ * Features:
1602
+ * - Dynamic array operations (push, insert, remove, clear)
1603
+ * - Reactive value and error tracking across all items
1604
+ * - Collective state management (touched, dirty, valid)
1605
+ * - Manual error/warning management at array level
1606
+ * - Type-safe access to individual controls
1607
+ *
1608
+ * @template TItem - The type of each item in the array
1609
+ * @template TControls - Object type defining available sibling controls
1610
+ *
1611
+ * @param inputItems - Array of initial items (values, configs, or controls)
1612
+ * @param getControl - Optional accessor for sibling controls
1613
+ * @returns Fully configured SignalFormArray instance
1614
+ *
1615
+ * @example
1616
+ * ```typescript
1617
+ * // Array of primitives
1618
+ * const tagsArray = createSignalFormArray(['tag1', 'tag2']);
1619
+ * tagsArray.push('tag3');
1620
+ *
1621
+ * // Array of objects
1622
+ * const addressesArray = createSignalFormArray<Address>([
1623
+ * { street: '123 Main', city: 'NYC' },
1624
+ * { street: '456 Oak', city: 'LA' }
1625
+ * ]);
1626
+ *
1627
+ * // Array with validators
1628
+ * const hobbiesArray = createSignalFormArray([
1629
+ * { value: 'coding', validators: [signalFormValidators.minLength(3)] },
1630
+ * 'gaming'
1631
+ * ]);
1632
+ * ```
1633
+ */
1634
+ function createSignalFormArray(inputItems, getControl) {
1635
+ const accessControl = getControl ??
1636
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1637
+ ((_) => {
1638
+ throw new Error('getControl is not available for this array.');
1639
+ });
1640
+ const controls = signal(inputItems.map(item => createNodeFromInput(item, accessControl)), ...(ngDevMode ? [{ debugName: "controls" }] : []));
1641
+ const selfDirty = signal(false, ...(ngDevMode ? [{ debugName: "selfDirty" }] : []));
1642
+ const selfTouched = signal(false, ...(ngDevMode ? [{ debugName: "selfTouched" }] : []));
1643
+ const selfDisabled = signal(false, ...(ngDevMode ? [{ debugName: "selfDisabled" }] : []));
1644
+ const manualErrors = signal({}, ...(ngDevMode ? [{ debugName: "manualErrors" }] : []));
1645
+ const manualWarnings = signal({}, ...(ngDevMode ? [{ debugName: "manualWarnings" }] : []));
1646
+ const value = computed(() => controls().map(control => control.value()), ...(ngDevMode ? [{ debugName: "value" }] : []));
1647
+ const errors = computed(() => controls().map(control => {
1648
+ if (control.kind === 'control') {
1649
+ const map = control.errors();
1650
+ return hasErrors(map) ? map : undefined;
1651
+ }
1652
+ const childErrors = control.errors();
1653
+ return isEmptyErrorTree(childErrors) ? undefined : childErrors;
1654
+ }), ...(ngDevMode ? [{ debugName: "errors" }] : []));
1655
+ const warnings = computed(() => controls().map(control => {
1656
+ if (control.kind === 'control') {
1657
+ const map = control.warnings();
1658
+ return hasErrors(map) ? map : undefined;
1659
+ }
1660
+ const childWarnings = control.warnings();
1661
+ return isEmptyErrorTree(childWarnings) ? undefined : childWarnings;
1662
+ }), ...(ngDevMode ? [{ debugName: "warnings" }] : []));
1663
+ const selfErrors = computed(() => {
1664
+ if (selfDisabled())
1665
+ return {};
1666
+ return { ...manualErrors() };
1667
+ }, ...(ngDevMode ? [{ debugName: "selfErrors" }] : []));
1668
+ const selfWarnings = computed(() => {
1669
+ if (selfDisabled())
1670
+ return {};
1671
+ return { ...manualWarnings() };
1672
+ }, ...(ngDevMode ? [{ debugName: "selfWarnings" }] : []));
1673
+ const invalid = computed(() => {
1674
+ if (selfDisabled())
1675
+ return false;
1676
+ return (hasErrors(selfErrors()) || controls().some(control => control.invalid()));
1677
+ }, ...(ngDevMode ? [{ debugName: "invalid" }] : []));
1678
+ const valid = computed(() => !invalid(), ...(ngDevMode ? [{ debugName: "valid" }] : []));
1679
+ const touched = computed(() => selfTouched() || controls().some(control => control.touched()), ...(ngDevMode ? [{ debugName: "touched" }] : []));
1680
+ const dirty = computed(() => selfDirty() || controls().some(control => control.dirty()), ...(ngDevMode ? [{ debugName: "dirty" }] : []));
1681
+ const insert = (item, index) => {
1682
+ const node = createNodeFromInput(item, accessControl);
1683
+ const position = index ?? controls().length;
1684
+ controls.update(old => {
1685
+ const newArray = [...old];
1686
+ newArray.splice(position, 0, node);
1687
+ return newArray;
1688
+ });
1689
+ selfDirty.set(true);
1690
+ return position;
1691
+ };
1692
+ const push = (item) => insert(item);
1693
+ const removeAt = (index) => {
1694
+ controls.update(old => {
1695
+ const newArray = [...old];
1696
+ newArray.splice(index, 1);
1697
+ return newArray;
1698
+ });
1699
+ selfDirty.set(true);
1700
+ };
1701
+ const clear = () => {
1702
+ controls.set([]);
1703
+ selfDirty.set(true);
1704
+ };
1705
+ const at = (index) => controls()[index];
1706
+ const setValue = (nextValues, options) => {
1707
+ controls.update(old => {
1708
+ if (nextValues.length !== old.length) {
1709
+ return nextValues.map(_value => createNodeFromInput(_value, accessControl));
1710
+ }
1711
+ old.forEach((control, index) => {
1712
+ control.setValue(nextValues[index], options);
1713
+ });
1714
+ return old;
1715
+ });
1716
+ if (options?.markDirty ?? true)
1717
+ selfDirty.set(true);
1718
+ if (options?.markTouched)
1719
+ selfTouched.set(true);
1720
+ };
1721
+ const reset = (nextValues) => {
1722
+ if (nextValues) {
1723
+ setValue(nextValues, { markDirty: false });
1724
+ }
1725
+ else {
1726
+ controls().forEach(control => control.reset());
1727
+ }
1728
+ selfDirty.set(false);
1729
+ selfTouched.set(false);
1730
+ manualErrors.set({});
1731
+ manualWarnings.set({});
1732
+ };
1733
+ const setDisabled = (disabled, options) => {
1734
+ selfDisabled.set(disabled);
1735
+ if (!options?.onlySelf) {
1736
+ controls().forEach(control => control.setDisabled(disabled));
1737
+ }
1738
+ };
1739
+ return {
1740
+ kind: 'array',
1741
+ controls,
1742
+ value,
1743
+ errors,
1744
+ warnings,
1745
+ selfErrors,
1746
+ selfWarnings,
1747
+ disabled: computed(() => selfDisabled()),
1748
+ touched,
1749
+ dirty,
1750
+ invalid,
1751
+ valid,
1752
+ insert,
1753
+ push,
1754
+ removeAt,
1755
+ clear,
1756
+ at,
1757
+ setValue,
1758
+ reset,
1759
+ markTouched: () => selfTouched.set(true),
1760
+ markUntouched: () => selfTouched.set(false),
1761
+ markDirty: () => selfDirty.set(true),
1762
+ markPristine: () => selfDirty.set(false),
1763
+ markAllTouched: () => controls().forEach(control => control.markTouched()),
1764
+ setDisabled,
1765
+ setError: (key, message) => manualErrors.update(current => ({ ...current, [key]: message })),
1766
+ clearError: (key) => manualErrors.update(current => {
1767
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1768
+ const { [key]: _removed, ...rest } = current;
1769
+ return rest;
1770
+ }),
1771
+ clearErrors: () => manualErrors.set({}),
1772
+ setWarning: (key, message) => manualWarnings.update(current => ({ ...current, [key]: message })),
1773
+ clearWarning: (key) => manualWarnings.update(current => {
1774
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1775
+ const { [key]: _removed, ...rest } = current;
1776
+ return rest;
1777
+ }),
1778
+ clearWarnings: () => manualWarnings.set({}),
1779
+ };
1780
+ }
1781
+
1782
+ /**
1783
+ * Regular expression for email validation.
1784
+ *
1785
+ * Aligned with Angular's built-in email validator. Validates standard email formats
1786
+ * with proper domain and local parts.
1787
+ *
1788
+ * Pattern requirements:
1789
+ * - Total length: 1-254 characters
1790
+ * - Local part (before @): 1-64 characters
1791
+ * - Allows alphanumeric and special characters: !#$%&'*+/=?^_`{|}~-
1792
+ * - Domain must have valid format with optional subdomains
1793
+ */
1794
+ const EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
1795
+ /**
1796
+ * Determines if a value is considered empty for validation purposes.
1797
+ *
1798
+ * Checks various types:
1799
+ * - null/undefined: always empty
1800
+ * - Numbers: empty when equals 0
1801
+ * - Strings/Arrays: empty when length is 0
1802
+ * - Sets: empty when size is 0
1803
+ *
1804
+ * @param value - Value to check for emptiness
1805
+ * @returns True if the value is considered empty
1806
+ *
1807
+ * @example
1808
+ * ```typescript
1809
+ * isEmptyInputValue(null); // true
1810
+ * isEmptyInputValue(''); // true
1811
+ * isEmptyInputValue(0); // true
1812
+ * isEmptyInputValue('hello'); // false
1813
+ * ```
1814
+ */
1815
+ function isEmptyInputValue(value) {
1816
+ if (value === null || value === undefined)
1817
+ return true;
1818
+ // TODO 1: check if number==0 is empty or number.length==0 is empty
1819
+ if (typeof value === 'number' /* or - isNumber(value) */) {
1820
+ return value == 0;
1821
+ }
1822
+ if (Array.isArray(value) || typeof value === 'string') {
1823
+ return value.length === 0;
1824
+ }
1825
+ if (value instanceof Set) {
1826
+ return value.size === 0;
1827
+ }
1828
+ return false;
1829
+ }
1830
+ /**
1831
+ * Type guard that checks if a value can be coerced to a valid number.
1832
+ *
1833
+ * Uses JavaScript's Number coercion and checks for NaN to determine
1834
+ * if a value represents a valid numeric value.
1835
+ *
1836
+ * @param value - Value to check
1837
+ * @returns True if value is or can be coerced to a number
1838
+ *
1839
+ * @example
1840
+ * ```typescript
1841
+ * isNumber(42); // true
1842
+ * isNumber('42'); // true
1843
+ * isNumber('hello'); // false
1844
+ * ```
1845
+ */
1846
+ function isNumber(value) {
1847
+ return !Number.isNaN(Number(value));
1848
+ }
1849
+ /**
1850
+ * Validator that requires a non-empty value.
1851
+ *
1852
+ * Fails when the value is null, undefined, empty string, empty array,
1853
+ * zero (for numbers), or empty Set.
1854
+ *
1855
+ * @param ctx - Validation context with the value to check
1856
+ * @returns Error object with 'required' key if validation fails, null otherwise
1857
+ *
1858
+ * @example
1859
+ * ```typescript
1860
+ * const control = signalForm({ name: { value: '', validators: [signalFormValidators.required] } });
1861
+ * // control.controls.name.errors() => { required: 'This field is required' }
1862
+ * ```
1863
+ */
1864
+ const required = ({ item: { value } }) => isEmptyInputValue(value) ? { required: 'This field is required' } : null;
1865
+ /**
1866
+ * Validator that enforces a maximum string or number length.
1867
+ *
1868
+ * Converts the value to string and checks if its length exceeds the specified maximum.
1869
+ * Works with both string and number types.
1870
+ *
1871
+ * @param num - Maximum allowed length (inclusive)
1872
+ * @returns Validator function
1873
+ *
1874
+ * @example
1875
+ * ```typescript
1876
+ * const control = signalForm({
1877
+ * username: { value: 'verylongusername', validators: [signalFormValidators.maxLength(10)] }
1878
+ * });
1879
+ * // control.controls.username.errors() => { maxLength: 'To long' }
1880
+ * ```
1881
+ */
1882
+ function maxLength(num) {
1883
+ return ({ item: { value } }) => (typeof value == 'string' || typeof value == 'number') &&
1884
+ value.toString().length > num
1885
+ ? { maxLength: 'To long' }
1886
+ : null;
1887
+ }
1888
+ /**
1889
+ * Validator that enforces a minimum string or number length.
1890
+ *
1891
+ * Converts the value to string and checks if its length is less than or equal to the specified minimum.
1892
+ * Works with both string and number types.
1893
+ *
1894
+ * @param num - Minimum required length (inclusive)
1895
+ * @returns Validator function
1896
+ *
1897
+ * @example
1898
+ * ```typescript
1899
+ * const control = signalForm({
1900
+ * code: { value: 'ab', validators: [signalFormValidators.minLength(3)] }
1901
+ * });
1902
+ * // control.controls.code.errors() => { minLength: 'To short' }
1903
+ * ```
1904
+ */
1905
+ function minLength(num) {
1906
+ return ({ item: { value } }) => (typeof value == 'string' || typeof value == 'number') &&
1907
+ value.toString().length <= num
1908
+ ? { minLength: 'To short' }
1909
+ : null;
1910
+ }
1911
+ /**
1912
+ * Validator that enforces a minimum numeric value.
1913
+ *
1914
+ * Checks if a numeric value is less than or equal to the specified minimum.
1915
+ * Value is coerced to a number for comparison.
1916
+ *
1917
+ * @param num - Minimum allowed value (exclusive - value must be greater than this)
1918
+ * @returns Validator function
1919
+ *
1920
+ * @example
1921
+ * ```typescript
1922
+ * const control = signalForm({
1923
+ * age: { value: -5, validators: [signalFormValidators.min(0)] }
1924
+ * });
1925
+ * // control.controls.age.errors() => { minLength: 'To small' }
1926
+ * ```
1927
+ */
1928
+ function min(num) {
1929
+ return ({ item: { value } }) => isNumber(value) && value <= num ? { minLength: 'To small' } : null;
1930
+ }
1931
+ /**
1932
+ * Validator that enforces a maximum numeric value.
1933
+ *
1934
+ * Checks if a numeric value exceeds the specified maximum.
1935
+ * Value is coerced to a number for comparison.
1936
+ *
1937
+ * @param num - Maximum allowed value (inclusive)
1938
+ * @returns Validator function
1939
+ *
1940
+ * @example
1941
+ * ```typescript
1942
+ * const control = signalForm({
1943
+ * age: { value: 150, validators: [signalFormValidators.max(120)] }
1944
+ * });
1945
+ * // control.controls.age.errors() => { minLength: 'To big' }
1946
+ * ```
1947
+ */
1948
+ function max(num) {
1949
+ return ({ item: { value } }) => isNumber(value) && value > num ? { minLength: 'To big' } : null;
1950
+ }
1951
+ /**
1952
+ * Validator that checks if a value is a valid email address.
1953
+ *
1954
+ * Uses Angular-compatible email regex pattern to validate email format.
1955
+ * Skips validation for empty values (use with `required` if needed).
1956
+ *
1957
+ * @param ctx - Validation context with the email value to check
1958
+ * @returns Error object with 'email' key if validation fails, null otherwise
1959
+ *
1960
+ * @example
1961
+ * ```typescript
1962
+ * const control = signalForm({
1963
+ * email: { value: 'invalid-email', validators: [signalFormValidators.email] }
1964
+ * });
1965
+ * // control.controls.email.errors() => { email: 'Invalid email' }
1966
+ * ```
1967
+ */
1968
+ const email = ({ item: { value } }) => {
1969
+ if (isEmptyInputValue(value))
1970
+ return null;
1971
+ return EMAIL_REGEXP.test(String(value)) ? null : { email: 'Invalid email' };
1972
+ };
1973
+ /**
1974
+ * Validator that checks if a value matches a specified regular expression pattern.
1975
+ *
1976
+ * Accepts either a RegExp object or a string pattern. String patterns are automatically
1977
+ * wrapped with ^ and $ anchors to match the entire value.
1978
+ *
1979
+ * Skips validation for empty values (use with `required` if needed).
1980
+ *
1981
+ * @param valuePattern - Regular expression or pattern string to match against
1982
+ * @returns Validator function
1983
+ *
1984
+ * @example
1985
+ * ```typescript
1986
+ * // Using regex
1987
+ * const control1 = signalForm({
1988
+ * code: { value: 'abc', validators: [signalFormValidators.pattern(/^[0-9]+$/)] }
1989
+ * });
1990
+ * // control1.controls.code.errors() => { pattern: 'RequiredPattern: ^[0-9]+$, ActualValue: abc' }
1991
+ *
1992
+ * // Using string pattern
1993
+ * const control2 = signalForm({
1994
+ * zipCode: { value: 'ABC', validators: [signalFormValidators.pattern('[0-9]{5}')] }
1995
+ * });
1996
+ * ```
1997
+ */
1998
+ function pattern(valuePattern) {
1999
+ if (!valuePattern)
2000
+ return () => null;
2001
+ let regex;
2002
+ let regexStr;
2003
+ if (typeof valuePattern === 'string') {
2004
+ regexStr = '';
2005
+ if (valuePattern.charAt(0) !== '^')
2006
+ regexStr += '^';
2007
+ regexStr += valuePattern;
2008
+ if (valuePattern.charAt(valuePattern.length - 1) !== '$')
2009
+ regexStr += '$';
2010
+ regex = new RegExp(regexStr);
2011
+ }
2012
+ else {
2013
+ regexStr = valuePattern.toString();
2014
+ regex = valuePattern;
2015
+ }
2016
+ return ({ item: { value } }) => {
2017
+ if (isEmptyInputValue(value))
2018
+ return null;
2019
+ const valueStr = String(value);
2020
+ return regex.test(valueStr)
2021
+ ? null
2022
+ : {
2023
+ pattern: `RequiredPattern: ${regexStr}, ActualValue: ${valueStr}`,
2024
+ };
2025
+ };
2026
+ }
2027
+ /**
2028
+ * Collection of built-in validators for signal-form controls.
2029
+ *
2030
+ * Provides common validation functions that can be used in the `validators` or `warnings`
2031
+ * arrays of form controls. All validators skip empty values except `required`.
2032
+ *
2033
+ * @property required - Ensures the value is not empty (null, undefined, '', [], 0, empty Set)
2034
+ * @property maxLength - Ensures string/number length doesn't exceed maximum
2035
+ * @property minLength - Ensures string/number length meets minimum requirement
2036
+ * @property min - Ensures numeric value is greater than minimum (exclusive)
2037
+ * @property max - Ensures numeric value doesn't exceed maximum (inclusive)
2038
+ * @property email - Validates email address format using Angular-compatible regex
2039
+ * @property pattern - Validates value matches a regular expression pattern
2040
+ *
2041
+ * @example
2042
+ * ```typescript
2043
+ * const form = signalForm({
2044
+ * email: {
2045
+ * value: '',
2046
+ * validators: [signalFormValidators.required, signalFormValidators.email]
2047
+ * },
2048
+ * age: {
2049
+ * value: 25,
2050
+ * validators: [signalFormValidators.min(0), signalFormValidators.max(120)],
2051
+ * warnings: [signalFormValidators.max(100)] // Warning but doesn't invalidate
2052
+ * },
2053
+ * username: {
2054
+ * value: '',
2055
+ * validators: [
2056
+ * signalFormValidators.required,
2057
+ * signalFormValidators.minLength(3),
2058
+ * signalFormValidators.maxLength(20),
2059
+ * signalFormValidators.pattern(/^[a-zA-Z0-9_]+$/)
2060
+ * ]
2061
+ * }
2062
+ * });
2063
+ * ```
2064
+ */
2065
+ const signalFormValidators = {
2066
+ required,
2067
+ maxLength,
2068
+ minLength,
2069
+ min,
2070
+ max,
2071
+ email,
2072
+ pattern,
2073
+ };
2074
+
2075
+ /**
2076
+ * Example demonstrating the usage of signal-based reactive forms.
2077
+ *
2078
+ * Shows various features including:
2079
+ * - Simple value initialization
2080
+ * - Validators that mark controls as invalid
2081
+ * - Warnings that don't affect validity
2082
+ * - Dynamic disabled state based on other controls
2083
+ * - Nested objects and arrays
2084
+ * - Type-safe control access
2085
+ *
2086
+ * This function is exported as an example and reference implementation.
2087
+ * It's not meant to be called in production code.
2088
+ *
2089
+ * @example
2090
+ * ```typescript
2091
+ * // Basic usage pattern from the example
2092
+ * const form = signalForm<Person>({
2093
+ * name: 'dvirus',
2094
+ * age: {
2095
+ * value: 130,
2096
+ * validators: [signalFormValidators.required, signalFormValidators.min(0)],
2097
+ * warnings: [signalFormValidators.max(120)],
2098
+ * disabled: (ctx) => ctx.getControl('name').value() === 'admin'
2099
+ * },
2100
+ * address: {
2101
+ * street: { value: '123 Main St', validators: [signalFormValidators.required] }
2102
+ * },
2103
+ * hobbies: [
2104
+ * { value: 'coding', validators: [signalFormValidators.minLength(3)] },
2105
+ * 'programming'
2106
+ * ]
2107
+ * });
2108
+ *
2109
+ * // Access form state
2110
+ * form.getControl('address').value(); // { street: '123 Main St' }
2111
+ * form.controls.hobbies.controls()[0].errors(); // {}
2112
+ * form.controls.age.firstErrorOrWarning(); // { name: 'max', message: 'To big', type: 'warning' }
2113
+ * ```
2114
+ */
2115
+ // function main() {
2116
+ // type Person = {
2117
+ // name: string;
2118
+ // age: number;
2119
+ // address: {
2120
+ // street: string;
2121
+ // city: string;
2122
+ // };
2123
+ // hobbies: string[];
2124
+ // };
2125
+ // /**
2126
+ // * Example 1: Simple form with direct value initialization.
2127
+ // *
2128
+ // * Creates a form from a plain object. Each property becomes
2129
+ // * a control with the provided value.
2130
+ // */
2131
+ // const model1: Person = {
2132
+ // name: 'dvirus',
2133
+ // age: 30,
2134
+ // address: {
2135
+ // street: '123 Main St',
2136
+ // city: 'Any-town',
2137
+ // },
2138
+ // hobbies: ['coding', 'gaming'],
2139
+ // };
2140
+ // const form1 = signalForm(model1);
2141
+ // /**
2142
+ // * Example 2: Advanced form with validators, warnings, and dynamic disabled state.
2143
+ // *
2144
+ // * Demonstrates:
2145
+ // * - Simple value for name field
2146
+ // * - Age control with validators (required, min) and warnings (max)
2147
+ // * - Dynamic disabled logic based on name value
2148
+ // * - Nested address object with validated street field
2149
+ // * - Array of hobbies with mixed control configs and simple values
2150
+ // */
2151
+ // const form2 = signalForm<Person>({
2152
+ // name: 'dvirus',
2153
+ // age: {
2154
+ // // initial value
2155
+ // value: 130,
2156
+ // // example of validators, they add error messages and mark the control as invalid if the validation fails
2157
+ // validators: [signalFormValidators.required, signalFormValidators.min(0)],
2158
+ // // warning are like validators but they don't make the control invalid, just add a warning message
2159
+ // warnings: [signalFormValidators.max(120)],
2160
+ // // example of dynamic disabling based on another control's value
2161
+ // disabled: (ctx) => ctx.getControl('name').value() === 'admin',
2162
+ // },
2163
+ // address: {
2164
+ // street: {
2165
+ // value: '123 Main St',
2166
+ // validators: [signalFormValidators.required],
2167
+ // disabled: false,
2168
+ // },
2169
+ // },
2170
+ // hobbies: [
2171
+ // {
2172
+ // value: 'coding',
2173
+ // validators: [signalFormValidators.minLength(3)],
2174
+ // disabled: computed(() => false),
2175
+ // },
2176
+ // 'programming',
2177
+ // 'Typing',
2178
+ // ],
2179
+ // });
2180
+ // // Example outputs demonstrating form access patterns
2181
+ // console.log(form2.getControl('address').value()); // { street: '123 Main St' }
2182
+ // console.log(form2.controls.hobbies.controls()[0].errors()); // {}
2183
+ // console.log(form2.controls.age.firstErrorOrWarning()); // {name: 'minLength', message: 'To big', type: 'warning'}
2184
+ // console.log(form2.controls.age.warnings()); // { minLength: 'To big' }
2185
+ // }
2186
+
2187
+ /**
2188
+ * Generated bundle index. Do not edit.
2189
+ */
2190
+
2191
+ export { controlSignal, createSignalFormArray, createSignalFormControl, createSignalFormGroup, formArray, formArraySignal, formControl, formGroup, formGroupSignal, fromSignalObj, isSignalFormArray, isSignalFormControl, isSignalFormGroup, isSignalObject, mergeSignalObjects, signalDebounce, signalForm, signalFormValidators, signalMap, signalNotifier, signalObject, signalOrFunction, signalOrValue, signalSet, toSignalObj, tryCatch, writableSignal };
2192
+ //# sourceMappingURL=dvirus-js-angular-signals.mjs.map