@signaltree/ng-forms 3.0.1 → 4.0.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.
- package/README.md +140 -804
- package/fesm2022/signaltree-ng-forms-src-audit.mjs +20 -0
- package/fesm2022/signaltree-ng-forms-src-audit.mjs.map +1 -0
- package/fesm2022/signaltree-ng-forms-src-history.mjs +113 -0
- package/fesm2022/signaltree-ng-forms-src-history.mjs.map +1 -0
- package/fesm2022/signaltree-ng-forms-src-rxjs.mjs +21 -0
- package/fesm2022/signaltree-ng-forms-src-rxjs.mjs.map +1 -0
- package/fesm2022/signaltree-ng-forms.mjs +911 -273
- package/fesm2022/signaltree-ng-forms.mjs.map +1 -1
- package/index.d.ts +74 -31
- package/package.json +20 -7
- package/src/audit/index.d.ts +15 -0
- package/src/history/index.d.ts +24 -0
- package/src/rxjs/index.d.ts +6 -0
|
@@ -1,260 +1,773 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, isSignal,
|
|
3
|
-
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
4
|
-
import { signalTree
|
|
5
|
-
import {
|
|
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';
|
|
6
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';
|
|
7
20
|
function createFormTree(initialValues, config = {}) {
|
|
8
|
-
const { validators = {}, asyncValidators = {}, ...treeConfig } = config;
|
|
9
|
-
const
|
|
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);
|
|
10
28
|
const flattenedState = valuesTree.state;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
...arr.slice(0, index),
|
|
38
|
-
item,
|
|
39
|
-
...arr.slice(index),
|
|
40
|
-
]);
|
|
41
|
-
markDirty();
|
|
42
|
-
};
|
|
43
|
-
enhanced.move = (from, to) => {
|
|
44
|
-
arraySignal.update((arr) => {
|
|
45
|
-
const newArr = [...arr];
|
|
46
|
-
const [item] = newArr.splice(from, 1);
|
|
47
|
-
if (item !== undefined) {
|
|
48
|
-
newArr.splice(to, 0, item);
|
|
49
|
-
}
|
|
50
|
-
return newArr;
|
|
51
|
-
});
|
|
52
|
-
markDirty();
|
|
53
|
-
};
|
|
54
|
-
enhanced.clear = () => {
|
|
55
|
-
arraySignal.set([]);
|
|
56
|
-
markDirty();
|
|
57
|
-
};
|
|
58
|
-
return enhanced;
|
|
59
|
-
};
|
|
60
|
-
const enhanceArraysRecursively = (obj) => {
|
|
61
|
-
for (const key in obj) {
|
|
62
|
-
const value = obj[key];
|
|
63
|
-
if (isSignal(value)) {
|
|
64
|
-
const signalValue = value();
|
|
65
|
-
if (Array.isArray(signalValue)) {
|
|
66
|
-
obj[key] = enhanceArray(value);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else if (typeof value === 'object' &&
|
|
70
|
-
value !== null &&
|
|
71
|
-
!Array.isArray(value)) {
|
|
72
|
-
enhanceArraysRecursively(value);
|
|
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;
|
|
73
55
|
}
|
|
56
|
+
refreshRunner();
|
|
57
|
+
return;
|
|
74
58
|
}
|
|
59
|
+
if (refreshTimer) {
|
|
60
|
+
clearTimeout(refreshTimer);
|
|
61
|
+
}
|
|
62
|
+
refreshTimer = setTimeout(() => {
|
|
63
|
+
refreshTimer = null;
|
|
64
|
+
refreshRunner();
|
|
65
|
+
}, validationBatchMs);
|
|
75
66
|
};
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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);
|
|
89
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));
|
|
90
101
|
}
|
|
91
|
-
return current;
|
|
92
102
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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);
|
|
100
163
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (isSignal(target) && 'set' in target) {
|
|
104
|
-
target.set(value);
|
|
164
|
+
else if (control) {
|
|
165
|
+
control.setValue(value, { emitEvent: true });
|
|
105
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();
|
|
106
187
|
};
|
|
107
188
|
const validate = async (field) => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const validator = validators[fieldPath];
|
|
113
|
-
if (validator) {
|
|
114
|
-
const value = getNestedValue(flattenedState, fieldPath);
|
|
115
|
-
const error = validator(value);
|
|
116
|
-
if (error) {
|
|
117
|
-
errors[fieldPath] = error;
|
|
118
|
-
}
|
|
189
|
+
if (field) {
|
|
190
|
+
const control = formGroup.get(field);
|
|
191
|
+
if (!control) {
|
|
192
|
+
return;
|
|
119
193
|
}
|
|
194
|
+
control.markAsTouched();
|
|
195
|
+
control.updateValueAndValidity({ emitEvent: true });
|
|
196
|
+
refreshAggregates(true);
|
|
197
|
+
await waitForPending(control);
|
|
198
|
+
refreshAggregates(true);
|
|
199
|
+
return;
|
|
120
200
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const value = getNestedValue(flattenedState, fieldPath);
|
|
134
|
-
const result = asyncValidator(value);
|
|
135
|
-
let error;
|
|
136
|
-
if (result instanceof Observable) {
|
|
137
|
-
error = await new Promise((resolve) => {
|
|
138
|
-
result.subscribe({
|
|
139
|
-
next: (val) => resolve(val),
|
|
140
|
-
error: () => resolve('Validation error'),
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
error = await result;
|
|
146
|
-
}
|
|
147
|
-
if (error) {
|
|
148
|
-
asyncErrors[fieldPath] = error;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
asyncErrors[fieldPath] = 'Validation error';
|
|
153
|
-
}
|
|
154
|
-
formSignals.asyncValidating.update((v) => ({
|
|
155
|
-
...v,
|
|
156
|
-
[fieldPath]: false,
|
|
157
|
-
}));
|
|
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());
|
|
158
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);
|
|
159
221
|
}
|
|
160
|
-
formSignals.asyncErrors.set(asyncErrors);
|
|
161
|
-
const hasErrors = Object.keys(errors).length > 0;
|
|
162
|
-
const hasAsyncErrors = Object.keys(asyncErrors).length > 0;
|
|
163
|
-
const isValidating = Object.values(formSignals.asyncValidating()).some((v) => v);
|
|
164
|
-
formSignals.valid.set(!hasErrors && !hasAsyncErrors && !isValidating);
|
|
165
222
|
};
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
fieldErrors[fieldPath] = computed(() => {
|
|
170
|
-
const errors = formSignals.errors();
|
|
171
|
-
return errors[fieldPath];
|
|
172
|
-
});
|
|
173
|
-
fieldAsyncErrors[fieldPath] = computed(() => {
|
|
174
|
-
const errors = formSignals.asyncErrors();
|
|
175
|
-
return errors[fieldPath];
|
|
176
|
-
});
|
|
177
|
-
});
|
|
223
|
+
const destroy = () => {
|
|
224
|
+
cleanupCallbacks.splice(0).forEach((fn) => fn());
|
|
225
|
+
};
|
|
178
226
|
const formTree = {
|
|
179
227
|
state: flattenedState,
|
|
180
228
|
$: flattenedState,
|
|
181
|
-
|
|
229
|
+
form: formGroup,
|
|
230
|
+
errors,
|
|
231
|
+
asyncErrors,
|
|
232
|
+
touched,
|
|
233
|
+
asyncValidating,
|
|
234
|
+
dirty,
|
|
235
|
+
valid,
|
|
236
|
+
submitting,
|
|
182
237
|
unwrap: () => valuesTree(),
|
|
183
|
-
setValue
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
void validate(field);
|
|
188
|
-
},
|
|
189
|
-
setValues: (values) => {
|
|
190
|
-
valuesTree((v) => ({ ...v, ...values }));
|
|
191
|
-
markDirty();
|
|
192
|
-
void validate();
|
|
193
|
-
},
|
|
194
|
-
reset: () => {
|
|
195
|
-
const resetSignals = (current, initial) => {
|
|
196
|
-
for (const [key, initialValue] of Object.entries(initial)) {
|
|
197
|
-
const currentValue = current[key];
|
|
198
|
-
if (isSignal(currentValue) && 'set' in currentValue) {
|
|
199
|
-
currentValue.set(initialValue);
|
|
200
|
-
}
|
|
201
|
-
else if (typeof initialValue === 'object' &&
|
|
202
|
-
initialValue !== null &&
|
|
203
|
-
!Array.isArray(initialValue) &&
|
|
204
|
-
typeof currentValue === 'object' &&
|
|
205
|
-
currentValue !== null &&
|
|
206
|
-
!isSignal(currentValue)) {
|
|
207
|
-
resetSignals(currentValue, initialValue);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
resetSignals(flattenedState, initialValues);
|
|
212
|
-
formSignals.errors.set({});
|
|
213
|
-
formSignals.asyncErrors.set({});
|
|
214
|
-
formSignals.touched.set({});
|
|
215
|
-
formSignals.asyncValidating.set({});
|
|
216
|
-
formSignals.dirty.set(false);
|
|
217
|
-
formSignals.valid.set(true);
|
|
218
|
-
formSignals.submitting.set(false);
|
|
219
|
-
},
|
|
220
|
-
submit: async (submitFn) => {
|
|
221
|
-
formSignals.submitting.set(true);
|
|
222
|
-
try {
|
|
223
|
-
await validate();
|
|
224
|
-
if (!formSignals.valid()) {
|
|
225
|
-
throw new Error('Form is invalid');
|
|
226
|
-
}
|
|
227
|
-
const currentValues = valuesTree();
|
|
228
|
-
const result = await submitFn(currentValues);
|
|
229
|
-
return result;
|
|
230
|
-
}
|
|
231
|
-
finally {
|
|
232
|
-
formSignals.submitting.set(false);
|
|
233
|
-
}
|
|
234
|
-
},
|
|
238
|
+
setValue,
|
|
239
|
+
setValues,
|
|
240
|
+
reset,
|
|
241
|
+
submit,
|
|
235
242
|
validate,
|
|
236
243
|
getFieldError: (field) => fieldErrors[field] || computed(() => undefined),
|
|
237
244
|
getFieldAsyncError: (field) => fieldAsyncErrors[field] || computed(() => undefined),
|
|
238
|
-
getFieldTouched: (field) => computed(() =>
|
|
239
|
-
const touched = formSignals.touched();
|
|
240
|
-
return touched[field];
|
|
241
|
-
}),
|
|
245
|
+
getFieldTouched: (field) => computed(() => formGroup.get(field)?.touched ?? false),
|
|
242
246
|
isFieldValid: (field) => computed(() => {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
const asyncValidating = formSignals.asyncValidating();
|
|
246
|
-
return !errors[field] && !asyncErrors[field] && !asyncValidating[field];
|
|
247
|
-
}),
|
|
248
|
-
isFieldAsyncValidating: (field) => computed(() => {
|
|
249
|
-
const asyncValidating = formSignals.asyncValidating();
|
|
250
|
-
return asyncValidating[field];
|
|
247
|
+
const control = formGroup.get(field);
|
|
248
|
+
return !!control && control.valid && !control.pending;
|
|
251
249
|
}),
|
|
250
|
+
isFieldAsyncValidating: (field) => computed(() => !!formGroup.get(field)?.pending),
|
|
252
251
|
fieldErrors,
|
|
253
252
|
fieldAsyncErrors,
|
|
254
253
|
values: valuesTree,
|
|
254
|
+
destroy,
|
|
255
255
|
};
|
|
256
256
|
return formTree;
|
|
257
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
|
+
}
|
|
258
771
|
class SignalValueDirective {
|
|
259
772
|
signalTreeSignalValue;
|
|
260
773
|
signalTreeSignalValueChange = new EventEmitter();
|
|
@@ -296,8 +809,8 @@ class SignalValueDirective {
|
|
|
296
809
|
setDisabledState(isDisabled) {
|
|
297
810
|
this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', isDisabled);
|
|
298
811
|
}
|
|
299
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
300
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.
|
|
812
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SignalValueDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
813
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.7", type: SignalValueDirective, isStandalone: true, selector: "[signalTreeSignalValue]", inputs: { signalTreeSignalValue: "signalTreeSignalValue" }, outputs: { signalTreeSignalValueChange: "signalTreeSignalValueChange" }, host: { listeners: { "input": "handleChange($event)", "change": "handleChange($event)", "blur": "handleBlur()" } }, providers: [
|
|
301
814
|
{
|
|
302
815
|
provide: NG_VALUE_ACCESSOR,
|
|
303
816
|
useExisting: forwardRef(() => SignalValueDirective),
|
|
@@ -305,7 +818,7 @@ class SignalValueDirective {
|
|
|
305
818
|
},
|
|
306
819
|
], ngImport: i0 });
|
|
307
820
|
}
|
|
308
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
821
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SignalValueDirective, decorators: [{
|
|
309
822
|
type: Directive,
|
|
310
823
|
args: [{
|
|
311
824
|
selector: '[signalTreeSignalValue]',
|
|
@@ -332,69 +845,194 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
|
|
|
332
845
|
type: HostListener,
|
|
333
846
|
args: ['blur']
|
|
334
847
|
}] } });
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
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) => {
|
|
338
855
|
const strValue = value;
|
|
339
856
|
return strValue && !strValue.includes('@') ? message : null;
|
|
340
|
-
}
|
|
341
|
-
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function minLength(min, message) {
|
|
860
|
+
return (value) => {
|
|
342
861
|
const strValue = value;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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) => {
|
|
346
875
|
const strValue = value;
|
|
347
876
|
return strValue && !regex.test(strValue) ? message : null;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
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) => {
|
|
352
907
|
if (!value)
|
|
353
908
|
return null;
|
|
354
909
|
const exists = await checkFn(value);
|
|
355
910
|
return exists ? message : null;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
function
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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;
|
|
365
947
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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,
|
|
369
960
|
};
|
|
961
|
+
historySignal.set(cloneHistory(internalHistory));
|
|
962
|
+
return;
|
|
370
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));
|
|
371
974
|
});
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
after: (action, payload, oldState, newState) => {
|
|
377
|
-
const changes = getChanges(oldState, newState);
|
|
378
|
-
if (Object.keys(changes).length > 0) {
|
|
379
|
-
auditLog.push({
|
|
380
|
-
timestamp: Date.now(),
|
|
381
|
-
changes,
|
|
382
|
-
metadata: getMetadata?.(),
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
},
|
|
975
|
+
const originalDestroy = formTree.destroy;
|
|
976
|
+
formTree.destroy = () => {
|
|
977
|
+
subscription.unsubscribe();
|
|
978
|
+
originalDestroy();
|
|
386
979
|
};
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (oldState[key] !== newState[key]) {
|
|
392
|
-
changes[key] = newState[key];
|
|
980
|
+
const undo = () => {
|
|
981
|
+
const history = historySignal();
|
|
982
|
+
if (history.past.length === 0) {
|
|
983
|
+
return;
|
|
393
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
|
+
};
|
|
394
1028
|
}
|
|
395
|
-
return
|
|
1029
|
+
return Object.assign(formTree, {
|
|
1030
|
+
undo,
|
|
1031
|
+
redo,
|
|
1032
|
+
clearHistory,
|
|
1033
|
+
history: historySignal.asReadonly(),
|
|
1034
|
+
});
|
|
396
1035
|
}
|
|
397
|
-
const SIGNAL_FORM_DIRECTIVES = [SignalValueDirective];
|
|
398
1036
|
|
|
399
|
-
export { SIGNAL_FORM_DIRECTIVES, SignalValueDirective,
|
|
1037
|
+
export { FormValidationError, SIGNAL_FORM_DIRECTIVES, SignalValueDirective, compose, createFormTree, createVirtualFormArray, debounce, email, max, maxLength, min, minLength, pattern, required, unique, withFormHistory };
|
|
400
1038
|
//# sourceMappingURL=signaltree-ng-forms.mjs.map
|