@signaltree/ng-forms 4.0.15 → 4.1.0

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.
@@ -1,1038 +0,0 @@
1
- import * as i0 from '@angular/core';
2
- import { signal, computed, isSignal, inject, DestroyRef, EventEmitter, ElementRef, Renderer2, effect, forwardRef, HostListener, Output, Input, Directive } from '@angular/core';
3
- import { FormGroup, FormArray, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
4
- import { signalTree } from '@signaltree/core';
5
- import { deepClone, parsePath, mergeDeep, matchPath, snapshotsEqual } from '@signaltree/shared';
6
- import { isObservable, firstValueFrom } from 'rxjs';
7
-
8
- class FormValidationError extends Error {
9
- errors;
10
- asyncErrors;
11
- constructor(errors, asyncErrors) {
12
- super('Form validation failed');
13
- this.errors = errors;
14
- this.asyncErrors = asyncErrors;
15
- this.name = 'FormValidationError';
16
- }
17
- }
18
- const SYNC_ERROR_KEY = 'signaltree';
19
- const ASYNC_ERROR_KEY = 'signaltreeAsync';
20
- function createFormTree(initialValues, config = {}) {
21
- const { validators: baseValidators = {}, asyncValidators: baseAsyncValidators = {}, destroyRef: providedDestroyRef, fieldConfigs = {}, conditionals = [], persistKey, storage, persistDebounceMs = 100, validationBatchMs = 0, ...treeConfig } = config;
22
- const syncValidators = normalizeSyncValidators(baseValidators, fieldConfigs);
23
- const asyncValidators = normalizeAsyncValidators(baseAsyncValidators, fieldConfigs);
24
- const { values: hydratedInitialValues } = hydrateInitialValues(initialValues, persistKey, storage);
25
- const initialSnapshot = deepClone(hydratedInitialValues);
26
- const valuesTree = signalTree(hydratedInitialValues, treeConfig);
27
- assertTreeNode(valuesTree.state);
28
- const flattenedState = valuesTree.state;
29
- enhanceArraysRecursively(flattenedState);
30
- const formGroup = createAbstractControl(hydratedInitialValues, '', syncValidators, asyncValidators);
31
- const destroyRef = providedDestroyRef ?? tryInjectDestroyRef();
32
- const cleanupCallbacks = [];
33
- const errors = signal({}, ...(ngDevMode ? [{ debugName: "errors" }] : []));
34
- const asyncErrors = signal({}, ...(ngDevMode ? [{ debugName: "asyncErrors" }] : []));
35
- const touched = signal({}, ...(ngDevMode ? [{ debugName: "touched" }] : []));
36
- const asyncValidating = signal({}, ...(ngDevMode ? [{ debugName: "asyncValidating" }] : []));
37
- const dirty = signal(formGroup.dirty, ...(ngDevMode ? [{ debugName: "dirty" }] : []));
38
- const valid = signal(formGroup.valid && !formGroup.pending, ...(ngDevMode ? [{ debugName: "valid" }] : []));
39
- const submitting = signal(false, ...(ngDevMode ? [{ debugName: "submitting" }] : []));
40
- const refreshRunner = () => {
41
- const snapshot = collectControlSnapshot(formGroup);
42
- errors.set(snapshot.syncErrors);
43
- asyncErrors.set(snapshot.asyncErrors);
44
- touched.set(snapshot.touched);
45
- asyncValidating.set(snapshot.pending);
46
- dirty.set(formGroup.dirty);
47
- valid.set(formGroup.valid && !formGroup.pending);
48
- };
49
- let refreshTimer = null;
50
- const refreshAggregates = (immediate = false) => {
51
- if (immediate || validationBatchMs <= 0) {
52
- if (refreshTimer) {
53
- clearTimeout(refreshTimer);
54
- refreshTimer = null;
55
- }
56
- refreshRunner();
57
- return;
58
- }
59
- if (refreshTimer) {
60
- clearTimeout(refreshTimer);
61
- }
62
- refreshTimer = setTimeout(() => {
63
- refreshTimer = null;
64
- refreshRunner();
65
- }, validationBatchMs);
66
- };
67
- refreshAggregates(true);
68
- const persistController = createPersistController(formGroup, persistKey, storage, persistDebounceMs, cleanupCallbacks);
69
- persistController.persistImmediately();
70
- const fieldErrorKeys = new Set([
71
- ...Object.keys(syncValidators),
72
- ...Object.keys(asyncValidators),
73
- ]);
74
- const fieldErrors = {};
75
- const fieldAsyncErrors = {};
76
- fieldErrorKeys.forEach((fieldPath) => {
77
- fieldErrors[fieldPath] = computed(() => errors()[fieldPath]);
78
- fieldAsyncErrors[fieldPath] = computed(() => asyncErrors()[fieldPath]);
79
- });
80
- const fieldConfigLookup = (path) => resolveFieldConfig(fieldConfigs, path);
81
- const connectControlRecursive = (control, path) => {
82
- if (control instanceof FormGroup) {
83
- Object.entries(control.controls).forEach(([key, child]) => {
84
- connectControlRecursive(child, joinPath(path, key));
85
- });
86
- return;
87
- }
88
- if (control instanceof FormArray) {
89
- const arraySignal = getSignalAtPath(flattenedState, path);
90
- if (arraySignal) {
91
- connectFormArrayAndSignal(control, arraySignal, path, syncValidators, asyncValidators, cleanupCallbacks, connectControlRecursive);
92
- }
93
- control.controls.forEach((child, index) => {
94
- connectControlRecursive(child, joinPath(path, String(index)));
95
- });
96
- return;
97
- }
98
- const signalAtPath = getSignalAtPath(flattenedState, path);
99
- if (signalAtPath) {
100
- connectControlAndSignal(control, signalAtPath, cleanupCallbacks, fieldConfigLookup(path));
101
- }
102
- };
103
- connectControlRecursive(formGroup, '');
104
- const conditionalState = new Map();
105
- const applyConditionals = conditionals.length > 0
106
- ? () => {
107
- const values = formGroup.getRawValue();
108
- conditionals.forEach(({ when, fields }) => {
109
- let visible = true;
110
- try {
111
- visible = when(values);
112
- }
113
- catch {
114
- visible = true;
115
- }
116
- fields.forEach((fieldPath) => {
117
- const control = formGroup.get(fieldPath);
118
- if (!control) {
119
- return;
120
- }
121
- const previous = conditionalState.get(fieldPath);
122
- if (previous === visible) {
123
- return;
124
- }
125
- conditionalState.set(fieldPath, visible);
126
- if (visible) {
127
- control.enable({ emitEvent: false });
128
- }
129
- else {
130
- control.disable({ emitEvent: false });
131
- }
132
- });
133
- });
134
- }
135
- : () => undefined;
136
- applyConditionals();
137
- const aggregateSubscriptions = [];
138
- aggregateSubscriptions.push(formGroup.valueChanges.subscribe(() => {
139
- refreshAggregates();
140
- persistController.schedulePersist();
141
- applyConditionals();
142
- }));
143
- aggregateSubscriptions.push(formGroup.statusChanges.subscribe(() => refreshAggregates()));
144
- if (formGroup.events) {
145
- aggregateSubscriptions.push(formGroup.events.subscribe(() => refreshAggregates()));
146
- }
147
- cleanupCallbacks.push(() => {
148
- aggregateSubscriptions.forEach((sub) => sub.unsubscribe());
149
- if (refreshTimer) {
150
- clearTimeout(refreshTimer);
151
- }
152
- });
153
- if (destroyRef) {
154
- destroyRef.onDestroy(() => {
155
- cleanupCallbacks.splice(0).forEach((fn) => fn());
156
- });
157
- }
158
- const setValue = (field, value) => {
159
- const targetSignal = getSignalAtPath(flattenedState, field);
160
- const control = formGroup.get(field);
161
- if (targetSignal && 'set' in targetSignal) {
162
- targetSignal.set(value);
163
- }
164
- else if (control) {
165
- control.setValue(value, { emitEvent: true });
166
- }
167
- if (control) {
168
- const untypedControl = control;
169
- untypedControl.markAsTouched();
170
- untypedControl.markAsDirty();
171
- untypedControl.updateValueAndValidity({ emitEvent: true });
172
- }
173
- refreshAggregates(true);
174
- persistController.schedulePersist();
175
- };
176
- const setValues = (values) => {
177
- Object.entries(values).forEach(([key, value]) => {
178
- setValue(key, value);
179
- });
180
- };
181
- const reset = () => {
182
- formGroup.reset(initialSnapshot);
183
- submitting.set(false);
184
- refreshAggregates(true);
185
- persistController.persistImmediately();
186
- applyConditionals();
187
- };
188
- const validate = async (field) => {
189
- if (field) {
190
- const control = formGroup.get(field);
191
- if (!control) {
192
- return;
193
- }
194
- control.markAsTouched();
195
- control.updateValueAndValidity({ emitEvent: true });
196
- refreshAggregates(true);
197
- await waitForPending(control);
198
- refreshAggregates(true);
199
- return;
200
- }
201
- formGroup.markAllAsTouched();
202
- formGroup.updateValueAndValidity({ emitEvent: true });
203
- refreshAggregates(true);
204
- await waitForPending(formGroup);
205
- refreshAggregates(true);
206
- };
207
- const submit = async (submitFn) => {
208
- submitting.set(true);
209
- try {
210
- await validate();
211
- if (!valid()) {
212
- throw new FormValidationError(errors(), asyncErrors());
213
- }
214
- const currentValues = formGroup.getRawValue();
215
- const result = await submitFn(currentValues);
216
- return result;
217
- }
218
- finally {
219
- submitting.set(false);
220
- refreshAggregates(true);
221
- }
222
- };
223
- const destroy = () => {
224
- cleanupCallbacks.splice(0).forEach((fn) => fn());
225
- };
226
- const formTree = {
227
- state: flattenedState,
228
- $: flattenedState,
229
- form: formGroup,
230
- errors,
231
- asyncErrors,
232
- touched,
233
- asyncValidating,
234
- dirty,
235
- valid,
236
- submitting,
237
- unwrap: () => valuesTree(),
238
- setValue,
239
- setValues,
240
- reset,
241
- submit,
242
- validate,
243
- getFieldError: (field) => fieldErrors[field] || computed(() => undefined),
244
- getFieldAsyncError: (field) => fieldAsyncErrors[field] || computed(() => undefined),
245
- getFieldTouched: (field) => computed(() => formGroup.get(field)?.touched ?? false),
246
- isFieldValid: (field) => computed(() => {
247
- const control = formGroup.get(field);
248
- return !!control && control.valid && !control.pending;
249
- }),
250
- isFieldAsyncValidating: (field) => computed(() => !!formGroup.get(field)?.pending),
251
- fieldErrors,
252
- fieldAsyncErrors,
253
- values: valuesTree,
254
- destroy,
255
- };
256
- return formTree;
257
- }
258
- function createVirtualFormArray(items, visibleRange, controlFactory = (value) => new FormControl(value)) {
259
- const start = Math.max(0, visibleRange.start);
260
- const end = Math.max(start, visibleRange.end);
261
- const controls = items
262
- .slice(start, end)
263
- .map((item, offset) => controlFactory(item, start + offset));
264
- return new FormArray(controls);
265
- }
266
- function enhanceArraysRecursively(obj, visited = new WeakSet()) {
267
- if (visited.has(obj)) {
268
- return;
269
- }
270
- visited.add(obj);
271
- for (const key in obj) {
272
- const value = obj[key];
273
- if (isSignal(value)) {
274
- const signalValue = value();
275
- if (Array.isArray(signalValue)) {
276
- obj[key] = enhanceArray(value);
277
- }
278
- }
279
- else if (typeof value === 'object' &&
280
- value !== null &&
281
- !Array.isArray(value)) {
282
- enhanceArraysRecursively(value, visited);
283
- }
284
- }
285
- }
286
- const enhanceArray = (arraySignal) => {
287
- const enhanced = arraySignal;
288
- enhanced.push = (item) => {
289
- arraySignal.update((arr) => [...arr, item]);
290
- };
291
- enhanced.removeAt = (index) => {
292
- arraySignal.update((arr) => arr.filter((_, i) => i !== index));
293
- };
294
- enhanced.setAt = (index, value) => {
295
- arraySignal.update((arr) => arr.map((item, i) => (i === index ? value : item)));
296
- };
297
- enhanced.insertAt = (index, item) => {
298
- arraySignal.update((arr) => [
299
- ...arr.slice(0, index),
300
- item,
301
- ...arr.slice(index),
302
- ]);
303
- };
304
- enhanced.move = (from, to) => {
305
- arraySignal.update((arr) => {
306
- const newArr = [...arr];
307
- const [item] = newArr.splice(from, 1);
308
- if (item !== undefined) {
309
- newArr.splice(to, 0, item);
310
- }
311
- return newArr;
312
- });
313
- };
314
- enhanced.clear = () => {
315
- arraySignal.set([]);
316
- };
317
- return enhanced;
318
- };
319
- function getSignalAtPath(node, path) {
320
- if (!path) {
321
- return null;
322
- }
323
- const segments = parsePath(path);
324
- let current = node;
325
- for (const segment of segments) {
326
- if (!current || typeof current !== 'object') {
327
- return null;
328
- }
329
- current = current[segment];
330
- }
331
- if (isSignal(current)) {
332
- return current;
333
- }
334
- return null;
335
- }
336
- function joinPath(parent, segment) {
337
- return parent ? `${parent}.${segment}` : segment;
338
- }
339
- function wrapSyncValidator(validator) {
340
- return (control) => {
341
- const result = validator(control.value);
342
- return result ? { [SYNC_ERROR_KEY]: result } : null;
343
- };
344
- }
345
- function wrapAsyncValidator(validator) {
346
- return async (control) => {
347
- try {
348
- const maybeAsync = validator(control.value);
349
- const resolved = isObservable(maybeAsync)
350
- ? await firstValueFrom(maybeAsync)
351
- : await maybeAsync;
352
- return resolved ? { [ASYNC_ERROR_KEY]: resolved } : null;
353
- }
354
- catch {
355
- return { [ASYNC_ERROR_KEY]: 'Validation error' };
356
- }
357
- };
358
- }
359
- function createAbstractControl(value, path, validators, asyncValidators) {
360
- const syncValidator = findValidator(validators, path);
361
- const asyncValidator = findValidator(asyncValidators, path);
362
- const syncFns = syncValidator
363
- ? [wrapSyncValidator(syncValidator)]
364
- : undefined;
365
- const asyncFns = asyncValidator
366
- ? [wrapAsyncValidator(asyncValidator)]
367
- : undefined;
368
- if (Array.isArray(value)) {
369
- const controls = value.map((item, index) => createAbstractControl(item, joinPath(path, String(index)), validators, asyncValidators));
370
- return new FormArray(controls, syncFns, asyncFns);
371
- }
372
- if (isPlainObject(value)) {
373
- const controls = {};
374
- for (const [key, child] of Object.entries(value)) {
375
- controls[key] = createAbstractControl(child, joinPath(path, key), validators, asyncValidators);
376
- }
377
- return new FormGroup(controls, {
378
- validators: syncFns,
379
- asyncValidators: asyncFns,
380
- });
381
- }
382
- return new FormControl(value, {
383
- validators: syncFns,
384
- asyncValidators: asyncFns,
385
- nonNullable: false,
386
- });
387
- }
388
- function connectControlAndSignal(control, valueSignal, cleanupCallbacks, fieldConfig) {
389
- let updatingFromControl = false;
390
- let updatingFromSignal = false;
391
- let versionCounter = 0;
392
- let lastControlVersion = 0;
393
- let controlDebounceTimer = null;
394
- const debounceMs = fieldConfig?.debounceMs ?? 0;
395
- const originalSet = valueSignal.set.bind(valueSignal);
396
- const originalUpdate = valueSignal.update.bind(valueSignal);
397
- const applyControlValue = (value) => {
398
- updatingFromSignal = true;
399
- if (!Object.is(control.value, value)) {
400
- const untypedControl = control;
401
- untypedControl.setValue(value, { emitEvent: true });
402
- untypedControl.markAsDirty();
403
- }
404
- updatingFromSignal = false;
405
- };
406
- valueSignal.set = (value) => {
407
- const currentVersion = ++versionCounter;
408
- originalSet(value);
409
- if (updatingFromControl) {
410
- return;
411
- }
412
- if (lastControlVersion > currentVersion) {
413
- return;
414
- }
415
- applyControlValue(value);
416
- };
417
- valueSignal.update = (updater) => {
418
- const next = updater(valueSignal());
419
- valueSignal.set(next);
420
- };
421
- const pushUpdateFromControl = (value) => {
422
- updatingFromControl = true;
423
- lastControlVersion = ++versionCounter;
424
- originalSet(value);
425
- updatingFromControl = false;
426
- };
427
- const handleControlChange = (value) => {
428
- if (updatingFromSignal) {
429
- return;
430
- }
431
- if (debounceMs > 0) {
432
- if (controlDebounceTimer) {
433
- clearTimeout(controlDebounceTimer);
434
- }
435
- controlDebounceTimer = setTimeout(() => {
436
- controlDebounceTimer = null;
437
- pushUpdateFromControl(value);
438
- }, debounceMs);
439
- return;
440
- }
441
- pushUpdateFromControl(value);
442
- };
443
- const subscription = control.valueChanges.subscribe((value) => {
444
- handleControlChange(value);
445
- });
446
- cleanupCallbacks.push(() => {
447
- subscription.unsubscribe();
448
- valueSignal.set = originalSet;
449
- valueSignal.update = originalUpdate;
450
- if (controlDebounceTimer) {
451
- clearTimeout(controlDebounceTimer);
452
- }
453
- });
454
- }
455
- function connectFormArrayAndSignal(formArray, arraySignal, path, validators, asyncValidators, cleanupCallbacks, connectControlRecursive) {
456
- let updatingFromControl = false;
457
- let updatingFromSignal = false;
458
- const originalSet = arraySignal.set.bind(arraySignal);
459
- const originalUpdate = arraySignal.update.bind(arraySignal);
460
- arraySignal.set = (value) => {
461
- originalSet(value);
462
- if (updatingFromControl) {
463
- return;
464
- }
465
- updatingFromSignal = true;
466
- syncFormArrayFromValue(formArray, value, path, validators, asyncValidators, connectControlRecursive);
467
- formArray.markAsDirty();
468
- updatingFromSignal = false;
469
- };
470
- arraySignal.update = (updater) => {
471
- const next = updater(arraySignal());
472
- arraySignal.set(next);
473
- };
474
- const subscription = formArray.valueChanges.subscribe((value) => {
475
- if (updatingFromSignal) {
476
- return;
477
- }
478
- updatingFromControl = true;
479
- originalSet(value);
480
- updatingFromControl = false;
481
- });
482
- cleanupCallbacks.push(() => {
483
- subscription.unsubscribe();
484
- arraySignal.set = originalSet;
485
- arraySignal.update = originalUpdate;
486
- });
487
- syncFormArrayFromValue(formArray, arraySignal(), path, validators, asyncValidators, connectControlRecursive);
488
- }
489
- function syncFormArrayFromValue(formArray, nextValue, path, validators, asyncValidators, connectControlRecursive) {
490
- if (!Array.isArray(nextValue)) {
491
- nextValue = [];
492
- }
493
- while (formArray.length > nextValue.length) {
494
- formArray.removeAt(formArray.length - 1);
495
- }
496
- nextValue.forEach((item, index) => {
497
- const childPath = joinPath(path, String(index));
498
- const existing = formArray.at(index);
499
- if (!existing) {
500
- const control = createAbstractControl(item, childPath, validators, asyncValidators);
501
- formArray.insert(index, control);
502
- connectControlRecursive(control, childPath);
503
- return;
504
- }
505
- if (existing instanceof FormArray) {
506
- syncFormArrayFromValue(existing, Array.isArray(item) ? item : [], childPath, validators, asyncValidators, connectControlRecursive);
507
- return;
508
- }
509
- if (existing instanceof FormGroup) {
510
- if (isPlainObject(item)) {
511
- existing.setValue(item, {
512
- emitEvent: false,
513
- });
514
- }
515
- return;
516
- }
517
- if (!Object.is(existing.value, item)) {
518
- const untypedExisting = existing;
519
- untypedExisting.setValue(item, { emitEvent: false });
520
- }
521
- });
522
- }
523
- function collectControlSnapshot(control) {
524
- const snapshot = {
525
- syncErrors: {},
526
- asyncErrors: {},
527
- touched: {},
528
- pending: {},
529
- };
530
- traverseControls(control, (currentPath, currentControl) => {
531
- if (!currentPath) {
532
- return;
533
- }
534
- if (currentControl.touched) {
535
- snapshot.touched[currentPath] = true;
536
- }
537
- if (currentControl.pending) {
538
- snapshot.pending[currentPath] = true;
539
- }
540
- const errors = currentControl.errors;
541
- if (!errors) {
542
- return;
543
- }
544
- const syncMessage = errors[SYNC_ERROR_KEY];
545
- if (typeof syncMessage === 'string') {
546
- snapshot.syncErrors[currentPath] = syncMessage;
547
- }
548
- const asyncMessage = errors[ASYNC_ERROR_KEY];
549
- if (typeof asyncMessage === 'string') {
550
- snapshot.asyncErrors[currentPath] = asyncMessage;
551
- }
552
- }, '');
553
- return snapshot;
554
- }
555
- function traverseControls(control, visitor, path = '') {
556
- visitor(path, control);
557
- if (control instanceof FormGroup) {
558
- Object.entries(control.controls).forEach(([key, child]) => {
559
- traverseControls(child, visitor, joinPath(path, key));
560
- });
561
- return;
562
- }
563
- if (control instanceof FormArray) {
564
- control.controls.forEach((child, index) => {
565
- traverseControls(child, visitor, joinPath(path, String(index)));
566
- });
567
- }
568
- }
569
- function waitForPending(control) {
570
- if (!control.pending) {
571
- return Promise.resolve();
572
- }
573
- return new Promise((resolve) => {
574
- const subscription = control.statusChanges.subscribe(() => {
575
- if (!control.pending) {
576
- subscription.unsubscribe();
577
- resolve();
578
- }
579
- });
580
- });
581
- }
582
- function isPlainObject(value) {
583
- return (!!value &&
584
- typeof value === 'object' &&
585
- !Array.isArray(value) &&
586
- Object.prototype.toString.call(value) === '[object Object]');
587
- }
588
- function tryInjectDestroyRef() {
589
- try {
590
- return inject(DestroyRef);
591
- }
592
- catch {
593
- return null;
594
- }
595
- }
596
- function normalizeSyncValidators(base, fieldConfigs) {
597
- const buckets = new Map();
598
- for (const [path, validator] of Object.entries(base)) {
599
- const existing = buckets.get(path) ?? [];
600
- existing.push(validator);
601
- buckets.set(path, existing);
602
- }
603
- for (const [path, config] of Object.entries(fieldConfigs)) {
604
- const validators = toValidatorArray(config.validators);
605
- if (validators.length === 0) {
606
- continue;
607
- }
608
- const existing = buckets.get(path) ?? [];
609
- existing.push(...validators);
610
- buckets.set(path, existing);
611
- }
612
- const normalized = {};
613
- buckets.forEach((validators, path) => {
614
- normalized[path] = (value) => {
615
- for (const validator of validators) {
616
- const result = validator(value);
617
- if (result) {
618
- return result;
619
- }
620
- }
621
- return null;
622
- };
623
- });
624
- return { ...base, ...normalized };
625
- }
626
- function normalizeAsyncValidators(base, fieldConfigs) {
627
- const buckets = new Map();
628
- for (const [path, validator] of Object.entries(base)) {
629
- const existing = buckets.get(path) ?? [];
630
- existing.push(validator);
631
- buckets.set(path, existing);
632
- }
633
- for (const [path, config] of Object.entries(fieldConfigs)) {
634
- const validators = toValidatorArray(config.asyncValidators);
635
- if (validators.length === 0) {
636
- continue;
637
- }
638
- const existing = buckets.get(path) ?? [];
639
- existing.push(...validators);
640
- buckets.set(path, existing);
641
- }
642
- const normalized = {};
643
- buckets.forEach((validators, path) => {
644
- normalized[path] = async (value) => {
645
- for (const validator of validators) {
646
- const maybeAsync = validator(value);
647
- const result = isObservable(maybeAsync)
648
- ? await firstValueFrom(maybeAsync)
649
- : await maybeAsync;
650
- if (result) {
651
- return result;
652
- }
653
- }
654
- return null;
655
- };
656
- });
657
- return { ...base, ...normalized };
658
- }
659
- function toValidatorArray(input) {
660
- if (!input) {
661
- return [];
662
- }
663
- if (Array.isArray(input)) {
664
- return input;
665
- }
666
- return Object.values(input);
667
- }
668
- function hydrateInitialValues(initialValues, persistKey, storage) {
669
- const baseClone = deepClone(initialValues);
670
- if (!persistKey || !storage) {
671
- return { values: baseClone };
672
- }
673
- try {
674
- const storedRaw = storage.getItem(persistKey);
675
- if (!storedRaw) {
676
- return { values: baseClone };
677
- }
678
- const parsed = JSON.parse(storedRaw);
679
- const merged = mergeDeep(baseClone, parsed);
680
- return { values: merged };
681
- }
682
- catch {
683
- return { values: baseClone };
684
- }
685
- }
686
- function assertTreeNode(state) {
687
- if (!state || typeof state !== 'object') {
688
- throw new Error('Invalid state structure for form tree');
689
- }
690
- }
691
- function resolveFieldConfig(fieldConfigs, path) {
692
- if (fieldConfigs[path]) {
693
- return fieldConfigs[path];
694
- }
695
- const keys = Object.keys(fieldConfigs);
696
- let match;
697
- for (const key of keys) {
698
- if (!key.includes('*')) {
699
- continue;
700
- }
701
- if (matchPath(key, path)) {
702
- if (!match || match.key.length < key.length) {
703
- match = { key, config: fieldConfigs[key] };
704
- }
705
- }
706
- }
707
- if (match) {
708
- return match.config;
709
- }
710
- return fieldConfigs['*'];
711
- }
712
- function findValidator(map, path) {
713
- if (map[path]) {
714
- return map[path];
715
- }
716
- let candidate;
717
- for (const key of Object.keys(map)) {
718
- if (!key.includes('*')) {
719
- continue;
720
- }
721
- if (matchPath(key, path)) {
722
- if (!candidate || candidate.key.length < key.length) {
723
- candidate = { key, value: map[key] };
724
- }
725
- }
726
- }
727
- if (candidate) {
728
- return candidate.value;
729
- }
730
- return map['*'];
731
- }
732
- function createPersistController(formGroup, persistKey, storage, debounceMs, cleanupCallbacks) {
733
- if (!persistKey || !storage) {
734
- return {
735
- schedulePersist: () => undefined,
736
- persistImmediately: () => undefined,
737
- };
738
- }
739
- let timer = null;
740
- const persist = () => {
741
- try {
742
- const payload = JSON.stringify(formGroup.getRawValue());
743
- storage.setItem(persistKey, payload);
744
- }
745
- catch {
746
- }
747
- };
748
- const schedulePersist = () => {
749
- if (debounceMs <= 0) {
750
- persist();
751
- return;
752
- }
753
- if (timer) {
754
- clearTimeout(timer);
755
- }
756
- timer = setTimeout(() => {
757
- timer = null;
758
- persist();
759
- }, debounceMs);
760
- };
761
- cleanupCallbacks.push(() => {
762
- if (timer) {
763
- clearTimeout(timer);
764
- }
765
- });
766
- return {
767
- schedulePersist,
768
- persistImmediately: persist,
769
- };
770
- }
771
- class SignalValueDirective {
772
- signalTreeSignalValue;
773
- signalTreeSignalValueChange = new EventEmitter();
774
- elementRef = inject(ElementRef);
775
- renderer = inject(Renderer2);
776
- onChange = () => {
777
- };
778
- onTouched = () => {
779
- };
780
- ngOnInit() {
781
- effect(() => {
782
- const value = this.signalTreeSignalValue();
783
- this.renderer.setProperty(this.elementRef.nativeElement, 'value', value);
784
- });
785
- }
786
- handleChange(event) {
787
- const target = event.target;
788
- const value = target?.value;
789
- if (value !== undefined) {
790
- this.signalTreeSignalValue.set(value);
791
- this.signalTreeSignalValueChange.emit(value);
792
- this.onChange(value);
793
- }
794
- }
795
- handleBlur() {
796
- this.onTouched();
797
- }
798
- writeValue(value) {
799
- if (value !== undefined) {
800
- this.signalTreeSignalValue.set(value);
801
- }
802
- }
803
- registerOnChange(fn) {
804
- this.onChange = fn;
805
- }
806
- registerOnTouched(fn) {
807
- this.onTouched = fn;
808
- }
809
- setDisabledState(isDisabled) {
810
- this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', isDisabled);
811
- }
812
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.11", ngImport: i0, type: SignalValueDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
813
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.11", type: SignalValueDirective, isStandalone: true, selector: "[signalTreeSignalValue]", inputs: { signalTreeSignalValue: "signalTreeSignalValue" }, outputs: { signalTreeSignalValueChange: "signalTreeSignalValueChange" }, host: { listeners: { "input": "handleChange($event)", "change": "handleChange($event)", "blur": "handleBlur()" } }, providers: [
814
- {
815
- provide: NG_VALUE_ACCESSOR,
816
- useExisting: forwardRef(() => SignalValueDirective),
817
- multi: true,
818
- },
819
- ], ngImport: i0 });
820
- }
821
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.11", ngImport: i0, type: SignalValueDirective, decorators: [{
822
- type: Directive,
823
- args: [{
824
- selector: '[signalTreeSignalValue]',
825
- providers: [
826
- {
827
- provide: NG_VALUE_ACCESSOR,
828
- useExisting: forwardRef(() => SignalValueDirective),
829
- multi: true,
830
- },
831
- ],
832
- standalone: true,
833
- }]
834
- }], propDecorators: { signalTreeSignalValue: [{
835
- type: Input
836
- }], signalTreeSignalValueChange: [{
837
- type: Output
838
- }], handleChange: [{
839
- type: HostListener,
840
- args: ['input', ['$event']]
841
- }, {
842
- type: HostListener,
843
- args: ['change', ['$event']]
844
- }], handleBlur: [{
845
- type: HostListener,
846
- args: ['blur']
847
- }] } });
848
- const SIGNAL_FORM_DIRECTIVES = [SignalValueDirective];
849
-
850
- function required(message = 'Required') {
851
- return (value) => (!value ? message : null);
852
- }
853
- function email(message = 'Invalid email') {
854
- return (value) => {
855
- const strValue = value;
856
- return strValue && !strValue.includes('@') ? message : null;
857
- };
858
- }
859
- function minLength(min, message) {
860
- return (value) => {
861
- const strValue = value;
862
- const errorMsg = message ?? `Min ${min} characters`;
863
- return strValue && strValue.length < min ? errorMsg : null;
864
- };
865
- }
866
- function maxLength(max, message) {
867
- return (value) => {
868
- const strValue = value;
869
- const errorMsg = message ?? `Max ${max} characters`;
870
- return strValue && strValue.length > max ? errorMsg : null;
871
- };
872
- }
873
- function pattern(regex, message = 'Invalid format') {
874
- return (value) => {
875
- const strValue = value;
876
- return strValue && !regex.test(strValue) ? message : null;
877
- };
878
- }
879
- function min(min, message) {
880
- return (value) => {
881
- const numValue = value;
882
- const errorMsg = message ?? `Must be at least ${min}`;
883
- return numValue < min ? errorMsg : null;
884
- };
885
- }
886
- function max(max, message) {
887
- return (value) => {
888
- const numValue = value;
889
- const errorMsg = message ?? `Must be at most ${max}`;
890
- return numValue > max ? errorMsg : null;
891
- };
892
- }
893
- function compose(validators) {
894
- return (value) => {
895
- for (const validator of validators) {
896
- const error = validator(value);
897
- if (error) {
898
- return error;
899
- }
900
- }
901
- return null;
902
- };
903
- }
904
-
905
- function unique(checkFn, message = 'Already exists') {
906
- return async (value) => {
907
- if (!value)
908
- return null;
909
- const exists = await checkFn(value);
910
- return exists ? message : null;
911
- };
912
- }
913
- function debounce(validator, delayMs) {
914
- let timeoutId;
915
- return async (value) => {
916
- return new Promise((resolve) => {
917
- clearTimeout(timeoutId);
918
- timeoutId = setTimeout(async () => {
919
- const maybeAsync = validator(value);
920
- const result = isObservable(maybeAsync)
921
- ? await firstValueFrom(maybeAsync)
922
- : await maybeAsync;
923
- resolve(result);
924
- }, delayMs);
925
- });
926
- };
927
- }
928
-
929
- function withFormHistory(formTree, options = {}) {
930
- const capacity = Math.max(1, options.capacity ?? 10);
931
- const historySignal = signal({
932
- past: [],
933
- present: deepClone(formTree.form.getRawValue()),
934
- future: [],
935
- }, ...(ngDevMode ? [{ debugName: "historySignal" }] : []));
936
- let recording = true;
937
- let suppressUpdates = 0;
938
- let internalHistory = {
939
- past: [],
940
- present: deepClone(formTree.form.getRawValue()),
941
- future: [],
942
- };
943
- const subscription = formTree.form.valueChanges.subscribe(() => {
944
- if (suppressUpdates > 0) {
945
- suppressUpdates--;
946
- return;
947
- }
948
- if (!recording) {
949
- internalHistory = {
950
- ...internalHistory,
951
- present: deepClone(formTree.form.getRawValue()),
952
- };
953
- return;
954
- }
955
- const snapshot = deepClone(formTree.form.getRawValue());
956
- if (snapshotsEqual(internalHistory.present, snapshot)) {
957
- internalHistory = {
958
- ...internalHistory,
959
- present: snapshot,
960
- };
961
- historySignal.set(cloneHistory(internalHistory));
962
- return;
963
- }
964
- const updatedPast = [...internalHistory.past, internalHistory.present];
965
- if (updatedPast.length > capacity) {
966
- updatedPast.shift();
967
- }
968
- internalHistory = {
969
- past: updatedPast,
970
- present: snapshot,
971
- future: [],
972
- };
973
- historySignal.set(cloneHistory(internalHistory));
974
- });
975
- const originalDestroy = formTree.destroy;
976
- formTree.destroy = () => {
977
- subscription.unsubscribe();
978
- originalDestroy();
979
- };
980
- const undo = () => {
981
- const history = historySignal();
982
- if (history.past.length === 0) {
983
- return;
984
- }
985
- const previous = deepClone(history.past[history.past.length - 1]);
986
- recording = false;
987
- suppressUpdates++;
988
- formTree.setValues(previous);
989
- recording = true;
990
- internalHistory = {
991
- past: history.past.slice(0, -1),
992
- present: previous,
993
- future: [history.present, ...history.future],
994
- };
995
- historySignal.set(cloneHistory(internalHistory));
996
- };
997
- const redo = () => {
998
- const history = historySignal();
999
- if (history.future.length === 0) {
1000
- return;
1001
- }
1002
- const next = deepClone(history.future[0]);
1003
- recording = false;
1004
- suppressUpdates++;
1005
- formTree.setValues(next);
1006
- recording = true;
1007
- internalHistory = {
1008
- past: [...history.past, history.present],
1009
- present: next,
1010
- future: history.future.slice(1),
1011
- };
1012
- historySignal.set(cloneHistory(internalHistory));
1013
- };
1014
- const clearHistory = () => {
1015
- internalHistory = {
1016
- past: [],
1017
- present: deepClone(formTree.form.getRawValue()),
1018
- future: [],
1019
- };
1020
- historySignal.set(cloneHistory(internalHistory));
1021
- };
1022
- function cloneHistory(state) {
1023
- return {
1024
- past: state.past.map((entry) => deepClone(entry)),
1025
- present: deepClone(state.present),
1026
- future: state.future.map((entry) => deepClone(entry)),
1027
- };
1028
- }
1029
- return Object.assign(formTree, {
1030
- undo,
1031
- redo,
1032
- clearHistory,
1033
- history: historySignal.asReadonly(),
1034
- });
1035
- }
1036
-
1037
- export { FormValidationError, SIGNAL_FORM_DIRECTIVES, SignalValueDirective, compose, createFormTree, createVirtualFormArray, debounce, email, max, maxLength, min, minLength, pattern, required, unique, withFormHistory };
1038
- //# sourceMappingURL=signaltree-ng-forms.mjs.map