@angular/forms 21.0.0-next.8 → 21.0.0-rc.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/fesm2022/forms.mjs +4828 -7310
- package/fesm2022/forms.mjs.map +1 -1
- package/fesm2022/signals.mjs +1849 -2990
- package/fesm2022/signals.mjs.map +1 -1
- package/package.json +4 -4
- package/types/forms.d.ts +6 -2
- package/types/signals.d.ts +239 -168
package/fesm2022/signals.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @license Angular v21.0.0-
|
|
2
|
+
* @license Angular v21.0.0-rc.0
|
|
3
3
|
* (c) 2010-2025 Google LLC. https://angular.dev/
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -7,3191 +7,2050 @@
|
|
|
7
7
|
import { httpResource } from '@angular/common/http';
|
|
8
8
|
import * as i0 from '@angular/core';
|
|
9
9
|
import { computed, untracked, InjectionToken, inject, Injector, input, ɵCONTROL as _CONTROL, effect, Directive, runInInjectionContext, linkedSignal, signal, APP_ID, ɵisPromise as _isPromise, resource } from '@angular/core';
|
|
10
|
+
import { Validators, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
|
|
10
11
|
import { SIGNAL } from '@angular/core/primitives/signals';
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
* A version of `Array.isArray` that handles narrowing of readonly arrays properly.
|
|
14
|
-
*/
|
|
15
13
|
function isArray(value) {
|
|
16
|
-
|
|
14
|
+
return Array.isArray(value);
|
|
17
15
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Checks if a value is an object.
|
|
20
|
-
*/
|
|
21
16
|
function isObject(value) {
|
|
22
|
-
|
|
17
|
+
return (typeof value === 'object' || typeof value === 'function') && value != null;
|
|
23
18
|
}
|
|
24
19
|
|
|
25
|
-
/**
|
|
26
|
-
* Perform a reduction over a field's children (if any) and return the result.
|
|
27
|
-
*
|
|
28
|
-
* Optionally, the reduction is short circuited based on the provided `shortCircuit` function.
|
|
29
|
-
*/
|
|
30
20
|
function reduceChildren(node, initialValue, fn, shortCircuit) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
21
|
+
const childrenMap = node.structure.childrenMap();
|
|
22
|
+
if (!childrenMap) {
|
|
23
|
+
return initialValue;
|
|
24
|
+
}
|
|
25
|
+
let value = initialValue;
|
|
26
|
+
for (const child of childrenMap.values()) {
|
|
27
|
+
if (shortCircuit?.(value)) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
value = fn(child, value);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
43
33
|
}
|
|
44
|
-
/** A shortCircuit function for reduceChildren that short-circuits if the value is false. */
|
|
45
34
|
function shortCircuitFalse(value) {
|
|
46
|
-
|
|
35
|
+
return !value;
|
|
47
36
|
}
|
|
48
|
-
/** A shortCircuit function for reduceChildren that short-circuits if the value is true. */
|
|
49
37
|
function shortCircuitTrue(value) {
|
|
50
|
-
|
|
38
|
+
return value;
|
|
51
39
|
}
|
|
52
40
|
|
|
53
|
-
/**
|
|
54
|
-
* Helper function taking validation state, and returning own state of the node.
|
|
55
|
-
* @param state
|
|
56
|
-
*/
|
|
57
41
|
function calculateValidationSelfStatus(state) {
|
|
58
|
-
|
|
42
|
+
if (state.errors().length > 0) {
|
|
43
|
+
return 'invalid';
|
|
44
|
+
}
|
|
45
|
+
if (state.pending()) {
|
|
46
|
+
return 'unknown';
|
|
47
|
+
}
|
|
48
|
+
return 'valid';
|
|
49
|
+
}
|
|
50
|
+
class FieldValidationState {
|
|
51
|
+
node;
|
|
52
|
+
constructor(node) {
|
|
53
|
+
this.node = node;
|
|
54
|
+
}
|
|
55
|
+
rawSyncTreeErrors = computed(() => {
|
|
56
|
+
if (this.shouldSkipValidation()) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return [...this.node.logicNode.logic.syncTreeErrors.compute(this.node.context), ...(this.node.structure.parent?.validationState.rawSyncTreeErrors() ?? [])];
|
|
60
|
+
}, ...(ngDevMode ? [{
|
|
61
|
+
debugName: "rawSyncTreeErrors"
|
|
62
|
+
}] : []));
|
|
63
|
+
syncErrors = computed(() => {
|
|
64
|
+
if (this.shouldSkipValidation()) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
return [...this.node.logicNode.logic.syncErrors.compute(this.node.context), ...this.syncTreeErrors(), ...normalizeErrors(this.node.submitState.serverErrors())];
|
|
68
|
+
}, ...(ngDevMode ? [{
|
|
69
|
+
debugName: "syncErrors"
|
|
70
|
+
}] : []));
|
|
71
|
+
syncValid = computed(() => {
|
|
72
|
+
if (this.shouldSkipValidation()) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return reduceChildren(this.node, this.syncErrors().length === 0, (child, value) => value && child.validationState.syncValid(), shortCircuitFalse);
|
|
76
|
+
}, ...(ngDevMode ? [{
|
|
77
|
+
debugName: "syncValid"
|
|
78
|
+
}] : []));
|
|
79
|
+
syncTreeErrors = computed(() => this.rawSyncTreeErrors().filter(err => err.field === this.node.fieldProxy), ...(ngDevMode ? [{
|
|
80
|
+
debugName: "syncTreeErrors"
|
|
81
|
+
}] : []));
|
|
82
|
+
rawAsyncErrors = computed(() => {
|
|
83
|
+
if (this.shouldSkipValidation()) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
return [...this.node.logicNode.logic.asyncErrors.compute(this.node.context), ...(this.node.structure.parent?.validationState.rawAsyncErrors() ?? [])];
|
|
87
|
+
}, ...(ngDevMode ? [{
|
|
88
|
+
debugName: "rawAsyncErrors"
|
|
89
|
+
}] : []));
|
|
90
|
+
asyncErrors = computed(() => {
|
|
91
|
+
if (this.shouldSkipValidation()) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
return this.rawAsyncErrors().filter(err => err === 'pending' || err.field === this.node.fieldProxy);
|
|
95
|
+
}, ...(ngDevMode ? [{
|
|
96
|
+
debugName: "asyncErrors"
|
|
97
|
+
}] : []));
|
|
98
|
+
errors = computed(() => [...this.syncErrors(), ...this.asyncErrors().filter(err => err !== 'pending')], ...(ngDevMode ? [{
|
|
99
|
+
debugName: "errors"
|
|
100
|
+
}] : []));
|
|
101
|
+
errorSummary = computed(() => reduceChildren(this.node, this.errors(), (child, result) => [...result, ...child.errorSummary()]), ...(ngDevMode ? [{
|
|
102
|
+
debugName: "errorSummary"
|
|
103
|
+
}] : []));
|
|
104
|
+
pending = computed(() => reduceChildren(this.node, this.asyncErrors().includes('pending'), (child, value) => value || child.validationState.asyncErrors().includes('pending')), ...(ngDevMode ? [{
|
|
105
|
+
debugName: "pending"
|
|
106
|
+
}] : []));
|
|
107
|
+
status = computed(() => {
|
|
108
|
+
if (this.shouldSkipValidation()) {
|
|
109
|
+
return 'valid';
|
|
110
|
+
}
|
|
111
|
+
let ownStatus = calculateValidationSelfStatus(this);
|
|
112
|
+
return reduceChildren(this.node, ownStatus, (child, value) => {
|
|
113
|
+
if (value === 'invalid' || child.validationState.status() === 'invalid') {
|
|
59
114
|
return 'invalid';
|
|
60
|
-
|
|
61
|
-
if (state.pending()) {
|
|
115
|
+
} else if (value === 'unknown' || child.validationState.status() === 'unknown') {
|
|
62
116
|
return 'unknown';
|
|
63
|
-
|
|
64
|
-
|
|
117
|
+
}
|
|
118
|
+
return 'valid';
|
|
119
|
+
}, v => v === 'invalid');
|
|
120
|
+
}, ...(ngDevMode ? [{
|
|
121
|
+
debugName: "status"
|
|
122
|
+
}] : []));
|
|
123
|
+
valid = computed(() => this.status() === 'valid', ...(ngDevMode ? [{
|
|
124
|
+
debugName: "valid"
|
|
125
|
+
}] : []));
|
|
126
|
+
invalid = computed(() => this.status() === 'invalid', ...(ngDevMode ? [{
|
|
127
|
+
debugName: "invalid"
|
|
128
|
+
}] : []));
|
|
129
|
+
shouldSkipValidation = computed(() => this.node.hidden() || this.node.disabled() || this.node.readonly(), ...(ngDevMode ? [{
|
|
130
|
+
debugName: "shouldSkipValidation"
|
|
131
|
+
}] : []));
|
|
65
132
|
}
|
|
66
|
-
/**
|
|
67
|
-
* The validation state associated with a `FieldNode`.
|
|
68
|
-
*
|
|
69
|
-
* This class collects together various types of errors to represent the full validation state of
|
|
70
|
-
* the field. There are 4 types of errors that need to be combined to determine validity:
|
|
71
|
-
* 1. The synchronous errors produced by the schema logic.
|
|
72
|
-
* 2. The synchronous tree errors produced by the schema logic. Tree errors may apply to a different
|
|
73
|
-
* field than the one that the logic that produced them is bound to. They support targeting the
|
|
74
|
-
* error at an arbitrary descendant field.
|
|
75
|
-
* 3. The asynchronous tree errors produced by the schema logic. These work like synchronous tree
|
|
76
|
-
* errors, except the error list may also contain a special sentinel value indicating that a
|
|
77
|
-
* validator is still running.
|
|
78
|
-
* 4. Server errors are not produced by the schema logic, but instead get imperatively added when a
|
|
79
|
-
* form submit fails with errors.
|
|
80
|
-
*/
|
|
81
|
-
class FieldValidationState {
|
|
82
|
-
node;
|
|
83
|
-
constructor(node) {
|
|
84
|
-
this.node = node;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* The full set of synchronous tree errors visible to this field. This includes ones that are
|
|
88
|
-
* targeted at a descendant field rather than at this field.
|
|
89
|
-
*/
|
|
90
|
-
rawSyncTreeErrors = computed(() => {
|
|
91
|
-
if (this.shouldSkipValidation()) {
|
|
92
|
-
return [];
|
|
93
|
-
}
|
|
94
|
-
return [
|
|
95
|
-
...this.node.logicNode.logic.syncTreeErrors.compute(this.node.context),
|
|
96
|
-
...(this.node.structure.parent?.validationState.rawSyncTreeErrors() ?? []),
|
|
97
|
-
];
|
|
98
|
-
}, ...(ngDevMode ? [{ debugName: "rawSyncTreeErrors" }] : []));
|
|
99
|
-
/**
|
|
100
|
-
* The full set of synchronous errors for this field, including synchronous tree errors and server
|
|
101
|
-
* errors. Server errors are considered "synchronous" because they are imperatively added. From
|
|
102
|
-
* the perspective of the field state they are either there or not, they are never in a pending
|
|
103
|
-
* state.
|
|
104
|
-
*/
|
|
105
|
-
syncErrors = computed(() => {
|
|
106
|
-
// Short-circuit running validators if validation doesn't apply to this field.
|
|
107
|
-
if (this.shouldSkipValidation()) {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
return [
|
|
111
|
-
...this.node.logicNode.logic.syncErrors.compute(this.node.context),
|
|
112
|
-
...this.syncTreeErrors(),
|
|
113
|
-
...normalizeErrors(this.node.submitState.serverErrors()),
|
|
114
|
-
];
|
|
115
|
-
}, ...(ngDevMode ? [{ debugName: "syncErrors" }] : []));
|
|
116
|
-
/**
|
|
117
|
-
* Whether the field is considered valid according solely to its synchronous validators.
|
|
118
|
-
* Errors resulting from a previous submit attempt are also considered for this state.
|
|
119
|
-
*/
|
|
120
|
-
syncValid = computed(() => {
|
|
121
|
-
// Short-circuit checking children if validation doesn't apply to this field.
|
|
122
|
-
if (this.shouldSkipValidation()) {
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
return reduceChildren(this.node, this.syncErrors().length === 0, (child, value) => value && child.validationState.syncValid(), shortCircuitFalse);
|
|
126
|
-
}, ...(ngDevMode ? [{ debugName: "syncValid" }] : []));
|
|
127
|
-
/**
|
|
128
|
-
* The synchronous tree errors visible to this field that are specifically targeted at this field
|
|
129
|
-
* rather than a descendant.
|
|
130
|
-
*/
|
|
131
|
-
syncTreeErrors = computed(() => this.rawSyncTreeErrors().filter((err) => err.field === this.node.fieldProxy), ...(ngDevMode ? [{ debugName: "syncTreeErrors" }] : []));
|
|
132
|
-
/**
|
|
133
|
-
* The full set of asynchronous tree errors visible to this field. This includes ones that are
|
|
134
|
-
* targeted at a descendant field rather than at this field, as well as sentinel 'pending' values
|
|
135
|
-
* indicating that the validator is still running and an error could still occur.
|
|
136
|
-
*/
|
|
137
|
-
rawAsyncErrors = computed(() => {
|
|
138
|
-
// Short-circuit running validators if validation doesn't apply to this field.
|
|
139
|
-
if (this.shouldSkipValidation()) {
|
|
140
|
-
return [];
|
|
141
|
-
}
|
|
142
|
-
return [
|
|
143
|
-
// TODO: add field in `validateAsync` and remove this map
|
|
144
|
-
...this.node.logicNode.logic.asyncErrors.compute(this.node.context),
|
|
145
|
-
// TODO: does it make sense to filter this to errors in this subtree?
|
|
146
|
-
...(this.node.structure.parent?.validationState.rawAsyncErrors() ?? []),
|
|
147
|
-
];
|
|
148
|
-
}, ...(ngDevMode ? [{ debugName: "rawAsyncErrors" }] : []));
|
|
149
|
-
/**
|
|
150
|
-
* The asynchronous tree errors visible to this field that are specifically targeted at this field
|
|
151
|
-
* rather than a descendant. This also includes all 'pending' sentinel values, since those could
|
|
152
|
-
* theoretically result in errors for this field.
|
|
153
|
-
*/
|
|
154
|
-
asyncErrors = computed(() => {
|
|
155
|
-
if (this.shouldSkipValidation()) {
|
|
156
|
-
return [];
|
|
157
|
-
}
|
|
158
|
-
return this.rawAsyncErrors().filter((err) => err === 'pending' || err.field === this.node.fieldProxy);
|
|
159
|
-
}, ...(ngDevMode ? [{ debugName: "asyncErrors" }] : []));
|
|
160
|
-
/**
|
|
161
|
-
* The combined set of all errors that currently apply to this field.
|
|
162
|
-
*/
|
|
163
|
-
errors = computed(() => [
|
|
164
|
-
...this.syncErrors(),
|
|
165
|
-
...this.asyncErrors().filter((err) => err !== 'pending'),
|
|
166
|
-
], ...(ngDevMode ? [{ debugName: "errors" }] : []));
|
|
167
|
-
errorSummary = computed(() => reduceChildren(this.node, this.errors(), (child, result) => [
|
|
168
|
-
...result,
|
|
169
|
-
...child.errorSummary(),
|
|
170
|
-
]), ...(ngDevMode ? [{ debugName: "errorSummary" }] : []));
|
|
171
|
-
/**
|
|
172
|
-
* Whether this field has any asynchronous validators still pending.
|
|
173
|
-
*/
|
|
174
|
-
pending = computed(() => reduceChildren(this.node, this.asyncErrors().includes('pending'), (child, value) => value || child.validationState.asyncErrors().includes('pending')), ...(ngDevMode ? [{ debugName: "pending" }] : []));
|
|
175
|
-
/**
|
|
176
|
-
* The validation status of the field.
|
|
177
|
-
* - The status is 'valid' if neither the field nor any of its children has any errors or pending
|
|
178
|
-
* validators.
|
|
179
|
-
* - The status is 'invalid' if the field or any of its children has an error
|
|
180
|
-
* (regardless of pending validators)
|
|
181
|
-
* - The status is 'unknown' if neither the field nor any of its children has any errors,
|
|
182
|
-
* but the field or any of its children does have a pending validator.
|
|
183
|
-
*
|
|
184
|
-
* A field is considered valid if *all* of the following are true:
|
|
185
|
-
* - It has no errors or pending validators
|
|
186
|
-
* - All of its children are considered valid
|
|
187
|
-
* A field is considered invalid if *any* of the following are true:
|
|
188
|
-
* - It has an error
|
|
189
|
-
* - Any of its children is considered invalid
|
|
190
|
-
* A field is considered to have unknown validity status if it is not valid or invalid.
|
|
191
|
-
*/
|
|
192
|
-
status = computed(() => {
|
|
193
|
-
// Short-circuit checking children if validation doesn't apply to this field.
|
|
194
|
-
if (this.shouldSkipValidation()) {
|
|
195
|
-
return 'valid';
|
|
196
|
-
}
|
|
197
|
-
let ownStatus = calculateValidationSelfStatus(this);
|
|
198
|
-
return reduceChildren(this.node, ownStatus, (child, value) => {
|
|
199
|
-
if (value === 'invalid' || child.validationState.status() === 'invalid') {
|
|
200
|
-
return 'invalid';
|
|
201
|
-
}
|
|
202
|
-
else if (value === 'unknown' || child.validationState.status() === 'unknown') {
|
|
203
|
-
return 'unknown';
|
|
204
|
-
}
|
|
205
|
-
return 'valid';
|
|
206
|
-
}, (v) => v === 'invalid');
|
|
207
|
-
}, ...(ngDevMode ? [{ debugName: "status" }] : []));
|
|
208
|
-
/**
|
|
209
|
-
* Whether the field is considered valid.
|
|
210
|
-
*
|
|
211
|
-
* A field is considered valid if *all* of the following are true:
|
|
212
|
-
* - It has no errors or pending validators
|
|
213
|
-
* - All of its children are considered valid
|
|
214
|
-
*
|
|
215
|
-
* Note: `!valid()` is *not* the same as `invalid()`. Both `valid()` and `invalid()` can be false
|
|
216
|
-
* if there are currently no errors, but validators are still pending.
|
|
217
|
-
*/
|
|
218
|
-
valid = computed(() => this.status() === 'valid', ...(ngDevMode ? [{ debugName: "valid" }] : []));
|
|
219
|
-
/**
|
|
220
|
-
* Whether the field is considered invalid.
|
|
221
|
-
*
|
|
222
|
-
* A field is considered invalid if *any* of the following are true:
|
|
223
|
-
* - It has an error
|
|
224
|
-
* - Any of its children is considered invalid
|
|
225
|
-
*
|
|
226
|
-
* Note: `!invalid()` is *not* the same as `valid()`. Both `valid()` and `invalid()` can be false
|
|
227
|
-
* if there are currently no errors, but validators are still pending.
|
|
228
|
-
*/
|
|
229
|
-
invalid = computed(() => this.status() === 'invalid', ...(ngDevMode ? [{ debugName: "invalid" }] : []));
|
|
230
|
-
/**
|
|
231
|
-
* Indicates whether validation should be skipped for this field because it is hidden, disabled,
|
|
232
|
-
* or readonly.
|
|
233
|
-
*/
|
|
234
|
-
shouldSkipValidation = computed(() => this.node.hidden() || this.node.disabled() || this.node.readonly(), ...(ngDevMode ? [{ debugName: "shouldSkipValidation" }] : []));
|
|
235
|
-
}
|
|
236
|
-
/** Normalizes a validation result to a list of validation errors. */
|
|
237
133
|
function normalizeErrors(error) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
134
|
+
if (error === undefined) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
if (isArray(error)) {
|
|
138
|
+
return error;
|
|
139
|
+
}
|
|
140
|
+
return [error];
|
|
245
141
|
}
|
|
246
142
|
function addDefaultField(errors, field) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
else if (errors) {
|
|
253
|
-
errors.field ??= field;
|
|
143
|
+
if (isArray(errors)) {
|
|
144
|
+
for (const error of errors) {
|
|
145
|
+
error.field ??= field;
|
|
254
146
|
}
|
|
255
|
-
|
|
147
|
+
} else if (errors) {
|
|
148
|
+
errors.field ??= field;
|
|
149
|
+
}
|
|
150
|
+
return errors;
|
|
256
151
|
}
|
|
257
152
|
|
|
258
|
-
/**
|
|
259
|
-
* Special key which is used to represent a dynamic logic property in a `FieldPathNode` path.
|
|
260
|
-
* This property is used to represent logic that applies to every element of some dynamic form data
|
|
261
|
-
* (i.e. an array).
|
|
262
|
-
*
|
|
263
|
-
* For example, a rule like `applyEach(p.myArray, () => { ... })` will add logic to the `DYNAMIC`
|
|
264
|
-
* property of `p.myArray`.
|
|
265
|
-
*/
|
|
266
153
|
const DYNAMIC = Symbol();
|
|
267
|
-
/** Represents a result that should be ignored because its predicate indicates it is not active. */
|
|
268
154
|
const IGNORED = Symbol();
|
|
269
|
-
/**
|
|
270
|
-
* Base class for all logic. It is responsible for combining the results from multiple individual
|
|
271
|
-
* logic functions registered in the schema, and using them to derive the value for some associated
|
|
272
|
-
* piece of field state.
|
|
273
|
-
*/
|
|
274
155
|
class AbstractLogic {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
this.fns.push(wrapWithPredicates(this.predicates, logicFn));
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Merges in the logic from another logic instance, subject to the predicates of both the other
|
|
292
|
-
* instance and this instance.
|
|
293
|
-
*/
|
|
294
|
-
mergeIn(other) {
|
|
295
|
-
const fns = this.predicates
|
|
296
|
-
? other.fns.map((fn) => wrapWithPredicates(this.predicates, fn))
|
|
297
|
-
: other.fns;
|
|
298
|
-
this.fns.push(...fns);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
/** Logic that combines its individual logic function results with logical OR. */
|
|
156
|
+
predicates;
|
|
157
|
+
fns = [];
|
|
158
|
+
constructor(predicates) {
|
|
159
|
+
this.predicates = predicates;
|
|
160
|
+
}
|
|
161
|
+
push(logicFn) {
|
|
162
|
+
this.fns.push(wrapWithPredicates(this.predicates, logicFn));
|
|
163
|
+
}
|
|
164
|
+
mergeIn(other) {
|
|
165
|
+
const fns = this.predicates ? other.fns.map(fn => wrapWithPredicates(this.predicates, fn)) : other.fns;
|
|
166
|
+
this.fns.push(...fns);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
302
169
|
class BooleanOrLogic extends AbstractLogic {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
170
|
+
get defaultValue() {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
compute(arg) {
|
|
174
|
+
return this.fns.some(f => {
|
|
175
|
+
const result = f(arg);
|
|
176
|
+
return result && result !== IGNORED;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
312
179
|
}
|
|
313
|
-
/**
|
|
314
|
-
* Logic that combines its individual logic function results by aggregating them in an array.
|
|
315
|
-
* Depending on its `ignore` function it may ignore certain values, omitting them from the array.
|
|
316
|
-
*/
|
|
317
180
|
class ArrayMergeIgnoreLogic extends AbstractLogic {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
}, []);
|
|
346
|
-
}
|
|
181
|
+
ignore;
|
|
182
|
+
static ignoreNull(predicates) {
|
|
183
|
+
return new ArrayMergeIgnoreLogic(predicates, e => e === null);
|
|
184
|
+
}
|
|
185
|
+
constructor(predicates, ignore) {
|
|
186
|
+
super(predicates);
|
|
187
|
+
this.ignore = ignore;
|
|
188
|
+
}
|
|
189
|
+
get defaultValue() {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
compute(arg) {
|
|
193
|
+
return this.fns.reduce((prev, f) => {
|
|
194
|
+
const value = f(arg);
|
|
195
|
+
if (value === undefined || value === IGNORED) {
|
|
196
|
+
return prev;
|
|
197
|
+
} else if (isArray(value)) {
|
|
198
|
+
return [...prev, ...(this.ignore ? value.filter(e => !this.ignore(e)) : value)];
|
|
199
|
+
} else {
|
|
200
|
+
if (this.ignore && this.ignore(value)) {
|
|
201
|
+
return prev;
|
|
202
|
+
}
|
|
203
|
+
return [...prev, value];
|
|
204
|
+
}
|
|
205
|
+
}, []);
|
|
206
|
+
}
|
|
347
207
|
}
|
|
348
|
-
/** Logic that combines its individual logic function results by aggregating them in an array. */
|
|
349
208
|
class ArrayMergeLogic extends ArrayMergeIgnoreLogic {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
209
|
+
constructor(predicates) {
|
|
210
|
+
super(predicates, undefined);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
class AggregateMetadataMergeLogic extends AbstractLogic {
|
|
214
|
+
key;
|
|
215
|
+
get defaultValue() {
|
|
216
|
+
return this.key.getInitial();
|
|
217
|
+
}
|
|
218
|
+
constructor(predicates, key) {
|
|
219
|
+
super(predicates);
|
|
220
|
+
this.key = key;
|
|
221
|
+
}
|
|
222
|
+
compute(ctx) {
|
|
223
|
+
if (this.fns.length === 0) {
|
|
224
|
+
return this.key.getInitial();
|
|
225
|
+
}
|
|
226
|
+
let acc = this.key.getInitial();
|
|
227
|
+
for (let i = 0; i < this.fns.length; i++) {
|
|
228
|
+
const item = this.fns[i](ctx);
|
|
229
|
+
if (item !== IGNORED) {
|
|
230
|
+
acc = this.key.reduce(acc, item);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return acc;
|
|
234
|
+
}
|
|
353
235
|
}
|
|
354
|
-
/** Logic that combines an AggregateProperty according to the property's own reduce function. */
|
|
355
|
-
class AggregatePropertyMergeLogic extends AbstractLogic {
|
|
356
|
-
key;
|
|
357
|
-
get defaultValue() {
|
|
358
|
-
return this.key.getInitial();
|
|
359
|
-
}
|
|
360
|
-
constructor(predicates, key) {
|
|
361
|
-
super(predicates);
|
|
362
|
-
this.key = key;
|
|
363
|
-
}
|
|
364
|
-
compute(ctx) {
|
|
365
|
-
if (this.fns.length === 0) {
|
|
366
|
-
return this.key.getInitial();
|
|
367
|
-
}
|
|
368
|
-
let acc = this.key.getInitial();
|
|
369
|
-
for (let i = 0; i < this.fns.length; i++) {
|
|
370
|
-
const item = this.fns[i](ctx);
|
|
371
|
-
if (item !== IGNORED) {
|
|
372
|
-
acc = this.key.reduce(acc, item);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return acc;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Wraps a logic function such that it returns the special `IGNORED` sentinel value if any of the
|
|
380
|
-
* given predicates evaluates to false.
|
|
381
|
-
*
|
|
382
|
-
* @param predicates A list of bound predicates to apply to the logic function
|
|
383
|
-
* @param logicFn The logic function to wrap
|
|
384
|
-
* @returns A wrapped version of the logic function that may return `IGNORED`.
|
|
385
|
-
*/
|
|
386
236
|
function wrapWithPredicates(predicates, logicFn) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (!predicate.fn(predicateField.context)) {
|
|
404
|
-
return IGNORED;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
return logicFn(arg);
|
|
408
|
-
};
|
|
237
|
+
if (predicates.length === 0) {
|
|
238
|
+
return logicFn;
|
|
239
|
+
}
|
|
240
|
+
return arg => {
|
|
241
|
+
for (const predicate of predicates) {
|
|
242
|
+
let predicateField = arg.stateOf(predicate.path);
|
|
243
|
+
const depthDiff = untracked(predicateField.structure.pathKeys).length - predicate.depth;
|
|
244
|
+
for (let i = 0; i < depthDiff; i++) {
|
|
245
|
+
predicateField = predicateField.structure.parent;
|
|
246
|
+
}
|
|
247
|
+
if (!predicate.fn(predicateField.context)) {
|
|
248
|
+
return IGNORED;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return logicFn(arg);
|
|
252
|
+
};
|
|
409
253
|
}
|
|
410
|
-
/**
|
|
411
|
-
* Container for all the different types of logic that can be applied to a field
|
|
412
|
-
* (disabled, hidden, errors, etc.)
|
|
413
|
-
*/
|
|
414
254
|
class LogicContainer {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
*/
|
|
469
|
-
getAggregateProperty(prop) {
|
|
470
|
-
if (!this.aggregateProperties.has(prop)) {
|
|
471
|
-
this.aggregateProperties.set(prop, new AggregatePropertyMergeLogic(this.predicates, prop));
|
|
472
|
-
}
|
|
473
|
-
return this.aggregateProperties.get(prop);
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Adds a factory function for a given property.
|
|
477
|
-
* @param prop The `Property` to associate the factory with.
|
|
478
|
-
* @param factory The factory function.
|
|
479
|
-
* @throws If a factory is already defined for the given key.
|
|
480
|
-
*/
|
|
481
|
-
addPropertyFactory(prop, factory) {
|
|
482
|
-
if (this.propertyFactories.has(prop)) {
|
|
483
|
-
// TODO: name of the property?
|
|
484
|
-
throw new Error(`Can't define value twice for the same Property`);
|
|
485
|
-
}
|
|
486
|
-
this.propertyFactories.set(prop, factory);
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* Merges logic from another `Logic` instance into this one.
|
|
490
|
-
* @param other The `Logic` instance to merge from.
|
|
491
|
-
*/
|
|
492
|
-
mergeIn(other) {
|
|
493
|
-
this.hidden.mergeIn(other.hidden);
|
|
494
|
-
this.disabledReasons.mergeIn(other.disabledReasons);
|
|
495
|
-
this.readonly.mergeIn(other.readonly);
|
|
496
|
-
this.syncErrors.mergeIn(other.syncErrors);
|
|
497
|
-
this.syncTreeErrors.mergeIn(other.syncTreeErrors);
|
|
498
|
-
this.asyncErrors.mergeIn(other.asyncErrors);
|
|
499
|
-
for (const [prop, propertyLogic] of other.getAggregatePropertyEntries()) {
|
|
500
|
-
this.getAggregateProperty(prop).mergeIn(propertyLogic);
|
|
501
|
-
}
|
|
502
|
-
for (const [prop, propertyFactory] of other.getPropertyFactoryEntries()) {
|
|
503
|
-
this.addPropertyFactory(prop, propertyFactory);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
255
|
+
predicates;
|
|
256
|
+
hidden;
|
|
257
|
+
disabledReasons;
|
|
258
|
+
readonly;
|
|
259
|
+
syncErrors;
|
|
260
|
+
syncTreeErrors;
|
|
261
|
+
asyncErrors;
|
|
262
|
+
aggregateMetadataKeys = new Map();
|
|
263
|
+
metadataFactories = new Map();
|
|
264
|
+
constructor(predicates) {
|
|
265
|
+
this.predicates = predicates;
|
|
266
|
+
this.hidden = new BooleanOrLogic(predicates);
|
|
267
|
+
this.disabledReasons = new ArrayMergeLogic(predicates);
|
|
268
|
+
this.readonly = new BooleanOrLogic(predicates);
|
|
269
|
+
this.syncErrors = ArrayMergeIgnoreLogic.ignoreNull(predicates);
|
|
270
|
+
this.syncTreeErrors = ArrayMergeIgnoreLogic.ignoreNull(predicates);
|
|
271
|
+
this.asyncErrors = ArrayMergeIgnoreLogic.ignoreNull(predicates);
|
|
272
|
+
}
|
|
273
|
+
hasAggregateMetadata(key) {
|
|
274
|
+
return this.aggregateMetadataKeys.has(key);
|
|
275
|
+
}
|
|
276
|
+
getAggregateMetadataEntries() {
|
|
277
|
+
return this.aggregateMetadataKeys.entries();
|
|
278
|
+
}
|
|
279
|
+
getMetadataFactoryEntries() {
|
|
280
|
+
return this.metadataFactories.entries();
|
|
281
|
+
}
|
|
282
|
+
getAggregateMetadata(key) {
|
|
283
|
+
if (!this.aggregateMetadataKeys.has(key)) {
|
|
284
|
+
this.aggregateMetadataKeys.set(key, new AggregateMetadataMergeLogic(this.predicates, key));
|
|
285
|
+
}
|
|
286
|
+
return this.aggregateMetadataKeys.get(key);
|
|
287
|
+
}
|
|
288
|
+
addMetadataFactory(key, factory) {
|
|
289
|
+
if (this.metadataFactories.has(key)) {
|
|
290
|
+
throw new Error(`Can't define value twice for the same MetadataKey`);
|
|
291
|
+
}
|
|
292
|
+
this.metadataFactories.set(key, factory);
|
|
293
|
+
}
|
|
294
|
+
mergeIn(other) {
|
|
295
|
+
this.hidden.mergeIn(other.hidden);
|
|
296
|
+
this.disabledReasons.mergeIn(other.disabledReasons);
|
|
297
|
+
this.readonly.mergeIn(other.readonly);
|
|
298
|
+
this.syncErrors.mergeIn(other.syncErrors);
|
|
299
|
+
this.syncTreeErrors.mergeIn(other.syncTreeErrors);
|
|
300
|
+
this.asyncErrors.mergeIn(other.asyncErrors);
|
|
301
|
+
for (const [key, metadataLogic] of other.getAggregateMetadataEntries()) {
|
|
302
|
+
this.getAggregateMetadata(key).mergeIn(metadataLogic);
|
|
303
|
+
}
|
|
304
|
+
for (const [key, metadataFactory] of other.getMetadataFactoryEntries()) {
|
|
305
|
+
this.addMetadataFactory(key, metadataFactory);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
506
308
|
}
|
|
507
309
|
|
|
508
310
|
let boundPathDepth = 0;
|
|
509
|
-
/**
|
|
510
|
-
* The depth of the current path when evaluating a logic function.
|
|
511
|
-
* Do not set this directly, it is a context variable managed by `setBoundPathDepthForResolution`.
|
|
512
|
-
*/
|
|
513
311
|
function getBoundPathDepth() {
|
|
514
|
-
|
|
312
|
+
return boundPathDepth;
|
|
515
313
|
}
|
|
516
|
-
/**
|
|
517
|
-
* Sets the bound path depth for the duration of the given logic function.
|
|
518
|
-
* This is used to ensure that the field resolution algorithm walks far enough up the field tree to
|
|
519
|
-
* reach the point where the root of the path we're bound to is applied. This normally isn't a big
|
|
520
|
-
* concern, but matters when we're dealing with recursive structures.
|
|
521
|
-
*
|
|
522
|
-
* Consider this example:
|
|
523
|
-
*
|
|
524
|
-
* ```
|
|
525
|
-
* const s = schema(p => {
|
|
526
|
-
* disabled(p.next, ({valueOf}) => valueOf(p.data));
|
|
527
|
-
* apply(p.next, s);
|
|
528
|
-
* });
|
|
529
|
-
* ```
|
|
530
|
-
*
|
|
531
|
-
* Here we need to know that the `disabled` logic was bound to a path of depth 1. Otherwise we'd
|
|
532
|
-
* attempt to resolve `p.data` in the context of the field corresponding to `p.next`.
|
|
533
|
-
* The resolution algorithm would start with the field for `p.next` and see that it *does* contain
|
|
534
|
-
* the logic for `s` (due to the fact that its recursively applied.) It would then decide not to
|
|
535
|
-
* walk up the field tree at all, and to immediately start walking down the keys for the target path
|
|
536
|
-
* `p.data`, leading it to grab the field corresponding to `p.next.data`.
|
|
537
|
-
*
|
|
538
|
-
* We avoid the problem described above by keeping track of the depth (relative to Schema root) of
|
|
539
|
-
* the path we were bound to. We then require the resolution algorithm to walk at least that far up
|
|
540
|
-
* the tree before finding a node that contains the logic for `s`.
|
|
541
|
-
*
|
|
542
|
-
* @param fn A logic function that is bound to a particular path
|
|
543
|
-
* @param depth The depth in the field tree of the field the logic is bound to
|
|
544
|
-
* @returns A version of the logic function that is aware of its depth.
|
|
545
|
-
*/
|
|
546
314
|
function setBoundPathDepthForResolution(fn, depth) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
};
|
|
315
|
+
return (...args) => {
|
|
316
|
+
try {
|
|
317
|
+
boundPathDepth = depth;
|
|
318
|
+
return fn(...args);
|
|
319
|
+
} finally {
|
|
320
|
+
boundPathDepth = 0;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
556
323
|
}
|
|
557
324
|
|
|
558
|
-
/**
|
|
559
|
-
* Abstract base class for building a `LogicNode`.
|
|
560
|
-
* This class defines the interface for adding various logic rules (e.g., hidden, disabled)
|
|
561
|
-
* and data factories to a node in the logic tree.
|
|
562
|
-
* LogicNodeBuilders are 1:1 with nodes in the Schema tree.
|
|
563
|
-
*/
|
|
564
325
|
class AbstractLogicNodeBuilder {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
* Builds the `LogicNode` from the accumulated rules and child builders.
|
|
573
|
-
* @returns The constructed `LogicNode`.
|
|
574
|
-
*/
|
|
575
|
-
build() {
|
|
576
|
-
return new LeafLogicNode(this, [], 0);
|
|
577
|
-
}
|
|
326
|
+
depth;
|
|
327
|
+
constructor(depth) {
|
|
328
|
+
this.depth = depth;
|
|
329
|
+
}
|
|
330
|
+
build() {
|
|
331
|
+
return new LeafLogicNode(this, [], 0);
|
|
332
|
+
}
|
|
578
333
|
}
|
|
579
|
-
/**
|
|
580
|
-
* A builder for `LogicNode`. Used to add logic to the final `LogicNode` tree.
|
|
581
|
-
* This builder supports merging multiple sources of logic, potentially with predicates,
|
|
582
|
-
* preserving the order of rule application.
|
|
583
|
-
*/
|
|
584
334
|
class LogicNodeBuilder extends AbstractLogicNodeBuilder {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
else {
|
|
654
|
-
this.all.push({ builder: other });
|
|
655
|
-
}
|
|
656
|
-
this.current = undefined;
|
|
657
|
-
}
|
|
658
|
-
/**
|
|
659
|
-
* Gets the current `NonMergeableLogicNodeBuilder` for adding rules directly to this
|
|
660
|
-
* `LogicNodeBuilder`. If no current builder exists, a new one is created.
|
|
661
|
-
* The current builder is cleared whenever `mergeIn` is called to preserve the order
|
|
662
|
-
* of rules when merging separate builder trees.
|
|
663
|
-
* @returns The current `NonMergeableLogicNodeBuilder`.
|
|
664
|
-
*/
|
|
665
|
-
getCurrent() {
|
|
666
|
-
if (this.current === undefined) {
|
|
667
|
-
this.current = new NonMergeableLogicNodeBuilder(this.depth);
|
|
668
|
-
this.all.push({ builder: this.current });
|
|
669
|
-
}
|
|
670
|
-
return this.current;
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Creates a new root `LogicNodeBuilder`.
|
|
674
|
-
* @returns A new instance of `LogicNodeBuilder`.
|
|
675
|
-
*/
|
|
676
|
-
static newRoot() {
|
|
677
|
-
return new LogicNodeBuilder(0);
|
|
678
|
-
}
|
|
335
|
+
constructor(depth) {
|
|
336
|
+
super(depth);
|
|
337
|
+
}
|
|
338
|
+
current;
|
|
339
|
+
all = [];
|
|
340
|
+
addHiddenRule(logic) {
|
|
341
|
+
this.getCurrent().addHiddenRule(logic);
|
|
342
|
+
}
|
|
343
|
+
addDisabledReasonRule(logic) {
|
|
344
|
+
this.getCurrent().addDisabledReasonRule(logic);
|
|
345
|
+
}
|
|
346
|
+
addReadonlyRule(logic) {
|
|
347
|
+
this.getCurrent().addReadonlyRule(logic);
|
|
348
|
+
}
|
|
349
|
+
addSyncErrorRule(logic) {
|
|
350
|
+
this.getCurrent().addSyncErrorRule(logic);
|
|
351
|
+
}
|
|
352
|
+
addSyncTreeErrorRule(logic) {
|
|
353
|
+
this.getCurrent().addSyncTreeErrorRule(logic);
|
|
354
|
+
}
|
|
355
|
+
addAsyncErrorRule(logic) {
|
|
356
|
+
this.getCurrent().addAsyncErrorRule(logic);
|
|
357
|
+
}
|
|
358
|
+
addAggregateMetadataRule(key, logic) {
|
|
359
|
+
this.getCurrent().addAggregateMetadataRule(key, logic);
|
|
360
|
+
}
|
|
361
|
+
addMetadataFactory(key, factory) {
|
|
362
|
+
this.getCurrent().addMetadataFactory(key, factory);
|
|
363
|
+
}
|
|
364
|
+
getChild(key) {
|
|
365
|
+
return this.getCurrent().getChild(key);
|
|
366
|
+
}
|
|
367
|
+
hasLogic(builder) {
|
|
368
|
+
if (this === builder) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
return this.all.some(({
|
|
372
|
+
builder: subBuilder
|
|
373
|
+
}) => subBuilder.hasLogic(builder));
|
|
374
|
+
}
|
|
375
|
+
mergeIn(other, predicate) {
|
|
376
|
+
if (predicate) {
|
|
377
|
+
this.all.push({
|
|
378
|
+
builder: other,
|
|
379
|
+
predicate: {
|
|
380
|
+
fn: setBoundPathDepthForResolution(predicate.fn, this.depth),
|
|
381
|
+
path: predicate.path
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
} else {
|
|
385
|
+
this.all.push({
|
|
386
|
+
builder: other
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
this.current = undefined;
|
|
390
|
+
}
|
|
391
|
+
getCurrent() {
|
|
392
|
+
if (this.current === undefined) {
|
|
393
|
+
this.current = new NonMergeableLogicNodeBuilder(this.depth);
|
|
394
|
+
this.all.push({
|
|
395
|
+
builder: this.current
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return this.current;
|
|
399
|
+
}
|
|
400
|
+
static newRoot() {
|
|
401
|
+
return new LogicNodeBuilder(0);
|
|
402
|
+
}
|
|
679
403
|
}
|
|
680
|
-
/**
|
|
681
|
-
* A type of `AbstractLogicNodeBuilder` used internally by the `LogicNodeBuilder` to record "pure"
|
|
682
|
-
* chunks of logic that do not require merging in other builders.
|
|
683
|
-
*/
|
|
684
404
|
class NonMergeableLogicNodeBuilder extends AbstractLogicNodeBuilder {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
return this.children.get(key);
|
|
724
|
-
}
|
|
725
|
-
hasLogic(builder) {
|
|
726
|
-
return this === builder;
|
|
727
|
-
}
|
|
405
|
+
logic = new LogicContainer([]);
|
|
406
|
+
children = new Map();
|
|
407
|
+
constructor(depth) {
|
|
408
|
+
super(depth);
|
|
409
|
+
}
|
|
410
|
+
addHiddenRule(logic) {
|
|
411
|
+
this.logic.hidden.push(setBoundPathDepthForResolution(logic, this.depth));
|
|
412
|
+
}
|
|
413
|
+
addDisabledReasonRule(logic) {
|
|
414
|
+
this.logic.disabledReasons.push(setBoundPathDepthForResolution(logic, this.depth));
|
|
415
|
+
}
|
|
416
|
+
addReadonlyRule(logic) {
|
|
417
|
+
this.logic.readonly.push(setBoundPathDepthForResolution(logic, this.depth));
|
|
418
|
+
}
|
|
419
|
+
addSyncErrorRule(logic) {
|
|
420
|
+
this.logic.syncErrors.push(setBoundPathDepthForResolution(logic, this.depth));
|
|
421
|
+
}
|
|
422
|
+
addSyncTreeErrorRule(logic) {
|
|
423
|
+
this.logic.syncTreeErrors.push(setBoundPathDepthForResolution(logic, this.depth));
|
|
424
|
+
}
|
|
425
|
+
addAsyncErrorRule(logic) {
|
|
426
|
+
this.logic.asyncErrors.push(setBoundPathDepthForResolution(logic, this.depth));
|
|
427
|
+
}
|
|
428
|
+
addAggregateMetadataRule(key, logic) {
|
|
429
|
+
this.logic.getAggregateMetadata(key).push(setBoundPathDepthForResolution(logic, this.depth));
|
|
430
|
+
}
|
|
431
|
+
addMetadataFactory(key, factory) {
|
|
432
|
+
this.logic.addMetadataFactory(key, setBoundPathDepthForResolution(factory, this.depth));
|
|
433
|
+
}
|
|
434
|
+
getChild(key) {
|
|
435
|
+
if (!this.children.has(key)) {
|
|
436
|
+
this.children.set(key, new LogicNodeBuilder(this.depth + 1));
|
|
437
|
+
}
|
|
438
|
+
return this.children.get(key);
|
|
439
|
+
}
|
|
440
|
+
hasLogic(builder) {
|
|
441
|
+
return this === builder;
|
|
442
|
+
}
|
|
728
443
|
}
|
|
729
|
-
/**
|
|
730
|
-
* A tree structure of `Logic` corresponding to a tree of fields.
|
|
731
|
-
* This implementation represents a leaf in the sense that its logic is derived
|
|
732
|
-
* from a single builder.
|
|
733
|
-
*/
|
|
734
444
|
class LeafLogicNode {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
else if (childBuilders.length === 1) {
|
|
768
|
-
const { builder, predicates } = childBuilders[0];
|
|
769
|
-
return new LeafLogicNode(builder, [...this.predicates, ...predicates.map((p) => bindLevel(p, this.depth))], this.depth + 1);
|
|
770
|
-
}
|
|
771
|
-
else {
|
|
772
|
-
const builtNodes = childBuilders.map(({ builder, predicates }) => new LeafLogicNode(builder, [...this.predicates, ...predicates.map((p) => bindLevel(p, this.depth))], this.depth + 1));
|
|
773
|
-
return new CompositeLogicNode(builtNodes);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* Checks whether the logic from a particular `AbstractLogicNodeBuilder` has been merged into this
|
|
778
|
-
* node.
|
|
779
|
-
* @param builder The builder to check for.
|
|
780
|
-
* @returns True if the builder has been merged, false otherwise.
|
|
781
|
-
*/
|
|
782
|
-
hasLogic(builder) {
|
|
783
|
-
return this.builder?.hasLogic(builder) ?? false;
|
|
784
|
-
}
|
|
445
|
+
builder;
|
|
446
|
+
predicates;
|
|
447
|
+
depth;
|
|
448
|
+
logic;
|
|
449
|
+
constructor(builder, predicates, depth) {
|
|
450
|
+
this.builder = builder;
|
|
451
|
+
this.predicates = predicates;
|
|
452
|
+
this.depth = depth;
|
|
453
|
+
this.logic = builder ? createLogic(builder, predicates, depth) : new LogicContainer([]);
|
|
454
|
+
}
|
|
455
|
+
getChild(key) {
|
|
456
|
+
const childBuilders = this.builder ? getAllChildBuilders(this.builder, key) : [];
|
|
457
|
+
if (childBuilders.length === 0) {
|
|
458
|
+
return new LeafLogicNode(undefined, [], this.depth + 1);
|
|
459
|
+
} else if (childBuilders.length === 1) {
|
|
460
|
+
const {
|
|
461
|
+
builder,
|
|
462
|
+
predicates
|
|
463
|
+
} = childBuilders[0];
|
|
464
|
+
return new LeafLogicNode(builder, [...this.predicates, ...predicates.map(p => bindLevel(p, this.depth))], this.depth + 1);
|
|
465
|
+
} else {
|
|
466
|
+
const builtNodes = childBuilders.map(({
|
|
467
|
+
builder,
|
|
468
|
+
predicates
|
|
469
|
+
}) => new LeafLogicNode(builder, [...this.predicates, ...predicates.map(p => bindLevel(p, this.depth))], this.depth + 1));
|
|
470
|
+
return new CompositeLogicNode(builtNodes);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
hasLogic(builder) {
|
|
474
|
+
return this.builder?.hasLogic(builder) ?? false;
|
|
475
|
+
}
|
|
785
476
|
}
|
|
786
|
-
/**
|
|
787
|
-
* A `LogicNode` that represents the composition of multiple `LogicNode` instances.
|
|
788
|
-
* This is used when logic for a particular path is contributed by several distinct
|
|
789
|
-
* builder branches that need to be merged.
|
|
790
|
-
*/
|
|
791
477
|
class CompositeLogicNode {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
* Retrieves the child `LogicNode` by composing the results of `getChild` from all
|
|
808
|
-
* underlying `LogicNode` instances.
|
|
809
|
-
* @param key The property key of the child.
|
|
810
|
-
* @returns A `CompositeLogicNode` representing the composed child.
|
|
811
|
-
*/
|
|
812
|
-
getChild(key) {
|
|
813
|
-
return new CompositeLogicNode(this.all.flatMap((child) => child.getChild(key)));
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Checks whether the logic from a particular `AbstractLogicNodeBuilder` has been merged into this
|
|
817
|
-
* node.
|
|
818
|
-
* @param builder The builder to check for.
|
|
819
|
-
* @returns True if the builder has been merged, false otherwise.
|
|
820
|
-
*/
|
|
821
|
-
hasLogic(builder) {
|
|
822
|
-
return this.all.some((node) => node.hasLogic(builder));
|
|
823
|
-
}
|
|
478
|
+
all;
|
|
479
|
+
logic;
|
|
480
|
+
constructor(all) {
|
|
481
|
+
this.all = all;
|
|
482
|
+
this.logic = new LogicContainer([]);
|
|
483
|
+
for (const node of all) {
|
|
484
|
+
this.logic.mergeIn(node.logic);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
getChild(key) {
|
|
488
|
+
return new CompositeLogicNode(this.all.flatMap(child => child.getChild(key)));
|
|
489
|
+
}
|
|
490
|
+
hasLogic(builder) {
|
|
491
|
+
return this.all.some(node => node.hasLogic(builder));
|
|
492
|
+
}
|
|
824
493
|
}
|
|
825
|
-
/**
|
|
826
|
-
* Gets all of the builders that contribute logic to the given child of the parent builder.
|
|
827
|
-
* This function recursively traverses the builder hierarchy.
|
|
828
|
-
* @param builder The parent `AbstractLogicNodeBuilder`.
|
|
829
|
-
* @param key The property key of the child.
|
|
830
|
-
* @returns An array of objects, each containing a `LogicNodeBuilder` for the child and any associated predicates.
|
|
831
|
-
*/
|
|
832
494
|
function getAllChildBuilders(builder, key) {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
})
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
495
|
+
if (builder instanceof LogicNodeBuilder) {
|
|
496
|
+
return builder.all.flatMap(({
|
|
497
|
+
builder,
|
|
498
|
+
predicate
|
|
499
|
+
}) => {
|
|
500
|
+
const children = getAllChildBuilders(builder, key);
|
|
501
|
+
if (predicate) {
|
|
502
|
+
return children.map(({
|
|
503
|
+
builder,
|
|
504
|
+
predicates
|
|
505
|
+
}) => ({
|
|
506
|
+
builder,
|
|
507
|
+
predicates: [...predicates, predicate]
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
return children;
|
|
511
|
+
});
|
|
512
|
+
} else if (builder instanceof NonMergeableLogicNodeBuilder) {
|
|
513
|
+
if (builder.children.has(key)) {
|
|
514
|
+
return [{
|
|
515
|
+
builder: builder.children.get(key),
|
|
516
|
+
predicates: []
|
|
517
|
+
}];
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
throw new Error('Unknown LogicNodeBuilder type');
|
|
521
|
+
}
|
|
522
|
+
return [];
|
|
854
523
|
}
|
|
855
|
-
/**
|
|
856
|
-
* Creates the full `Logic` for a given builder.
|
|
857
|
-
* This function handles different types of builders (`LogicNodeBuilder`, `NonMergeableLogicNodeBuilder`)
|
|
858
|
-
* and applies the provided predicates.
|
|
859
|
-
* @param builder The `AbstractLogicNodeBuilder` to process.
|
|
860
|
-
* @param predicates Predicates to apply to the logic derived from the builder.
|
|
861
|
-
* @param depth The depth in the field tree of the field which this logic applies to.
|
|
862
|
-
* @returns The `Logic` instance.
|
|
863
|
-
*/
|
|
864
524
|
function createLogic(builder, predicates, depth) {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
525
|
+
const logic = new LogicContainer(predicates);
|
|
526
|
+
if (builder instanceof LogicNodeBuilder) {
|
|
527
|
+
const builtNodes = builder.all.map(({
|
|
528
|
+
builder,
|
|
529
|
+
predicate
|
|
530
|
+
}) => new LeafLogicNode(builder, predicate ? [...predicates, bindLevel(predicate, depth)] : predicates, depth));
|
|
531
|
+
for (const node of builtNodes) {
|
|
532
|
+
logic.mergeIn(node.logic);
|
|
533
|
+
}
|
|
534
|
+
} else if (builder instanceof NonMergeableLogicNodeBuilder) {
|
|
535
|
+
logic.mergeIn(builder.logic);
|
|
536
|
+
} else {
|
|
537
|
+
throw new Error('Unknown LogicNodeBuilder type');
|
|
538
|
+
}
|
|
539
|
+
return logic;
|
|
879
540
|
}
|
|
880
|
-
/**
|
|
881
|
-
* Create a bound version of the given predicate to a specific depth in the field tree.
|
|
882
|
-
* This allows us to unambiguously know which `FieldContext` the predicate function should receive.
|
|
883
|
-
*
|
|
884
|
-
* This is of particular concern when a schema is applied recursively to itself. Since the schema is
|
|
885
|
-
* only compiled once, each nested application adds the same predicate instance. We differentiate
|
|
886
|
-
* these by recording the depth of the field they're bound to.
|
|
887
|
-
*
|
|
888
|
-
* @param predicate The unbound predicate
|
|
889
|
-
* @param depth The depth of the field the predicate is bound to
|
|
890
|
-
* @returns A bound predicate
|
|
891
|
-
*/
|
|
892
541
|
function bindLevel(predicate, depth) {
|
|
893
|
-
|
|
542
|
+
return {
|
|
543
|
+
...predicate,
|
|
544
|
+
depth: depth
|
|
545
|
+
};
|
|
894
546
|
}
|
|
895
547
|
|
|
896
|
-
/**
|
|
897
|
-
* Special key which is used to retrieve the `FieldPathNode` instance from its `FieldPath` proxy wrapper.
|
|
898
|
-
*/
|
|
899
548
|
const PATH = Symbol('PATH');
|
|
900
|
-
/**
|
|
901
|
-
* A path in the schema on which logic is stored so that it can be added to the corresponding field
|
|
902
|
-
* when the field is created.
|
|
903
|
-
*/
|
|
904
549
|
class FieldPathNode {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
* @param predicate A predicate indicating when the merged in logic should be active.
|
|
947
|
-
*/
|
|
948
|
-
mergeIn(other, predicate) {
|
|
949
|
-
const path = other.compile();
|
|
950
|
-
this.logic.mergeIn(path.logic, predicate);
|
|
951
|
-
}
|
|
952
|
-
/** Extracts the underlying path node from the given path proxy. */
|
|
953
|
-
static unwrapFieldPath(formPath) {
|
|
954
|
-
return formPath[PATH];
|
|
955
|
-
}
|
|
956
|
-
/** Creates a new root path node to be passed in to a schema function. */
|
|
957
|
-
static newRoot() {
|
|
958
|
-
return new FieldPathNode([], LogicNodeBuilder.newRoot(), undefined);
|
|
959
|
-
}
|
|
550
|
+
keys;
|
|
551
|
+
parent;
|
|
552
|
+
keyInParent;
|
|
553
|
+
root;
|
|
554
|
+
children = new Map();
|
|
555
|
+
fieldPathProxy = new Proxy(this, FIELD_PATH_PROXY_HANDLER);
|
|
556
|
+
logicBuilder;
|
|
557
|
+
constructor(keys, root, parent, keyInParent) {
|
|
558
|
+
this.keys = keys;
|
|
559
|
+
this.parent = parent;
|
|
560
|
+
this.keyInParent = keyInParent;
|
|
561
|
+
this.root = root ?? this;
|
|
562
|
+
if (!parent) {
|
|
563
|
+
this.logicBuilder = LogicNodeBuilder.newRoot();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
get builder() {
|
|
567
|
+
if (this.logicBuilder) {
|
|
568
|
+
return this.logicBuilder;
|
|
569
|
+
}
|
|
570
|
+
return this.parent.builder.getChild(this.keyInParent);
|
|
571
|
+
}
|
|
572
|
+
get element() {
|
|
573
|
+
return this.getChild(DYNAMIC);
|
|
574
|
+
}
|
|
575
|
+
getChild(key) {
|
|
576
|
+
if (!this.children.has(key)) {
|
|
577
|
+
this.children.set(key, new FieldPathNode([...this.keys, key], this.root, this, key));
|
|
578
|
+
}
|
|
579
|
+
return this.children.get(key);
|
|
580
|
+
}
|
|
581
|
+
mergeIn(other, predicate) {
|
|
582
|
+
const path = other.compile();
|
|
583
|
+
this.builder.mergeIn(path.builder, predicate);
|
|
584
|
+
}
|
|
585
|
+
static unwrapFieldPath(formPath) {
|
|
586
|
+
return formPath[PATH];
|
|
587
|
+
}
|
|
588
|
+
static newRoot() {
|
|
589
|
+
return new FieldPathNode([], undefined, undefined, undefined);
|
|
590
|
+
}
|
|
960
591
|
}
|
|
961
|
-
/** Proxy handler which implements `FieldPath` on top of a `FieldPathNode`. */
|
|
962
592
|
const FIELD_PATH_PROXY_HANDLER = {
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
593
|
+
get(node, property) {
|
|
594
|
+
if (property === PATH) {
|
|
595
|
+
return node;
|
|
596
|
+
}
|
|
597
|
+
return node.getChild(property).fieldPathProxy;
|
|
598
|
+
}
|
|
969
599
|
};
|
|
970
600
|
|
|
971
|
-
/**
|
|
972
|
-
* Keeps track of the path node for the schema function that is currently being compiled. This is
|
|
973
|
-
* used to detect erroneous references to a path node outside of the context of its schema function.
|
|
974
|
-
* Do not set this directly, it is a context variable managed by `SchemaImpl.compile`.
|
|
975
|
-
*/
|
|
976
601
|
let currentCompilingNode = undefined;
|
|
977
|
-
/**
|
|
978
|
-
* A cache of all schemas compiled under the current root compilation. This is used to avoid doing
|
|
979
|
-
* extra work when compiling a schema that reuses references to the same sub-schema. For example:
|
|
980
|
-
*
|
|
981
|
-
* ```
|
|
982
|
-
* const sub = schema(p => ...);
|
|
983
|
-
* const s = schema(p => {
|
|
984
|
-
* apply(p.a, sub);
|
|
985
|
-
* apply(p.b, sub);
|
|
986
|
-
* });
|
|
987
|
-
* ```
|
|
988
|
-
*
|
|
989
|
-
* This also ensures that we don't go into an infinite loop when compiling a schema that references
|
|
990
|
-
* itself.
|
|
991
|
-
*
|
|
992
|
-
* Do not directly add or remove entries from this map, it is a context variable managed by
|
|
993
|
-
* `SchemaImpl.compile` and `SchemaImpl.rootCompile`.
|
|
994
|
-
*/
|
|
995
602
|
const compiledSchemas = new Map();
|
|
996
|
-
/**
|
|
997
|
-
* Implements the `Schema` concept.
|
|
998
|
-
*/
|
|
999
603
|
class SchemaImpl {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
*/
|
|
1040
|
-
static rootCompile(schema) {
|
|
1041
|
-
try {
|
|
1042
|
-
compiledSchemas.clear();
|
|
1043
|
-
if (schema === undefined) {
|
|
1044
|
-
return FieldPathNode.newRoot();
|
|
1045
|
-
}
|
|
1046
|
-
if (schema instanceof SchemaImpl) {
|
|
1047
|
-
return schema.compile();
|
|
1048
|
-
}
|
|
1049
|
-
return new SchemaImpl(schema).compile();
|
|
1050
|
-
}
|
|
1051
|
-
finally {
|
|
1052
|
-
// Use a try/finally to ensure we properly reset the compilation context upon completion,
|
|
1053
|
-
// even if there are errors while compiling the schema.
|
|
1054
|
-
compiledSchemas.clear();
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
604
|
+
schemaFn;
|
|
605
|
+
constructor(schemaFn) {
|
|
606
|
+
this.schemaFn = schemaFn;
|
|
607
|
+
}
|
|
608
|
+
compile() {
|
|
609
|
+
if (compiledSchemas.has(this)) {
|
|
610
|
+
return compiledSchemas.get(this);
|
|
611
|
+
}
|
|
612
|
+
const path = FieldPathNode.newRoot();
|
|
613
|
+
compiledSchemas.set(this, path);
|
|
614
|
+
let prevCompilingNode = currentCompilingNode;
|
|
615
|
+
try {
|
|
616
|
+
currentCompilingNode = path;
|
|
617
|
+
this.schemaFn(path.fieldPathProxy);
|
|
618
|
+
} finally {
|
|
619
|
+
currentCompilingNode = prevCompilingNode;
|
|
620
|
+
}
|
|
621
|
+
return path;
|
|
622
|
+
}
|
|
623
|
+
static create(schema) {
|
|
624
|
+
if (schema instanceof SchemaImpl) {
|
|
625
|
+
return schema;
|
|
626
|
+
}
|
|
627
|
+
return new SchemaImpl(schema);
|
|
628
|
+
}
|
|
629
|
+
static rootCompile(schema) {
|
|
630
|
+
try {
|
|
631
|
+
compiledSchemas.clear();
|
|
632
|
+
if (schema === undefined) {
|
|
633
|
+
return FieldPathNode.newRoot();
|
|
634
|
+
}
|
|
635
|
+
if (schema instanceof SchemaImpl) {
|
|
636
|
+
return schema.compile();
|
|
637
|
+
}
|
|
638
|
+
return new SchemaImpl(schema).compile();
|
|
639
|
+
} finally {
|
|
640
|
+
compiledSchemas.clear();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
1057
643
|
}
|
|
1058
|
-
/** Checks if the given value is a schema or schema function. */
|
|
1059
644
|
function isSchemaOrSchemaFn(value) {
|
|
1060
|
-
|
|
645
|
+
return value instanceof SchemaImpl || typeof value === 'function';
|
|
1061
646
|
}
|
|
1062
|
-
/** Checks that a path node belongs to the schema function currently being compiled. */
|
|
1063
647
|
function assertPathIsCurrent(path) {
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
648
|
+
if (currentCompilingNode !== FieldPathNode.unwrapFieldPath(path).root) {
|
|
649
|
+
throw new Error(`A FieldPath can only be used directly within the Schema that owns it,` + ` **not** outside of it or within a sub-schema.`);
|
|
650
|
+
}
|
|
1068
651
|
}
|
|
1069
652
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
class
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
653
|
+
class MetadataKey {
|
|
654
|
+
brand;
|
|
655
|
+
constructor() {}
|
|
656
|
+
}
|
|
657
|
+
function createMetadataKey() {
|
|
658
|
+
return new MetadataKey();
|
|
659
|
+
}
|
|
660
|
+
class AggregateMetadataKey {
|
|
661
|
+
reduce;
|
|
662
|
+
getInitial;
|
|
663
|
+
brand;
|
|
664
|
+
constructor(reduce, getInitial) {
|
|
665
|
+
this.reduce = reduce;
|
|
666
|
+
this.getInitial = getInitial;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function reducedMetadataKey(reduce, getInitial) {
|
|
670
|
+
return new AggregateMetadataKey(reduce, getInitial);
|
|
671
|
+
}
|
|
672
|
+
function listMetadataKey() {
|
|
673
|
+
return reducedMetadataKey((acc, item) => item === undefined ? acc : [...acc, item], () => []);
|
|
674
|
+
}
|
|
675
|
+
function minMetadataKey() {
|
|
676
|
+
return reducedMetadataKey((prev, next) => {
|
|
677
|
+
if (prev === undefined) {
|
|
678
|
+
return next;
|
|
679
|
+
}
|
|
680
|
+
if (next === undefined) {
|
|
681
|
+
return prev;
|
|
682
|
+
}
|
|
683
|
+
return Math.min(prev, next);
|
|
684
|
+
}, () => undefined);
|
|
685
|
+
}
|
|
686
|
+
function maxMetadataKey() {
|
|
687
|
+
return reducedMetadataKey((prev, next) => {
|
|
688
|
+
if (prev === undefined) {
|
|
689
|
+
return next;
|
|
690
|
+
}
|
|
691
|
+
if (next === undefined) {
|
|
692
|
+
return prev;
|
|
693
|
+
}
|
|
694
|
+
return Math.max(prev, next);
|
|
695
|
+
}, () => undefined);
|
|
696
|
+
}
|
|
697
|
+
function orMetadataKey() {
|
|
698
|
+
return reducedMetadataKey((prev, next) => prev || next, () => false);
|
|
699
|
+
}
|
|
700
|
+
function andMetadataKey() {
|
|
701
|
+
return reducedMetadataKey((prev, next) => prev && next, () => true);
|
|
702
|
+
}
|
|
703
|
+
const REQUIRED = orMetadataKey();
|
|
704
|
+
const MIN = maxMetadataKey();
|
|
705
|
+
const MAX = minMetadataKey();
|
|
706
|
+
const MIN_LENGTH = maxMetadataKey();
|
|
707
|
+
const MAX_LENGTH = minMetadataKey();
|
|
708
|
+
const PATTERN = listMetadataKey();
|
|
709
|
+
|
|
710
|
+
function requiredError(options) {
|
|
711
|
+
return new RequiredValidationError(options);
|
|
1081
712
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
*
|
|
1085
|
-
* @experimental 21.0.0
|
|
1086
|
-
*/
|
|
1087
|
-
function createProperty() {
|
|
1088
|
-
return new Property();
|
|
713
|
+
function minError(min, options) {
|
|
714
|
+
return new MinValidationError(min, options);
|
|
1089
715
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
716
|
+
function maxError(max, options) {
|
|
717
|
+
return new MaxValidationError(max, options);
|
|
718
|
+
}
|
|
719
|
+
function minLengthError(minLength, options) {
|
|
720
|
+
return new MinLengthValidationError(minLength, options);
|
|
721
|
+
}
|
|
722
|
+
function maxLengthError(maxLength, options) {
|
|
723
|
+
return new MaxLengthValidationError(maxLength, options);
|
|
724
|
+
}
|
|
725
|
+
function patternError(pattern, options) {
|
|
726
|
+
return new PatternValidationError(pattern, options);
|
|
727
|
+
}
|
|
728
|
+
function emailError(options) {
|
|
729
|
+
return new EmailValidationError(options);
|
|
730
|
+
}
|
|
731
|
+
function standardSchemaError(issue, options) {
|
|
732
|
+
return new StandardSchemaValidationError(issue, options);
|
|
733
|
+
}
|
|
734
|
+
function customError(obj) {
|
|
735
|
+
return new CustomValidationError(obj);
|
|
736
|
+
}
|
|
737
|
+
class CustomValidationError {
|
|
738
|
+
__brand = undefined;
|
|
739
|
+
kind = '';
|
|
740
|
+
field;
|
|
741
|
+
message;
|
|
742
|
+
constructor(options) {
|
|
743
|
+
if (options) {
|
|
744
|
+
Object.assign(this, options);
|
|
1106
745
|
}
|
|
746
|
+
}
|
|
1107
747
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
748
|
+
class _NgValidationError {
|
|
749
|
+
__brand = undefined;
|
|
750
|
+
kind = '';
|
|
751
|
+
field;
|
|
752
|
+
message;
|
|
753
|
+
constructor(options) {
|
|
754
|
+
if (options) {
|
|
755
|
+
Object.assign(this, options);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
1118
758
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
*
|
|
1122
|
-
* @experimental 21.0.0
|
|
1123
|
-
*/
|
|
1124
|
-
function listProperty() {
|
|
1125
|
-
return reducedProperty((acc, item) => (item === undefined ? acc : [...acc, item]), () => []);
|
|
759
|
+
class RequiredValidationError extends _NgValidationError {
|
|
760
|
+
kind = 'required';
|
|
1126
761
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
if (prev === undefined) {
|
|
1135
|
-
return next;
|
|
1136
|
-
}
|
|
1137
|
-
if (next === undefined) {
|
|
1138
|
-
return prev;
|
|
1139
|
-
}
|
|
1140
|
-
return Math.min(prev, next);
|
|
1141
|
-
}, () => undefined);
|
|
762
|
+
class MinValidationError extends _NgValidationError {
|
|
763
|
+
min;
|
|
764
|
+
kind = 'min';
|
|
765
|
+
constructor(min, options) {
|
|
766
|
+
super(options);
|
|
767
|
+
this.min = min;
|
|
768
|
+
}
|
|
1142
769
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
if (prev === undefined) {
|
|
1151
|
-
return next;
|
|
1152
|
-
}
|
|
1153
|
-
if (next === undefined) {
|
|
1154
|
-
return prev;
|
|
1155
|
-
}
|
|
1156
|
-
return Math.max(prev, next);
|
|
1157
|
-
}, () => undefined);
|
|
770
|
+
class MaxValidationError extends _NgValidationError {
|
|
771
|
+
max;
|
|
772
|
+
kind = 'max';
|
|
773
|
+
constructor(max, options) {
|
|
774
|
+
super(options);
|
|
775
|
+
this.max = max;
|
|
776
|
+
}
|
|
1158
777
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
778
|
+
class MinLengthValidationError extends _NgValidationError {
|
|
779
|
+
minLength;
|
|
780
|
+
kind = 'minLength';
|
|
781
|
+
constructor(minLength, options) {
|
|
782
|
+
super(options);
|
|
783
|
+
this.minLength = minLength;
|
|
784
|
+
}
|
|
1166
785
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
786
|
+
class MaxLengthValidationError extends _NgValidationError {
|
|
787
|
+
maxLength;
|
|
788
|
+
kind = 'maxLength';
|
|
789
|
+
constructor(maxLength, options) {
|
|
790
|
+
super(options);
|
|
791
|
+
this.maxLength = maxLength;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
class PatternValidationError extends _NgValidationError {
|
|
795
|
+
pattern;
|
|
796
|
+
kind = 'pattern';
|
|
797
|
+
constructor(pattern, options) {
|
|
798
|
+
super(options);
|
|
799
|
+
this.pattern = pattern;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
class EmailValidationError extends _NgValidationError {
|
|
803
|
+
kind = 'email';
|
|
804
|
+
}
|
|
805
|
+
class StandardSchemaValidationError extends _NgValidationError {
|
|
806
|
+
issue;
|
|
807
|
+
kind = 'standardSchema';
|
|
808
|
+
constructor(issue, options) {
|
|
809
|
+
super(options);
|
|
810
|
+
this.issue = issue;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const NgValidationError = _NgValidationError;
|
|
814
|
+
|
|
815
|
+
function getLengthOrSize(value) {
|
|
816
|
+
const v = value;
|
|
817
|
+
return typeof v.length === 'number' ? v.length : v.size;
|
|
818
|
+
}
|
|
819
|
+
function getOption(opt, ctx) {
|
|
820
|
+
return opt instanceof Function ? opt(ctx) : opt;
|
|
821
|
+
}
|
|
822
|
+
function isEmpty(value) {
|
|
823
|
+
if (typeof value === 'number') {
|
|
824
|
+
return isNaN(value);
|
|
825
|
+
}
|
|
826
|
+
return value === '' || value === false || value == null;
|
|
827
|
+
}
|
|
828
|
+
function isPlainError(error) {
|
|
829
|
+
return typeof error === 'object' && (Object.getPrototypeOf(error) === Object.prototype || Object.getPrototypeOf(error) === null);
|
|
830
|
+
}
|
|
831
|
+
function ensureCustomValidationError(error) {
|
|
832
|
+
if (isPlainError(error)) {
|
|
833
|
+
return customError(error);
|
|
834
|
+
}
|
|
835
|
+
return error;
|
|
836
|
+
}
|
|
837
|
+
function ensureCustomValidationResult(result) {
|
|
838
|
+
if (result === null || result === undefined) {
|
|
839
|
+
return result;
|
|
840
|
+
}
|
|
841
|
+
if (isArray(result)) {
|
|
842
|
+
return result.map(ensureCustomValidationError);
|
|
843
|
+
}
|
|
844
|
+
return ensureCustomValidationError(result);
|
|
1174
845
|
}
|
|
1175
|
-
/**
|
|
1176
|
-
* An aggregate property representing whether the field is required.
|
|
1177
|
-
*
|
|
1178
|
-
* @category validation
|
|
1179
|
-
* @experimental 21.0.0
|
|
1180
|
-
*/
|
|
1181
|
-
const REQUIRED = orProperty();
|
|
1182
|
-
/**
|
|
1183
|
-
* An aggregate property representing the min value of the field.
|
|
1184
|
-
*
|
|
1185
|
-
* @category validation
|
|
1186
|
-
* @experimental 21.0.0
|
|
1187
|
-
*/
|
|
1188
|
-
const MIN = maxProperty();
|
|
1189
|
-
/**
|
|
1190
|
-
* An aggregate property representing the max value of the field.
|
|
1191
|
-
*
|
|
1192
|
-
* @category validation
|
|
1193
|
-
* @experimental 21.0.0
|
|
1194
|
-
*/
|
|
1195
|
-
const MAX = minProperty();
|
|
1196
|
-
/**
|
|
1197
|
-
* An aggregate property representing the min length of the field.
|
|
1198
|
-
*
|
|
1199
|
-
* @category validation
|
|
1200
|
-
* @experimental 21.0.0
|
|
1201
|
-
*/
|
|
1202
|
-
const MIN_LENGTH = maxProperty();
|
|
1203
|
-
/**
|
|
1204
|
-
* An aggregate property representing the max length of the field.
|
|
1205
|
-
*
|
|
1206
|
-
* @category validation
|
|
1207
|
-
* @experimental 21.0.0
|
|
1208
|
-
*/
|
|
1209
|
-
const MAX_LENGTH = minProperty();
|
|
1210
|
-
/**
|
|
1211
|
-
* An aggregate property representing the patterns the field must match.
|
|
1212
|
-
*
|
|
1213
|
-
* @category validation
|
|
1214
|
-
* @experimental 21.0.0
|
|
1215
|
-
*/
|
|
1216
|
-
const PATTERN = listProperty();
|
|
1217
846
|
|
|
1218
|
-
/**
|
|
1219
|
-
* Adds logic to a field to conditionally disable it. A disabled field does not contribute to the
|
|
1220
|
-
* validation, touched/dirty, or other state of its parent field.
|
|
1221
|
-
*
|
|
1222
|
-
* @param path The target path to add the disabled logic to.
|
|
1223
|
-
* @param logic A reactive function that returns `true` (or a string reason) when the field is disabled,
|
|
1224
|
-
* and `false` when it is not disabled.
|
|
1225
|
-
* @template TValue The type of value stored in the field the logic is bound to.
|
|
1226
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
1227
|
-
*
|
|
1228
|
-
* @category logic
|
|
1229
|
-
* @experimental 21.0.0
|
|
1230
|
-
*/
|
|
1231
847
|
function disabled(path, logic) {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
}
|
|
848
|
+
assertPathIsCurrent(path);
|
|
849
|
+
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
|
850
|
+
pathNode.builder.addDisabledReasonRule(ctx => {
|
|
851
|
+
let result = true;
|
|
852
|
+
if (typeof logic === 'string') {
|
|
853
|
+
result = logic;
|
|
854
|
+
} else if (logic) {
|
|
855
|
+
result = logic(ctx);
|
|
856
|
+
}
|
|
857
|
+
if (typeof result === 'string') {
|
|
858
|
+
return {
|
|
859
|
+
field: ctx.field,
|
|
860
|
+
message: result
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
return result ? {
|
|
864
|
+
field: ctx.field
|
|
865
|
+
} : undefined;
|
|
866
|
+
});
|
|
1247
867
|
}
|
|
1248
|
-
/**
|
|
1249
|
-
* Adds logic to a field to conditionally make it readonly. A readonly field does not contribute to
|
|
1250
|
-
* the validation, touched/dirty, or other state of its parent field.
|
|
1251
|
-
*
|
|
1252
|
-
* @param path The target path to make readonly.
|
|
1253
|
-
* @param logic A reactive function that returns `true` when the field is readonly.
|
|
1254
|
-
* @template TValue The type of value stored in the field the logic is bound to.
|
|
1255
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
1256
|
-
*
|
|
1257
|
-
* @category logic
|
|
1258
|
-
* @experimental 21.0.0
|
|
1259
|
-
*/
|
|
1260
868
|
function readonly(path, logic = () => true) {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
869
|
+
assertPathIsCurrent(path);
|
|
870
|
+
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
|
871
|
+
pathNode.builder.addReadonlyRule(logic);
|
|
1264
872
|
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Adds logic to a field to conditionally hide it. A hidden field does not contribute to the
|
|
1267
|
-
* validation, touched/dirty, or other state of its parent field.
|
|
1268
|
-
*
|
|
1269
|
-
* If a field may be hidden it is recommended to guard it with an `@if` in the template:
|
|
1270
|
-
* ```
|
|
1271
|
-
* @if (!email().hidden()) {
|
|
1272
|
-
* <label for="email">Email</label>
|
|
1273
|
-
* <input id="email" type="email" [control]="email" />
|
|
1274
|
-
* }
|
|
1275
|
-
* ```
|
|
1276
|
-
*
|
|
1277
|
-
* @param path The target path to add the hidden logic to.
|
|
1278
|
-
* @param logic A reactive function that returns `true` when the field is hidden.
|
|
1279
|
-
* @template TValue The type of value stored in the field the logic is bound to.
|
|
1280
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
1281
|
-
*
|
|
1282
|
-
* @category logic
|
|
1283
|
-
* @experimental 21.0.0
|
|
1284
|
-
*/
|
|
1285
873
|
function hidden(path, logic) {
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
874
|
+
assertPathIsCurrent(path);
|
|
875
|
+
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
|
876
|
+
pathNode.builder.addHiddenRule(logic);
|
|
1289
877
|
}
|
|
1290
|
-
/**
|
|
1291
|
-
* Adds logic to a field to determine if the field has validation errors.
|
|
1292
|
-
*
|
|
1293
|
-
* @param path The target path to add the validation logic to.
|
|
1294
|
-
* @param logic A `Validator` that returns the current validation errors.
|
|
1295
|
-
* @template TValue The type of value stored in the field the logic is bound to.
|
|
1296
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
1297
|
-
*
|
|
1298
|
-
* @category logic
|
|
1299
|
-
* @experimental 21.0.0
|
|
1300
|
-
*/
|
|
1301
878
|
function validate(path, logic) {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
879
|
+
assertPathIsCurrent(path);
|
|
880
|
+
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
|
881
|
+
pathNode.builder.addSyncErrorRule(ctx => {
|
|
882
|
+
return ensureCustomValidationResult(addDefaultField(logic(ctx), ctx.field));
|
|
883
|
+
});
|
|
1305
884
|
}
|
|
1306
|
-
/**
|
|
1307
|
-
* Adds logic to a field to determine if the field or any of its child fields has validation errors.
|
|
1308
|
-
*
|
|
1309
|
-
* @param path The target path to add the validation logic to.
|
|
1310
|
-
* @param logic A `TreeValidator` that returns the current validation errors.
|
|
1311
|
-
* Errors returned by the validator may specify a target field to indicate an error on a child field.
|
|
1312
|
-
* @template TValue The type of value stored in the field the logic is bound to.
|
|
1313
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
1314
|
-
*
|
|
1315
|
-
* @category logic
|
|
1316
|
-
* @experimental 21.0.0
|
|
1317
|
-
*/
|
|
1318
885
|
function validateTree(path, logic) {
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
function property(path, ...rest) {
|
|
1342
|
-
assertPathIsCurrent(path);
|
|
1343
|
-
let key;
|
|
1344
|
-
let factory;
|
|
1345
|
-
if (rest.length === 2) {
|
|
1346
|
-
[key, factory] = rest;
|
|
1347
|
-
}
|
|
1348
|
-
else {
|
|
1349
|
-
[factory] = rest;
|
|
1350
|
-
}
|
|
1351
|
-
key ??= createProperty();
|
|
1352
|
-
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
|
1353
|
-
pathNode.logic.addPropertyFactory(key, factory);
|
|
1354
|
-
return key;
|
|
886
|
+
assertPathIsCurrent(path);
|
|
887
|
+
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
|
888
|
+
pathNode.builder.addSyncTreeErrorRule(ctx => addDefaultField(logic(ctx), ctx.field));
|
|
889
|
+
}
|
|
890
|
+
function aggregateMetadata(path, key, logic) {
|
|
891
|
+
assertPathIsCurrent(path);
|
|
892
|
+
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
|
893
|
+
pathNode.builder.addAggregateMetadataRule(key, logic);
|
|
894
|
+
}
|
|
895
|
+
function metadata(path, ...rest) {
|
|
896
|
+
assertPathIsCurrent(path);
|
|
897
|
+
let key;
|
|
898
|
+
let factory;
|
|
899
|
+
if (rest.length === 2) {
|
|
900
|
+
[key, factory] = rest;
|
|
901
|
+
} else {
|
|
902
|
+
[factory] = rest;
|
|
903
|
+
}
|
|
904
|
+
key ??= createMetadataKey();
|
|
905
|
+
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
|
906
|
+
pathNode.builder.addMetadataFactory(key, factory);
|
|
907
|
+
return key;
|
|
1355
908
|
}
|
|
1356
909
|
|
|
1357
|
-
/**
|
|
1358
|
-
* Adds async validation to the field corresponding to the given path based on a resource.
|
|
1359
|
-
* Async validation for a field only runs once all synchronous validation is passing.
|
|
1360
|
-
*
|
|
1361
|
-
* @param path A path indicating the field to bind the async validation logic to.
|
|
1362
|
-
* @param opts The async validation options.
|
|
1363
|
-
* @template TValue The type of value stored in the field being validated.
|
|
1364
|
-
* @template TParams The type of parameters to the resource.
|
|
1365
|
-
* @template TResult The type of result returned by the resource
|
|
1366
|
-
* @template TPathKind The kind of path being validated (a root path, child path, or item of an array)
|
|
1367
|
-
*
|
|
1368
|
-
* @category validation
|
|
1369
|
-
* @experimental 21.0.0
|
|
1370
|
-
*/
|
|
1371
910
|
function validateAsync(path, opts) {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
});
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
911
|
+
assertPathIsCurrent(path);
|
|
912
|
+
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
|
913
|
+
const RESOURCE = metadata(path, ctx => {
|
|
914
|
+
const params = computed(() => {
|
|
915
|
+
const node = ctx.stateOf(path);
|
|
916
|
+
const validationState = node.validationState;
|
|
917
|
+
if (validationState.shouldSkipValidation() || !validationState.syncValid()) {
|
|
918
|
+
return undefined;
|
|
919
|
+
}
|
|
920
|
+
return opts.params(ctx);
|
|
921
|
+
}, ...(ngDevMode ? [{
|
|
922
|
+
debugName: "params"
|
|
923
|
+
}] : []));
|
|
924
|
+
return opts.factory(params);
|
|
925
|
+
});
|
|
926
|
+
pathNode.builder.addAsyncErrorRule(ctx => {
|
|
927
|
+
const res = ctx.state.metadata(RESOURCE);
|
|
928
|
+
let errors;
|
|
929
|
+
switch (res.status()) {
|
|
930
|
+
case 'idle':
|
|
931
|
+
return undefined;
|
|
932
|
+
case 'loading':
|
|
933
|
+
case 'reloading':
|
|
934
|
+
return 'pending';
|
|
935
|
+
case 'resolved':
|
|
936
|
+
case 'local':
|
|
937
|
+
if (!res.hasValue()) {
|
|
938
|
+
return undefined;
|
|
939
|
+
}
|
|
940
|
+
errors = opts.onSuccess(res.value(), ctx);
|
|
941
|
+
return addDefaultField(errors, ctx.field);
|
|
942
|
+
case 'error':
|
|
943
|
+
errors = opts.onError(res.error(), ctx);
|
|
944
|
+
return addDefaultField(errors, ctx.field);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
1405
947
|
}
|
|
1406
|
-
/**
|
|
1407
|
-
* Adds async validation to the field corresponding to the given path based on an httpResource.
|
|
1408
|
-
* Async validation for a field only runs once all synchronous validation is passing.
|
|
1409
|
-
*
|
|
1410
|
-
* @param path A path indicating the field to bind the async validation logic to.
|
|
1411
|
-
* @param opts The http validation options.
|
|
1412
|
-
* @template TValue The type of value stored in the field being validated.
|
|
1413
|
-
* @template TResult The type of result returned by the httpResource
|
|
1414
|
-
* @template TPathKind The kind of path being validated (a root path, child path, or item of an array)
|
|
1415
|
-
*
|
|
1416
|
-
* @category validation
|
|
1417
|
-
* @experimental 21.0.0
|
|
1418
|
-
*/
|
|
1419
948
|
function validateHttp(path, opts) {
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
949
|
+
validateAsync(path, {
|
|
950
|
+
params: opts.request,
|
|
951
|
+
factory: request => httpResource(request, opts.options),
|
|
952
|
+
onSuccess: opts.onSuccess,
|
|
953
|
+
onError: opts.onError
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
class InteropNgControl {
|
|
958
|
+
field;
|
|
959
|
+
constructor(field) {
|
|
960
|
+
this.field = field;
|
|
961
|
+
}
|
|
962
|
+
control = this;
|
|
963
|
+
get value() {
|
|
964
|
+
return this.field().value();
|
|
965
|
+
}
|
|
966
|
+
get valid() {
|
|
967
|
+
return this.field().valid();
|
|
968
|
+
}
|
|
969
|
+
get invalid() {
|
|
970
|
+
return this.field().invalid();
|
|
971
|
+
}
|
|
972
|
+
get pending() {
|
|
973
|
+
return this.field().pending();
|
|
974
|
+
}
|
|
975
|
+
get disabled() {
|
|
976
|
+
return this.field().disabled();
|
|
977
|
+
}
|
|
978
|
+
get enabled() {
|
|
979
|
+
return !this.field().disabled();
|
|
980
|
+
}
|
|
981
|
+
get errors() {
|
|
982
|
+
const errors = this.field().errors();
|
|
983
|
+
if (errors.length === 0) {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
const errObj = {};
|
|
987
|
+
for (const error of errors) {
|
|
988
|
+
errObj[error.kind] = error;
|
|
989
|
+
}
|
|
990
|
+
return errObj;
|
|
991
|
+
}
|
|
992
|
+
get pristine() {
|
|
993
|
+
return !this.field().dirty();
|
|
994
|
+
}
|
|
995
|
+
get dirty() {
|
|
996
|
+
return this.field().dirty();
|
|
997
|
+
}
|
|
998
|
+
get touched() {
|
|
999
|
+
return this.field().touched();
|
|
1000
|
+
}
|
|
1001
|
+
get untouched() {
|
|
1002
|
+
return !this.field().touched();
|
|
1003
|
+
}
|
|
1004
|
+
get status() {
|
|
1005
|
+
if (this.field().disabled()) {
|
|
1006
|
+
return 'DISABLED';
|
|
1007
|
+
}
|
|
1008
|
+
if (this.field().valid()) {
|
|
1009
|
+
return 'VALID';
|
|
1010
|
+
}
|
|
1011
|
+
if (this.field().invalid()) {
|
|
1012
|
+
return 'INVALID';
|
|
1013
|
+
}
|
|
1014
|
+
if (this.field().pending()) {
|
|
1015
|
+
return 'PENDING';
|
|
1016
|
+
}
|
|
1017
|
+
throw Error('AssertionError: unknown form control status');
|
|
1018
|
+
}
|
|
1019
|
+
valueAccessor = null;
|
|
1020
|
+
hasValidator(validator) {
|
|
1021
|
+
if (validator === Validators.required) {
|
|
1022
|
+
return this.field().metadata(REQUIRED)();
|
|
1023
|
+
}
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
updateValueAndValidity() {}
|
|
1425
1027
|
}
|
|
1426
1028
|
|
|
1427
|
-
/**
|
|
1428
|
-
* Lightweight DI token provided by the {@link Field} directive.
|
|
1429
|
-
*/
|
|
1430
1029
|
const FIELD = new InjectionToken(typeof ngDevMode !== undefined && ngDevMode ? 'FIELD' : '');
|
|
1431
|
-
/**
|
|
1432
|
-
* Binds a form `FieldTree` to a UI control that edits it. A UI control can be one of several things:
|
|
1433
|
-
* 1. A native HTML input or textarea
|
|
1434
|
-
* 2. A signal forms custom control that implements `FormValueControl` or `FormCheckboxControl`
|
|
1435
|
-
* 3. TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131712274. A
|
|
1436
|
-
* component that provides a ControlValueAccessor. This should only be used to backwards
|
|
1437
|
-
* compatibility with reactive forms. Prefer options (1) and (2).
|
|
1438
|
-
*
|
|
1439
|
-
* This directive has several responsibilities:
|
|
1440
|
-
* 1. Two-way binds the field's value with the UI control's value
|
|
1441
|
-
* 2. Binds additional forms related state on the field to the UI control (disabled, required, etc.)
|
|
1442
|
-
* 3. Relays relevant events on the control to the field (e.g. marks field touched on blur)
|
|
1443
|
-
* 4. TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131712274.
|
|
1444
|
-
* Provides a fake `NgControl` that implements a subset of the features available on the
|
|
1445
|
-
* reactive forms `NgControl`. This is provided to improve interoperability with controls
|
|
1446
|
-
* designed to work with reactive forms. It should not be used by controls written for signal
|
|
1447
|
-
* forms.
|
|
1448
|
-
*
|
|
1449
|
-
* @category control
|
|
1450
|
-
* @experimental 21.0.0
|
|
1451
|
-
*/
|
|
1452
1030
|
class Field {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1031
|
+
injector = inject(Injector);
|
|
1032
|
+
field = input.required(...(ngDevMode ? [{
|
|
1033
|
+
debugName: "field"
|
|
1034
|
+
}] : []));
|
|
1035
|
+
state = computed(() => this.field()(), ...(ngDevMode ? [{
|
|
1036
|
+
debugName: "state"
|
|
1037
|
+
}] : []));
|
|
1038
|
+
[_CONTROL] = undefined;
|
|
1039
|
+
controlValueAccessors = inject(NG_VALUE_ACCESSOR, {
|
|
1040
|
+
optional: true,
|
|
1041
|
+
self: true
|
|
1042
|
+
});
|
|
1043
|
+
interopNgControl;
|
|
1044
|
+
get controlValueAccessor() {
|
|
1045
|
+
return this.controlValueAccessors?.[0] ?? this.interopNgControl?.valueAccessor ?? undefined;
|
|
1046
|
+
}
|
|
1047
|
+
get ɵhasInteropControl() {
|
|
1048
|
+
return this.controlValueAccessor !== undefined;
|
|
1049
|
+
}
|
|
1050
|
+
ɵgetOrCreateNgControl() {
|
|
1051
|
+
return this.interopNgControl ??= new InteropNgControl(this.state);
|
|
1052
|
+
}
|
|
1053
|
+
ɵinteropControlCreate() {
|
|
1054
|
+
const controlValueAccessor = this.controlValueAccessor;
|
|
1055
|
+
controlValueAccessor.registerOnChange(value => {
|
|
1056
|
+
const state = this.state();
|
|
1057
|
+
state.value.set(value);
|
|
1058
|
+
state.markAsDirty();
|
|
1059
|
+
});
|
|
1060
|
+
controlValueAccessor.registerOnTouched(() => this.state().markAsTouched());
|
|
1061
|
+
}
|
|
1062
|
+
ɵinteropControlUpdate() {
|
|
1063
|
+
const controlValueAccessor = this.controlValueAccessor;
|
|
1064
|
+
const value = this.state().value();
|
|
1065
|
+
const disabled = this.state().disabled();
|
|
1066
|
+
untracked(() => {
|
|
1067
|
+
controlValueAccessor.writeValue(value);
|
|
1068
|
+
controlValueAccessor.setDisabledState?.(disabled);
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
ɵregister() {
|
|
1072
|
+
effect(onCleanup => {
|
|
1073
|
+
const fieldNode = this.state();
|
|
1074
|
+
fieldNode.nodeState.fieldBindings.update(controls => [...controls, this]);
|
|
1075
|
+
onCleanup(() => {
|
|
1076
|
+
fieldNode.nodeState.fieldBindings.update(controls => controls.filter(c => c !== this));
|
|
1077
|
+
});
|
|
1078
|
+
}, {
|
|
1079
|
+
injector: this.injector
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1083
|
+
minVersion: "12.0.0",
|
|
1084
|
+
version: "21.0.0-rc.0",
|
|
1085
|
+
ngImport: i0,
|
|
1086
|
+
type: Field,
|
|
1087
|
+
deps: [],
|
|
1088
|
+
target: i0.ɵɵFactoryTarget.Directive
|
|
1089
|
+
});
|
|
1090
|
+
static ɵdir = i0.ɵɵngDeclareDirective({
|
|
1091
|
+
minVersion: "17.1.0",
|
|
1092
|
+
version: "21.0.0-rc.0",
|
|
1093
|
+
type: Field,
|
|
1094
|
+
isStandalone: true,
|
|
1095
|
+
selector: "[field]",
|
|
1096
|
+
inputs: {
|
|
1097
|
+
field: {
|
|
1098
|
+
classPropertyName: "field",
|
|
1099
|
+
publicName: "field",
|
|
1100
|
+
isSignal: true,
|
|
1101
|
+
isRequired: true,
|
|
1102
|
+
transformFunction: null
|
|
1103
|
+
}
|
|
1104
|
+
},
|
|
1105
|
+
providers: [{
|
|
1106
|
+
provide: FIELD,
|
|
1107
|
+
useExisting: Field
|
|
1108
|
+
}, {
|
|
1109
|
+
provide: NgControl,
|
|
1110
|
+
useFactory: () => inject(Field).ɵgetOrCreateNgControl()
|
|
1111
|
+
}],
|
|
1112
|
+
ngImport: i0
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1116
|
+
minVersion: "12.0.0",
|
|
1117
|
+
version: "21.0.0-rc.0",
|
|
1118
|
+
ngImport: i0,
|
|
1119
|
+
type: Field,
|
|
1120
|
+
decorators: [{
|
|
1121
|
+
type: Directive,
|
|
1122
|
+
args: [{
|
|
1123
|
+
selector: '[field]',
|
|
1124
|
+
providers: [{
|
|
1125
|
+
provide: FIELD,
|
|
1126
|
+
useExisting: Field
|
|
1127
|
+
}, {
|
|
1128
|
+
provide: NgControl,
|
|
1129
|
+
useFactory: () => inject(Field).ɵgetOrCreateNgControl()
|
|
1130
|
+
}]
|
|
1131
|
+
}]
|
|
1132
|
+
}],
|
|
1133
|
+
propDecorators: {
|
|
1134
|
+
field: [{
|
|
1135
|
+
type: i0.Input,
|
|
1136
|
+
args: [{
|
|
1137
|
+
isSignal: true,
|
|
1138
|
+
alias: "field",
|
|
1139
|
+
required: true
|
|
1140
|
+
}]
|
|
1141
|
+
}]
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1480
1144
|
|
|
1481
|
-
/**
|
|
1482
|
-
* `FieldContext` implementation, backed by a `FieldNode`.
|
|
1483
|
-
*/
|
|
1484
1145
|
class FieldNodeContext {
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
get field() {
|
|
1542
|
-
return this.node.fieldProxy;
|
|
1543
|
-
}
|
|
1544
|
-
get state() {
|
|
1545
|
-
return this.node;
|
|
1546
|
-
}
|
|
1547
|
-
get value() {
|
|
1548
|
-
return this.node.structure.value;
|
|
1549
|
-
}
|
|
1550
|
-
get key() {
|
|
1551
|
-
return this.node.structure.keyInParent;
|
|
1552
|
-
}
|
|
1553
|
-
index = computed(() => {
|
|
1554
|
-
// Attempt to read the key first, this will throw an error if we're on a root field.
|
|
1555
|
-
const key = this.key();
|
|
1556
|
-
// Assert that the parent is actually an array.
|
|
1557
|
-
if (!isArray(untracked(this.node.structure.parent.value))) {
|
|
1558
|
-
throw new Error(`RuntimeError: cannot access index, parent field is not an array`);
|
|
1559
|
-
}
|
|
1560
|
-
// Return the key as a number if we are indeed inside an array field.
|
|
1561
|
-
return Number(key);
|
|
1562
|
-
}, ...(ngDevMode ? [{ debugName: "index" }] : []));
|
|
1563
|
-
fieldOf = (p) => this.resolve(p);
|
|
1564
|
-
stateOf = (p) => this.resolve(p)();
|
|
1565
|
-
valueOf = (p) => this.resolve(p)().value();
|
|
1146
|
+
node;
|
|
1147
|
+
cache = new WeakMap();
|
|
1148
|
+
constructor(node) {
|
|
1149
|
+
this.node = node;
|
|
1150
|
+
}
|
|
1151
|
+
resolve(target) {
|
|
1152
|
+
if (!this.cache.has(target)) {
|
|
1153
|
+
const resolver = computed(() => {
|
|
1154
|
+
const targetPathNode = FieldPathNode.unwrapFieldPath(target);
|
|
1155
|
+
let field = this.node;
|
|
1156
|
+
let stepsRemaining = getBoundPathDepth();
|
|
1157
|
+
while (stepsRemaining > 0 || !field.structure.logic.hasLogic(targetPathNode.root.builder)) {
|
|
1158
|
+
stepsRemaining--;
|
|
1159
|
+
field = field.structure.parent;
|
|
1160
|
+
if (field === undefined) {
|
|
1161
|
+
throw new Error('Path is not part of this field tree.');
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
for (let key of targetPathNode.keys) {
|
|
1165
|
+
field = field.structure.getChild(key);
|
|
1166
|
+
if (field === undefined) {
|
|
1167
|
+
throw new Error(`Cannot resolve path .${targetPathNode.keys.join('.')} relative to field ${['<root>', ...this.node.structure.pathKeys()].join('.')}.`);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return field.fieldProxy;
|
|
1171
|
+
}, ...(ngDevMode ? [{
|
|
1172
|
+
debugName: "resolver"
|
|
1173
|
+
}] : []));
|
|
1174
|
+
this.cache.set(target, resolver);
|
|
1175
|
+
}
|
|
1176
|
+
return this.cache.get(target)();
|
|
1177
|
+
}
|
|
1178
|
+
get field() {
|
|
1179
|
+
return this.node.fieldProxy;
|
|
1180
|
+
}
|
|
1181
|
+
get state() {
|
|
1182
|
+
return this.node;
|
|
1183
|
+
}
|
|
1184
|
+
get value() {
|
|
1185
|
+
return this.node.structure.value;
|
|
1186
|
+
}
|
|
1187
|
+
get key() {
|
|
1188
|
+
return this.node.structure.keyInParent;
|
|
1189
|
+
}
|
|
1190
|
+
index = computed(() => {
|
|
1191
|
+
const key = this.key();
|
|
1192
|
+
if (!isArray(untracked(this.node.structure.parent.value))) {
|
|
1193
|
+
throw new Error(`RuntimeError: cannot access index, parent field is not an array`);
|
|
1194
|
+
}
|
|
1195
|
+
return Number(key);
|
|
1196
|
+
}, ...(ngDevMode ? [{
|
|
1197
|
+
debugName: "index"
|
|
1198
|
+
}] : []));
|
|
1199
|
+
fieldOf = p => this.resolve(p);
|
|
1200
|
+
stateOf = p => this.resolve(p)();
|
|
1201
|
+
valueOf = p => this.resolve(p)().value();
|
|
1566
1202
|
}
|
|
1567
1203
|
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
node;
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
get(
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
return this.properties.get(prop);
|
|
1600
|
-
}
|
|
1601
|
-
/**
|
|
1602
|
-
* Checks whether the current property state has the given property.
|
|
1603
|
-
* @param prop
|
|
1604
|
-
* @returns
|
|
1605
|
-
*/
|
|
1606
|
-
has(prop) {
|
|
1607
|
-
if (prop instanceof AggregateProperty) {
|
|
1608
|
-
// For aggregate properties, they get added to the map lazily, on first access, so we can't
|
|
1609
|
-
// rely on checking presence in the properties map. Instead we check if there is any logic for
|
|
1610
|
-
// the given property.
|
|
1611
|
-
return this.node.logicNode.logic.hasAggregateProperty(prop);
|
|
1612
|
-
}
|
|
1613
|
-
else {
|
|
1614
|
-
// Non-aggregate proeprties get added to our properties map on construction, so we can just
|
|
1615
|
-
// refer to their presence in the map.
|
|
1616
|
-
return this.properties.has(prop);
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1204
|
+
class FieldMetadataState {
|
|
1205
|
+
node;
|
|
1206
|
+
metadata = new Map();
|
|
1207
|
+
constructor(node) {
|
|
1208
|
+
this.node = node;
|
|
1209
|
+
untracked(() => runInInjectionContext(this.node.structure.injector, () => {
|
|
1210
|
+
for (const [key, factory] of this.node.logicNode.logic.getMetadataFactoryEntries()) {
|
|
1211
|
+
this.metadata.set(key, factory(this.node.context));
|
|
1212
|
+
}
|
|
1213
|
+
}));
|
|
1214
|
+
}
|
|
1215
|
+
get(key) {
|
|
1216
|
+
if (key instanceof MetadataKey) {
|
|
1217
|
+
return this.metadata.get(key);
|
|
1218
|
+
}
|
|
1219
|
+
if (!this.metadata.has(key)) {
|
|
1220
|
+
const logic = this.node.logicNode.logic.getAggregateMetadata(key);
|
|
1221
|
+
const result = computed(() => logic.compute(this.node.context), ...(ngDevMode ? [{
|
|
1222
|
+
debugName: "result"
|
|
1223
|
+
}] : []));
|
|
1224
|
+
this.metadata.set(key, result);
|
|
1225
|
+
}
|
|
1226
|
+
return this.metadata.get(key);
|
|
1227
|
+
}
|
|
1228
|
+
has(key) {
|
|
1229
|
+
if (key instanceof AggregateMetadataKey) {
|
|
1230
|
+
return this.node.logicNode.logic.hasAggregateMetadata(key);
|
|
1231
|
+
} else {
|
|
1232
|
+
return this.metadata.has(key);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1619
1235
|
}
|
|
1620
1236
|
|
|
1621
|
-
/**
|
|
1622
|
-
* Proxy handler which implements `FieldTree<T>` on top of `FieldNode`.
|
|
1623
|
-
*/
|
|
1624
1237
|
const FIELD_PROXY_HANDLER = {
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
// Allow access to the length for field arrays, it should be the same as the length of the data.
|
|
1643
|
-
if (p === 'length') {
|
|
1644
|
-
return tgt.value().length;
|
|
1645
|
-
}
|
|
1646
|
-
// Allow access to the iterator. This allows the user to spread the field array into a
|
|
1647
|
-
// standard array in order to call methods like `filter`, `map`, etc.
|
|
1648
|
-
if (p === Symbol.iterator) {
|
|
1649
|
-
return Array.prototype[p];
|
|
1650
|
-
}
|
|
1651
|
-
// Note: We can consider supporting additional array methods if we want in the future,
|
|
1652
|
-
// but they should be thoroughly tested. Just forwarding the method directly from the
|
|
1653
|
-
// `Array` prototype results in broken behavior for some methods like `map`.
|
|
1654
|
-
}
|
|
1655
|
-
// Otherwise, this property doesn't exist.
|
|
1656
|
-
return undefined;
|
|
1657
|
-
},
|
|
1238
|
+
get(getTgt, p) {
|
|
1239
|
+
const tgt = getTgt();
|
|
1240
|
+
const child = tgt.structure.getChild(p);
|
|
1241
|
+
if (child !== undefined) {
|
|
1242
|
+
return child.fieldProxy;
|
|
1243
|
+
}
|
|
1244
|
+
const value = untracked(tgt.value);
|
|
1245
|
+
if (isArray(value)) {
|
|
1246
|
+
if (p === 'length') {
|
|
1247
|
+
return tgt.value().length;
|
|
1248
|
+
}
|
|
1249
|
+
if (p === Symbol.iterator) {
|
|
1250
|
+
return Array.prototype[p];
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return undefined;
|
|
1254
|
+
}
|
|
1658
1255
|
};
|
|
1659
1256
|
|
|
1660
|
-
/**
|
|
1661
|
-
* Creates a writable signal for a specific property on a source writeable signal.
|
|
1662
|
-
* @param source A writeable signal to derive from
|
|
1663
|
-
* @param prop A signal of a property key of the source value
|
|
1664
|
-
* @returns A writeable signal for the given property of the source value.
|
|
1665
|
-
* @template S The source value type
|
|
1666
|
-
* @template K The key type for S
|
|
1667
|
-
*/
|
|
1668
1257
|
function deepSignal(source, prop) {
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
read.
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
return read;
|
|
1258
|
+
const read = computed(() => source()[prop()]);
|
|
1259
|
+
read[SIGNAL] = source[SIGNAL];
|
|
1260
|
+
read.set = value => {
|
|
1261
|
+
source.update(current => valueForWrite(current, value, prop()));
|
|
1262
|
+
};
|
|
1263
|
+
read.update = fn => {
|
|
1264
|
+
read.set(fn(untracked(read)));
|
|
1265
|
+
};
|
|
1266
|
+
read.asReadonly = () => read;
|
|
1267
|
+
return read;
|
|
1680
1268
|
}
|
|
1681
|
-
/**
|
|
1682
|
-
* Gets an updated root value to use when setting a value on a deepSignal with the given path.
|
|
1683
|
-
* @param sourceValue The current value of the deepSignal's source.
|
|
1684
|
-
* @param newPropValue The value being written to the deepSignal's property
|
|
1685
|
-
* @param prop The deepSignal's property key
|
|
1686
|
-
* @returns An updated value for the deepSignal's source
|
|
1687
|
-
*/
|
|
1688
1269
|
function valueForWrite(sourceValue, newPropValue, prop) {
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1270
|
+
if (isArray(sourceValue)) {
|
|
1271
|
+
const newValue = [...sourceValue];
|
|
1272
|
+
newValue[prop] = newPropValue;
|
|
1273
|
+
return newValue;
|
|
1274
|
+
} else {
|
|
1275
|
+
return {
|
|
1276
|
+
...sourceValue,
|
|
1277
|
+
[prop]: newPropValue
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1697
1280
|
}
|
|
1698
1281
|
|
|
1699
|
-
/** Structural component of a `FieldNode` which tracks its path, parent, and children. */
|
|
1700
1282
|
class FieldNodeStructure {
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
if (isObject(childValue) && childValue.hasOwnProperty(this.identitySymbol)) {
|
|
1735
|
-
// For arrays, we want to use the tracking identity of the value instead of the raw property
|
|
1736
|
-
// as our index into the `childrenMap`.
|
|
1737
|
-
key = childValue[this.identitySymbol];
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
return map.get((typeof key === 'number' ? key.toString() : key));
|
|
1741
|
-
}
|
|
1742
|
-
/** Destroys the field when it is no longer needed. */
|
|
1743
|
-
destroy() {
|
|
1744
|
-
this.injector.destroy();
|
|
1745
|
-
}
|
|
1283
|
+
logic;
|
|
1284
|
+
identitySymbol = Symbol();
|
|
1285
|
+
_injector = undefined;
|
|
1286
|
+
get injector() {
|
|
1287
|
+
this._injector ??= Injector.create({
|
|
1288
|
+
providers: [],
|
|
1289
|
+
parent: this.fieldManager.injector
|
|
1290
|
+
});
|
|
1291
|
+
return this._injector;
|
|
1292
|
+
}
|
|
1293
|
+
constructor(logic) {
|
|
1294
|
+
this.logic = logic;
|
|
1295
|
+
}
|
|
1296
|
+
children() {
|
|
1297
|
+
return this.childrenMap()?.values() ?? [];
|
|
1298
|
+
}
|
|
1299
|
+
getChild(key) {
|
|
1300
|
+
const map = this.childrenMap();
|
|
1301
|
+
const value = this.value();
|
|
1302
|
+
if (!map || !isObject(value)) {
|
|
1303
|
+
return undefined;
|
|
1304
|
+
}
|
|
1305
|
+
if (isArray(value)) {
|
|
1306
|
+
const childValue = value[key];
|
|
1307
|
+
if (isObject(childValue) && childValue.hasOwnProperty(this.identitySymbol)) {
|
|
1308
|
+
key = childValue[this.identitySymbol];
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return map.get(typeof key === 'number' ? key.toString() : key);
|
|
1312
|
+
}
|
|
1313
|
+
destroy() {
|
|
1314
|
+
this.injector.destroy();
|
|
1315
|
+
}
|
|
1746
1316
|
}
|
|
1747
|
-
/** The structural component of a `FieldNode` that is the root of its field tree. */
|
|
1748
1317
|
class RootFieldNodeStructure extends FieldNodeStructure {
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
* @param adapter Adapter that knows how to create new fields and appropriate state.
|
|
1774
|
-
* @param createChildNode A factory function to create child nodes for this field.
|
|
1775
|
-
*/
|
|
1776
|
-
constructor(
|
|
1777
|
-
/** The full field node that corresponds to this structure. */
|
|
1778
|
-
node, pathNode, logic, fieldManager, value, adapter, createChildNode) {
|
|
1779
|
-
super(logic);
|
|
1780
|
-
this.node = node;
|
|
1781
|
-
this.fieldManager = fieldManager;
|
|
1782
|
-
this.value = value;
|
|
1783
|
-
this.childrenMap = makeChildrenMapSignal(node, value, this.identitySymbol, pathNode, logic, adapter, createChildNode);
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
/** The structural component of a child `FieldNode` within a field tree. */
|
|
1318
|
+
node;
|
|
1319
|
+
fieldManager;
|
|
1320
|
+
value;
|
|
1321
|
+
get parent() {
|
|
1322
|
+
return undefined;
|
|
1323
|
+
}
|
|
1324
|
+
get root() {
|
|
1325
|
+
return this.node;
|
|
1326
|
+
}
|
|
1327
|
+
get pathKeys() {
|
|
1328
|
+
return ROOT_PATH_KEYS;
|
|
1329
|
+
}
|
|
1330
|
+
get keyInParent() {
|
|
1331
|
+
return ROOT_KEY_IN_PARENT;
|
|
1332
|
+
}
|
|
1333
|
+
childrenMap;
|
|
1334
|
+
constructor(node, pathNode, logic, fieldManager, value, adapter, createChildNode) {
|
|
1335
|
+
super(logic);
|
|
1336
|
+
this.node = node;
|
|
1337
|
+
this.fieldManager = fieldManager;
|
|
1338
|
+
this.value = value;
|
|
1339
|
+
this.childrenMap = makeChildrenMapSignal(node, value, this.identitySymbol, pathNode, logic, adapter, createChildNode);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1787
1342
|
class ChildFieldNodeStructure extends FieldNodeStructure {
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
const data = parentValue[lastKnownKey];
|
|
1841
|
-
if (isObject(data) &&
|
|
1842
|
-
data.hasOwnProperty(parent.structure.identitySymbol) &&
|
|
1843
|
-
data[parent.structure.identitySymbol] === identityInParent) {
|
|
1844
|
-
return lastKnownKey;
|
|
1845
|
-
}
|
|
1846
|
-
// Otherwise, we need to check all the keys in the parent.
|
|
1847
|
-
for (let i = 0; i < parentValue.length; i++) {
|
|
1848
|
-
const data = parentValue[i];
|
|
1849
|
-
if (isObject(data) &&
|
|
1850
|
-
data.hasOwnProperty(parent.structure.identitySymbol) &&
|
|
1851
|
-
data[parent.structure.identitySymbol] === identityInParent) {
|
|
1852
|
-
return (lastKnownKey = i.toString());
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
throw new Error(`RuntimeError: orphan field, can't find element in array ${getDebugName(parent)}`);
|
|
1856
|
-
}, ...(ngDevMode ? [{ debugName: "keyInParent" }] : []));
|
|
1857
|
-
}
|
|
1858
|
-
this.value = deepSignal(this.parent.structure.value, this.keyInParent);
|
|
1859
|
-
this.childrenMap = makeChildrenMapSignal(node, this.value, this.identitySymbol, pathNode, logic, adapter, createChildNode);
|
|
1860
|
-
this.fieldManager.structures.add(this);
|
|
1861
|
-
}
|
|
1343
|
+
parent;
|
|
1344
|
+
root;
|
|
1345
|
+
pathKeys;
|
|
1346
|
+
keyInParent;
|
|
1347
|
+
value;
|
|
1348
|
+
childrenMap;
|
|
1349
|
+
get fieldManager() {
|
|
1350
|
+
return this.root.structure.fieldManager;
|
|
1351
|
+
}
|
|
1352
|
+
constructor(node, pathNode, logic, parent, identityInParent, initialKeyInParent, adapter, createChildNode) {
|
|
1353
|
+
super(logic);
|
|
1354
|
+
this.parent = parent;
|
|
1355
|
+
this.root = this.parent.structure.root;
|
|
1356
|
+
this.pathKeys = computed(() => [...parent.structure.pathKeys(), this.keyInParent()], ...(ngDevMode ? [{
|
|
1357
|
+
debugName: "pathKeys"
|
|
1358
|
+
}] : []));
|
|
1359
|
+
if (identityInParent === undefined) {
|
|
1360
|
+
const key = initialKeyInParent;
|
|
1361
|
+
this.keyInParent = computed(() => {
|
|
1362
|
+
if (parent.structure.childrenMap()?.get(key) !== node) {
|
|
1363
|
+
throw new Error(`RuntimeError: orphan field, looking for property '${key}' of ${getDebugName(parent)}`);
|
|
1364
|
+
}
|
|
1365
|
+
return key;
|
|
1366
|
+
}, ...(ngDevMode ? [{
|
|
1367
|
+
debugName: "keyInParent"
|
|
1368
|
+
}] : []));
|
|
1369
|
+
} else {
|
|
1370
|
+
let lastKnownKey = initialKeyInParent;
|
|
1371
|
+
this.keyInParent = computed(() => {
|
|
1372
|
+
const parentValue = parent.structure.value();
|
|
1373
|
+
if (!isArray(parentValue)) {
|
|
1374
|
+
throw new Error(`RuntimeError: orphan field, expected ${getDebugName(parent)} to be an array`);
|
|
1375
|
+
}
|
|
1376
|
+
const data = parentValue[lastKnownKey];
|
|
1377
|
+
if (isObject(data) && data.hasOwnProperty(parent.structure.identitySymbol) && data[parent.structure.identitySymbol] === identityInParent) {
|
|
1378
|
+
return lastKnownKey;
|
|
1379
|
+
}
|
|
1380
|
+
for (let i = 0; i < parentValue.length; i++) {
|
|
1381
|
+
const data = parentValue[i];
|
|
1382
|
+
if (isObject(data) && data.hasOwnProperty(parent.structure.identitySymbol) && data[parent.structure.identitySymbol] === identityInParent) {
|
|
1383
|
+
return lastKnownKey = i.toString();
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
throw new Error(`RuntimeError: orphan field, can't find element in array ${getDebugName(parent)}`);
|
|
1387
|
+
}, ...(ngDevMode ? [{
|
|
1388
|
+
debugName: "keyInParent"
|
|
1389
|
+
}] : []));
|
|
1390
|
+
}
|
|
1391
|
+
this.value = deepSignal(this.parent.structure.value, this.keyInParent);
|
|
1392
|
+
this.childrenMap = makeChildrenMapSignal(node, this.value, this.identitySymbol, pathNode, logic, adapter, createChildNode);
|
|
1393
|
+
this.fieldManager.structures.add(this);
|
|
1394
|
+
}
|
|
1862
1395
|
}
|
|
1863
|
-
/** Global id used for tracking keys. */
|
|
1864
1396
|
let globalId = 0;
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
* A signal representing a non-existent key of the field in its parent, used for root fields which
|
|
1869
|
-
* do not have a parent. This signal will throw if it is read.
|
|
1870
|
-
*/
|
|
1397
|
+
const ROOT_PATH_KEYS = computed(() => [], ...(ngDevMode ? [{
|
|
1398
|
+
debugName: "ROOT_PATH_KEYS"
|
|
1399
|
+
}] : []));
|
|
1871
1400
|
const ROOT_KEY_IN_PARENT = computed(() => {
|
|
1872
|
-
|
|
1873
|
-
}, ...(ngDevMode ? [{
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
*
|
|
1877
|
-
* @param node The field to create the children map signal for.
|
|
1878
|
-
* @param valueSignal The value signal for the field.
|
|
1879
|
-
* @param identitySymbol The key used to access the tracking id of a field.
|
|
1880
|
-
* @param pathNode The path node corresponding to the field in the schema.
|
|
1881
|
-
* @param logic The logic to apply to the field.
|
|
1882
|
-
* @param adapter Adapter that knows how to create new fields and appropriate state.
|
|
1883
|
-
* @param createChildNode A factory function to create child nodes for this field.
|
|
1884
|
-
* @returns
|
|
1885
|
-
*/
|
|
1401
|
+
throw new Error(`RuntimeError: the top-level field in the form has no parent`);
|
|
1402
|
+
}, ...(ngDevMode ? [{
|
|
1403
|
+
debugName: "ROOT_KEY_IN_PARENT"
|
|
1404
|
+
}] : []));
|
|
1886
1405
|
function makeChildrenMapSignal(node, valueSignal, identitySymbol, pathNode, logic, adapter, createChildNode) {
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
const
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
oldKeys = new Set(childrenMap.keys());
|
|
1906
|
-
for (let i = 0; i < value.length; i++) {
|
|
1907
|
-
const childValue = value[i];
|
|
1908
|
-
if (isObject(childValue) && childValue.hasOwnProperty(identitySymbol)) {
|
|
1909
|
-
oldKeys.delete(childValue[identitySymbol]);
|
|
1910
|
-
}
|
|
1911
|
-
else {
|
|
1912
|
-
oldKeys.delete(i.toString());
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
for (const key of oldKeys) {
|
|
1916
|
-
childrenMap.delete(key);
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
else {
|
|
1920
|
-
for (let key of childrenMap.keys()) {
|
|
1921
|
-
if (!value.hasOwnProperty(key)) {
|
|
1922
|
-
childrenMap.delete(key);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1406
|
+
return linkedSignal({
|
|
1407
|
+
source: valueSignal,
|
|
1408
|
+
computation: (value, previous) => {
|
|
1409
|
+
let childrenMap = previous?.value;
|
|
1410
|
+
if (!isObject(value)) {
|
|
1411
|
+
return undefined;
|
|
1412
|
+
}
|
|
1413
|
+
const isValueArray = isArray(value);
|
|
1414
|
+
if (childrenMap !== undefined) {
|
|
1415
|
+
let oldKeys = undefined;
|
|
1416
|
+
if (isValueArray) {
|
|
1417
|
+
oldKeys = new Set(childrenMap.keys());
|
|
1418
|
+
for (let i = 0; i < value.length; i++) {
|
|
1419
|
+
const childValue = value[i];
|
|
1420
|
+
if (isObject(childValue) && childValue.hasOwnProperty(identitySymbol)) {
|
|
1421
|
+
oldKeys.delete(childValue[identitySymbol]);
|
|
1422
|
+
} else {
|
|
1423
|
+
oldKeys.delete(i.toString());
|
|
1926
1424
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
// The value might have _become_ `undefined`, so we need to delete it here.
|
|
1936
|
-
childrenMap?.delete(key);
|
|
1937
|
-
continue;
|
|
1938
|
-
}
|
|
1939
|
-
if (isValueArray && isObject(childValue)) {
|
|
1940
|
-
// For object values in arrays, assign a synthetic identity instead.
|
|
1941
|
-
trackingId = childValue[identitySymbol] ??= Symbol(ngDevMode ? `id:${globalId++}` : '');
|
|
1942
|
-
}
|
|
1943
|
-
const identity = trackingId ?? key;
|
|
1944
|
-
if (childrenMap?.has(identity)) {
|
|
1945
|
-
continue;
|
|
1946
|
-
}
|
|
1947
|
-
// Determine the logic for the field that we're defining.
|
|
1948
|
-
let childPath;
|
|
1949
|
-
let childLogic;
|
|
1950
|
-
if (isValueArray) {
|
|
1951
|
-
// Fields for array elements have their logic defined by the `element` mechanism.
|
|
1952
|
-
// TODO: other dynamic data
|
|
1953
|
-
childPath = pathNode.getChild(DYNAMIC);
|
|
1954
|
-
childLogic = logic.getChild(DYNAMIC);
|
|
1955
|
-
}
|
|
1956
|
-
else {
|
|
1957
|
-
// Fields for plain properties exist in our logic node's child map.
|
|
1958
|
-
childPath = pathNode.getChild(key);
|
|
1959
|
-
childLogic = logic.getChild(key);
|
|
1960
|
-
}
|
|
1961
|
-
childrenMap ??= new Map();
|
|
1962
|
-
childrenMap.set(identity, createChildNode({
|
|
1963
|
-
kind: 'child',
|
|
1964
|
-
parent: node,
|
|
1965
|
-
pathNode: childPath,
|
|
1966
|
-
logic: childLogic,
|
|
1967
|
-
initialKeyInParent: key,
|
|
1968
|
-
identityInParent: trackingId,
|
|
1969
|
-
fieldAdapter: adapter,
|
|
1970
|
-
}));
|
|
1425
|
+
}
|
|
1426
|
+
for (const key of oldKeys) {
|
|
1427
|
+
childrenMap.delete(key);
|
|
1428
|
+
}
|
|
1429
|
+
} else {
|
|
1430
|
+
for (let key of childrenMap.keys()) {
|
|
1431
|
+
if (!value.hasOwnProperty(key)) {
|
|
1432
|
+
childrenMap.delete(key);
|
|
1971
1433
|
}
|
|
1972
|
-
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
for (let key of Object.keys(value)) {
|
|
1438
|
+
let trackingId = undefined;
|
|
1439
|
+
const childValue = value[key];
|
|
1440
|
+
if (childValue === undefined) {
|
|
1441
|
+
childrenMap?.delete(key);
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (isValueArray && isObject(childValue) && !isArray(childValue)) {
|
|
1445
|
+
trackingId = childValue[identitySymbol] ??= Symbol(ngDevMode ? `id:${globalId++}` : '');
|
|
1446
|
+
}
|
|
1447
|
+
const identity = trackingId ?? key;
|
|
1448
|
+
if (childrenMap?.has(identity)) {
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
let childPath;
|
|
1452
|
+
let childLogic;
|
|
1453
|
+
if (isValueArray) {
|
|
1454
|
+
childPath = pathNode.getChild(DYNAMIC);
|
|
1455
|
+
childLogic = logic.getChild(DYNAMIC);
|
|
1456
|
+
} else {
|
|
1457
|
+
childPath = pathNode.getChild(key);
|
|
1458
|
+
childLogic = logic.getChild(key);
|
|
1459
|
+
}
|
|
1460
|
+
childrenMap ??= new Map();
|
|
1461
|
+
childrenMap.set(identity, createChildNode({
|
|
1462
|
+
kind: 'child',
|
|
1463
|
+
parent: node,
|
|
1464
|
+
pathNode: childPath,
|
|
1465
|
+
logic: childLogic,
|
|
1466
|
+
initialKeyInParent: key,
|
|
1467
|
+
identityInParent: trackingId,
|
|
1468
|
+
fieldAdapter: adapter
|
|
1469
|
+
}));
|
|
1470
|
+
}
|
|
1471
|
+
return childrenMap;
|
|
1472
|
+
},
|
|
1473
|
+
equal: () => false
|
|
1474
|
+
});
|
|
1976
1475
|
}
|
|
1977
|
-
/** Gets a human readable name for a field node for use in error messages. */
|
|
1978
1476
|
function getDebugName(node) {
|
|
1979
|
-
|
|
1477
|
+
return `<root>.${node.structure.pathKeys().join('.')}`;
|
|
1980
1478
|
}
|
|
1981
1479
|
|
|
1982
|
-
/**
|
|
1983
|
-
* State of a `FieldNode` that's associated with form submission.
|
|
1984
|
-
*/
|
|
1985
1480
|
class FieldSubmitState {
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
serverErrors
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
return this.selfSubmitting() || (this.node.structure.parent?.submitting() ?? false);
|
|
2008
|
-
}, ...(ngDevMode ? [{ debugName: "submitting" }] : []));
|
|
1481
|
+
node;
|
|
1482
|
+
selfSubmitting = signal(false, ...(ngDevMode ? [{
|
|
1483
|
+
debugName: "selfSubmitting"
|
|
1484
|
+
}] : []));
|
|
1485
|
+
serverErrors;
|
|
1486
|
+
constructor(node) {
|
|
1487
|
+
this.node = node;
|
|
1488
|
+
this.serverErrors = linkedSignal(...(ngDevMode ? [{
|
|
1489
|
+
debugName: "serverErrors",
|
|
1490
|
+
source: this.node.structure.value,
|
|
1491
|
+
computation: () => []
|
|
1492
|
+
}] : [{
|
|
1493
|
+
source: this.node.structure.value,
|
|
1494
|
+
computation: () => []
|
|
1495
|
+
}]));
|
|
1496
|
+
}
|
|
1497
|
+
submitting = computed(() => {
|
|
1498
|
+
return this.selfSubmitting() || (this.node.structure.parent?.submitting() ?? false);
|
|
1499
|
+
}, ...(ngDevMode ? [{
|
|
1500
|
+
debugName: "submitting"
|
|
1501
|
+
}] : []));
|
|
2009
1502
|
}
|
|
2010
1503
|
|
|
2011
|
-
/**
|
|
2012
|
-
* Internal node in the form tree for a given field.
|
|
2013
|
-
*
|
|
2014
|
-
* Field nodes have several responsibilities:
|
|
2015
|
-
* - They track instance state for the particular field (touched)
|
|
2016
|
-
* - They compute signals for derived state (valid, disabled, etc) based on their associated
|
|
2017
|
-
* `LogicNode`
|
|
2018
|
-
* - They act as the public API for the field (they implement the `FieldState` interface)
|
|
2019
|
-
* - They implement navigation of the form tree via `.parent` and `.getChild()`.
|
|
2020
|
-
*
|
|
2021
|
-
* This class is largely a wrapper that aggregates several smaller pieces that each manage a subset of
|
|
2022
|
-
* the responsibilities.
|
|
2023
|
-
*/
|
|
2024
1504
|
class FieldNode {
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
/**
|
|
2147
|
-
* Creates a new root field node for a new form.
|
|
2148
|
-
*/
|
|
2149
|
-
static newRoot(fieldManager, value, pathNode, adapter) {
|
|
2150
|
-
return adapter.newRoot(fieldManager, value, pathNode, adapter);
|
|
2151
|
-
}
|
|
2152
|
-
/**
|
|
2153
|
-
* Creates a child field node based on the given options.
|
|
2154
|
-
*/
|
|
2155
|
-
static newChild(options) {
|
|
2156
|
-
return options.fieldAdapter.newChild(options);
|
|
2157
|
-
}
|
|
2158
|
-
createStructure(options) {
|
|
2159
|
-
return options.kind === 'root'
|
|
2160
|
-
? new RootFieldNodeStructure(this, options.pathNode, options.logic, options.fieldManager, options.value, options.fieldAdapter, FieldNode.newChild)
|
|
2161
|
-
: new ChildFieldNodeStructure(this, options.pathNode, options.logic, options.parent, options.identityInParent, options.initialKeyInParent, options.fieldAdapter, FieldNode.newChild);
|
|
2162
|
-
}
|
|
1505
|
+
structure;
|
|
1506
|
+
validationState;
|
|
1507
|
+
metadataState;
|
|
1508
|
+
nodeState;
|
|
1509
|
+
submitState;
|
|
1510
|
+
_context = undefined;
|
|
1511
|
+
fieldAdapter;
|
|
1512
|
+
get context() {
|
|
1513
|
+
return this._context ??= new FieldNodeContext(this);
|
|
1514
|
+
}
|
|
1515
|
+
fieldProxy = new Proxy(() => this, FIELD_PROXY_HANDLER);
|
|
1516
|
+
constructor(options) {
|
|
1517
|
+
this.fieldAdapter = options.fieldAdapter;
|
|
1518
|
+
this.structure = this.fieldAdapter.createStructure(this, options);
|
|
1519
|
+
this.validationState = this.fieldAdapter.createValidationState(this, options);
|
|
1520
|
+
this.nodeState = this.fieldAdapter.createNodeState(this, options);
|
|
1521
|
+
this.metadataState = new FieldMetadataState(this);
|
|
1522
|
+
this.submitState = new FieldSubmitState(this);
|
|
1523
|
+
}
|
|
1524
|
+
get logicNode() {
|
|
1525
|
+
return this.structure.logic;
|
|
1526
|
+
}
|
|
1527
|
+
get value() {
|
|
1528
|
+
return this.structure.value;
|
|
1529
|
+
}
|
|
1530
|
+
get keyInParent() {
|
|
1531
|
+
return this.structure.keyInParent;
|
|
1532
|
+
}
|
|
1533
|
+
get errors() {
|
|
1534
|
+
return this.validationState.errors;
|
|
1535
|
+
}
|
|
1536
|
+
get errorSummary() {
|
|
1537
|
+
return this.validationState.errorSummary;
|
|
1538
|
+
}
|
|
1539
|
+
get pending() {
|
|
1540
|
+
return this.validationState.pending;
|
|
1541
|
+
}
|
|
1542
|
+
get valid() {
|
|
1543
|
+
return this.validationState.valid;
|
|
1544
|
+
}
|
|
1545
|
+
get invalid() {
|
|
1546
|
+
return this.validationState.invalid;
|
|
1547
|
+
}
|
|
1548
|
+
get dirty() {
|
|
1549
|
+
return this.nodeState.dirty;
|
|
1550
|
+
}
|
|
1551
|
+
get touched() {
|
|
1552
|
+
return this.nodeState.touched;
|
|
1553
|
+
}
|
|
1554
|
+
get disabled() {
|
|
1555
|
+
return this.nodeState.disabled;
|
|
1556
|
+
}
|
|
1557
|
+
get disabledReasons() {
|
|
1558
|
+
return this.nodeState.disabledReasons;
|
|
1559
|
+
}
|
|
1560
|
+
get hidden() {
|
|
1561
|
+
return this.nodeState.hidden;
|
|
1562
|
+
}
|
|
1563
|
+
get readonly() {
|
|
1564
|
+
return this.nodeState.readonly;
|
|
1565
|
+
}
|
|
1566
|
+
get fieldBindings() {
|
|
1567
|
+
return this.nodeState.fieldBindings;
|
|
1568
|
+
}
|
|
1569
|
+
get submitting() {
|
|
1570
|
+
return this.submitState.submitting;
|
|
1571
|
+
}
|
|
1572
|
+
get name() {
|
|
1573
|
+
return this.nodeState.name;
|
|
1574
|
+
}
|
|
1575
|
+
metadataOrUndefined(key) {
|
|
1576
|
+
return this.hasMetadata(key) ? this.metadata(key) : undefined;
|
|
1577
|
+
}
|
|
1578
|
+
get max() {
|
|
1579
|
+
return this.metadataOrUndefined(MAX);
|
|
1580
|
+
}
|
|
1581
|
+
get maxLength() {
|
|
1582
|
+
return this.metadataOrUndefined(MAX_LENGTH);
|
|
1583
|
+
}
|
|
1584
|
+
get min() {
|
|
1585
|
+
return this.metadataOrUndefined(MIN);
|
|
1586
|
+
}
|
|
1587
|
+
get minLength() {
|
|
1588
|
+
return this.metadataOrUndefined(MIN_LENGTH);
|
|
1589
|
+
}
|
|
1590
|
+
get pattern() {
|
|
1591
|
+
return this.metadataOrUndefined(PATTERN);
|
|
1592
|
+
}
|
|
1593
|
+
get required() {
|
|
1594
|
+
return this.metadataOrUndefined(REQUIRED);
|
|
1595
|
+
}
|
|
1596
|
+
metadata(key) {
|
|
1597
|
+
return this.metadataState.get(key);
|
|
1598
|
+
}
|
|
1599
|
+
hasMetadata(key) {
|
|
1600
|
+
return this.metadataState.has(key);
|
|
1601
|
+
}
|
|
1602
|
+
markAsTouched() {
|
|
1603
|
+
this.nodeState.markAsTouched();
|
|
1604
|
+
}
|
|
1605
|
+
markAsDirty() {
|
|
1606
|
+
this.nodeState.markAsDirty();
|
|
1607
|
+
}
|
|
1608
|
+
reset() {
|
|
1609
|
+
this.nodeState.markAsUntouched();
|
|
1610
|
+
this.nodeState.markAsPristine();
|
|
1611
|
+
for (const child of this.structure.children()) {
|
|
1612
|
+
child.reset();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
static newRoot(fieldManager, value, pathNode, adapter) {
|
|
1616
|
+
return adapter.newRoot(fieldManager, value, pathNode, adapter);
|
|
1617
|
+
}
|
|
1618
|
+
static newChild(options) {
|
|
1619
|
+
return options.fieldAdapter.newChild(options);
|
|
1620
|
+
}
|
|
1621
|
+
createStructure(options) {
|
|
1622
|
+
return options.kind === 'root' ? new RootFieldNodeStructure(this, options.pathNode, options.logic, options.fieldManager, options.value, options.fieldAdapter, FieldNode.newChild) : new ChildFieldNodeStructure(this, options.pathNode, options.logic, options.parent, options.identityInParent, options.initialKeyInParent, options.fieldAdapter, FieldNode.newChild);
|
|
1623
|
+
}
|
|
2163
1624
|
}
|
|
2164
1625
|
|
|
2165
|
-
/**
|
|
2166
|
-
* The non-validation and non-submit state associated with a `FieldNode`, such as touched and dirty
|
|
2167
|
-
* status, as well as derived logical state.
|
|
2168
|
-
*/
|
|
2169
1626
|
class FieldNodeState {
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
selfDirty
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
*/
|
|
2232
|
-
touched = computed(() => {
|
|
2233
|
-
const selfTouchedValue = this.selfTouched() && !this.isNonInteractive();
|
|
2234
|
-
return reduceChildren(this.node, selfTouchedValue, (child, value) => value || child.nodeState.touched(), shortCircuitTrue);
|
|
2235
|
-
}, ...(ngDevMode ? [{ debugName: "touched" }] : []));
|
|
2236
|
-
/**
|
|
2237
|
-
* The reasons for this field's disablement. This includes disabled reasons for any parent field
|
|
2238
|
-
* that may have been disabled, indirectly causing this field to be disabled as well.
|
|
2239
|
-
* The `field` property of the `DisabledReason` can be used to determine which field ultimately
|
|
2240
|
-
* caused the disablement.
|
|
2241
|
-
*/
|
|
2242
|
-
disabledReasons = computed(() => [
|
|
2243
|
-
...(this.node.structure.parent?.nodeState.disabledReasons() ?? []),
|
|
2244
|
-
...this.node.logicNode.logic.disabledReasons.compute(this.node.context),
|
|
2245
|
-
], ...(ngDevMode ? [{ debugName: "disabledReasons" }] : []));
|
|
2246
|
-
/**
|
|
2247
|
-
* Whether this field is considered disabled.
|
|
2248
|
-
*
|
|
2249
|
-
* A field is considered disabled if one of the following is true:
|
|
2250
|
-
* - The schema contains logic that directly disabled it
|
|
2251
|
-
* - Its parent field is considered disabled
|
|
2252
|
-
*/
|
|
2253
|
-
disabled = computed(() => !!this.disabledReasons().length, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
2254
|
-
/**
|
|
2255
|
-
* Whether this field is considered readonly.
|
|
2256
|
-
*
|
|
2257
|
-
* A field is considered readonly if one of the following is true:
|
|
2258
|
-
* - The schema contains logic that directly made it readonly
|
|
2259
|
-
* - Its parent field is considered readonly
|
|
2260
|
-
*/
|
|
2261
|
-
readonly = computed(() => (this.node.structure.parent?.nodeState.readonly() ||
|
|
2262
|
-
this.node.logicNode.logic.readonly.compute(this.node.context)) ??
|
|
2263
|
-
false, ...(ngDevMode ? [{ debugName: "readonly" }] : []));
|
|
2264
|
-
/**
|
|
2265
|
-
* Whether this field is considered hidden.
|
|
2266
|
-
*
|
|
2267
|
-
* A field is considered hidden if one of the following is true:
|
|
2268
|
-
* - The schema contains logic that directly hides it
|
|
2269
|
-
* - Its parent field is considered hidden
|
|
2270
|
-
*/
|
|
2271
|
-
hidden = computed(() => (this.node.structure.parent?.nodeState.hidden() ||
|
|
2272
|
-
this.node.logicNode.logic.hidden.compute(this.node.context)) ??
|
|
2273
|
-
false, ...(ngDevMode ? [{ debugName: "hidden" }] : []));
|
|
2274
|
-
name = computed(() => {
|
|
2275
|
-
const parent = this.node.structure.parent;
|
|
2276
|
-
if (!parent) {
|
|
2277
|
-
return this.node.structure.fieldManager.rootName;
|
|
2278
|
-
}
|
|
2279
|
-
return `${parent.name()}.${this.node.structure.keyInParent()}`;
|
|
2280
|
-
}, ...(ngDevMode ? [{ debugName: "name" }] : []));
|
|
2281
|
-
/** Whether this field is considered non-interactive.
|
|
2282
|
-
*
|
|
2283
|
-
* A field is considered non-interactive if one of the following is true:
|
|
2284
|
-
* - It is hidden
|
|
2285
|
-
* - It is disabled
|
|
2286
|
-
* - It is readonly
|
|
2287
|
-
*/
|
|
2288
|
-
isNonInteractive = computed(() => this.hidden() || this.disabled() || this.readonly(), ...(ngDevMode ? [{ debugName: "isNonInteractive" }] : []));
|
|
1627
|
+
node;
|
|
1628
|
+
selfTouched = signal(false, ...(ngDevMode ? [{
|
|
1629
|
+
debugName: "selfTouched"
|
|
1630
|
+
}] : []));
|
|
1631
|
+
selfDirty = signal(false, ...(ngDevMode ? [{
|
|
1632
|
+
debugName: "selfDirty"
|
|
1633
|
+
}] : []));
|
|
1634
|
+
markAsTouched() {
|
|
1635
|
+
this.selfTouched.set(true);
|
|
1636
|
+
}
|
|
1637
|
+
markAsDirty() {
|
|
1638
|
+
this.selfDirty.set(true);
|
|
1639
|
+
}
|
|
1640
|
+
markAsPristine() {
|
|
1641
|
+
this.selfDirty.set(false);
|
|
1642
|
+
}
|
|
1643
|
+
markAsUntouched() {
|
|
1644
|
+
this.selfTouched.set(false);
|
|
1645
|
+
}
|
|
1646
|
+
fieldBindings = signal([], ...(ngDevMode ? [{
|
|
1647
|
+
debugName: "fieldBindings"
|
|
1648
|
+
}] : []));
|
|
1649
|
+
constructor(node) {
|
|
1650
|
+
this.node = node;
|
|
1651
|
+
}
|
|
1652
|
+
dirty = computed(() => {
|
|
1653
|
+
const selfDirtyValue = this.selfDirty() && !this.isNonInteractive();
|
|
1654
|
+
return reduceChildren(this.node, selfDirtyValue, (child, value) => value || child.nodeState.dirty(), shortCircuitTrue);
|
|
1655
|
+
}, ...(ngDevMode ? [{
|
|
1656
|
+
debugName: "dirty"
|
|
1657
|
+
}] : []));
|
|
1658
|
+
touched = computed(() => {
|
|
1659
|
+
const selfTouchedValue = this.selfTouched() && !this.isNonInteractive();
|
|
1660
|
+
return reduceChildren(this.node, selfTouchedValue, (child, value) => value || child.nodeState.touched(), shortCircuitTrue);
|
|
1661
|
+
}, ...(ngDevMode ? [{
|
|
1662
|
+
debugName: "touched"
|
|
1663
|
+
}] : []));
|
|
1664
|
+
disabledReasons = computed(() => [...(this.node.structure.parent?.nodeState.disabledReasons() ?? []), ...this.node.logicNode.logic.disabledReasons.compute(this.node.context)], ...(ngDevMode ? [{
|
|
1665
|
+
debugName: "disabledReasons"
|
|
1666
|
+
}] : []));
|
|
1667
|
+
disabled = computed(() => !!this.disabledReasons().length, ...(ngDevMode ? [{
|
|
1668
|
+
debugName: "disabled"
|
|
1669
|
+
}] : []));
|
|
1670
|
+
readonly = computed(() => (this.node.structure.parent?.nodeState.readonly() || this.node.logicNode.logic.readonly.compute(this.node.context)) ?? false, ...(ngDevMode ? [{
|
|
1671
|
+
debugName: "readonly"
|
|
1672
|
+
}] : []));
|
|
1673
|
+
hidden = computed(() => (this.node.structure.parent?.nodeState.hidden() || this.node.logicNode.logic.hidden.compute(this.node.context)) ?? false, ...(ngDevMode ? [{
|
|
1674
|
+
debugName: "hidden"
|
|
1675
|
+
}] : []));
|
|
1676
|
+
name = computed(() => {
|
|
1677
|
+
const parent = this.node.structure.parent;
|
|
1678
|
+
if (!parent) {
|
|
1679
|
+
return this.node.structure.fieldManager.rootName;
|
|
1680
|
+
}
|
|
1681
|
+
return `${parent.name()}.${this.node.structure.keyInParent()}`;
|
|
1682
|
+
}, ...(ngDevMode ? [{
|
|
1683
|
+
debugName: "name"
|
|
1684
|
+
}] : []));
|
|
1685
|
+
isNonInteractive = computed(() => this.hidden() || this.disabled() || this.readonly(), ...(ngDevMode ? [{
|
|
1686
|
+
debugName: "isNonInteractive"
|
|
1687
|
+
}] : []));
|
|
2289
1688
|
}
|
|
2290
1689
|
|
|
2291
|
-
/**
|
|
2292
|
-
* Basic adapter supporting standard form behavior.
|
|
2293
|
-
*/
|
|
2294
1690
|
class BasicFieldAdapter {
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
return new FieldNode(options);
|
|
2318
|
-
}
|
|
2319
|
-
/**
|
|
2320
|
-
* Creates a node state.
|
|
2321
|
-
* @param node
|
|
2322
|
-
*/
|
|
2323
|
-
createNodeState(node) {
|
|
2324
|
-
return new FieldNodeState(node);
|
|
2325
|
-
}
|
|
2326
|
-
/**
|
|
2327
|
-
* Creates a validation state.
|
|
2328
|
-
* @param node
|
|
2329
|
-
*/
|
|
2330
|
-
createValidationState(node) {
|
|
2331
|
-
return new FieldValidationState(node);
|
|
2332
|
-
}
|
|
2333
|
-
/**
|
|
2334
|
-
* Creates a node structure.
|
|
2335
|
-
* @param node
|
|
2336
|
-
* @param options
|
|
2337
|
-
*/
|
|
2338
|
-
createStructure(node, options) {
|
|
2339
|
-
return node.createStructure(options);
|
|
2340
|
-
}
|
|
1691
|
+
newRoot(fieldManager, value, pathNode, adapter) {
|
|
1692
|
+
return new FieldNode({
|
|
1693
|
+
kind: 'root',
|
|
1694
|
+
fieldManager,
|
|
1695
|
+
value,
|
|
1696
|
+
pathNode,
|
|
1697
|
+
logic: pathNode.builder.build(),
|
|
1698
|
+
fieldAdapter: adapter
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
newChild(options) {
|
|
1702
|
+
return new FieldNode(options);
|
|
1703
|
+
}
|
|
1704
|
+
createNodeState(node) {
|
|
1705
|
+
return new FieldNodeState(node);
|
|
1706
|
+
}
|
|
1707
|
+
createValidationState(node) {
|
|
1708
|
+
return new FieldValidationState(node);
|
|
1709
|
+
}
|
|
1710
|
+
createStructure(node, options) {
|
|
1711
|
+
return node.createStructure(options);
|
|
1712
|
+
}
|
|
2341
1713
|
}
|
|
2342
1714
|
|
|
2343
|
-
/**
|
|
2344
|
-
* Manages the collection of fields associated with a given `form`.
|
|
2345
|
-
*
|
|
2346
|
-
* Fields are created implicitly, through reactivity, and may create "owned" entities like effects
|
|
2347
|
-
* or resources. When a field is no longer connected to the form, these owned entities should be
|
|
2348
|
-
* destroyed, which is the job of the `FormFieldManager`.
|
|
2349
|
-
*/
|
|
2350
1715
|
class FormFieldManager {
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
createFieldManagementEffect(root) {
|
|
2377
|
-
effect(() => {
|
|
2378
|
-
const liveStructures = new Set();
|
|
2379
|
-
this.markStructuresLive(root, liveStructures);
|
|
2380
|
-
// Destroy all nodes that are no longer live.
|
|
2381
|
-
for (const structure of this.structures) {
|
|
2382
|
-
if (!liveStructures.has(structure)) {
|
|
2383
|
-
this.structures.delete(structure);
|
|
2384
|
-
untracked(() => structure.destroy());
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
}, { injector: this.injector });
|
|
2388
|
-
}
|
|
2389
|
-
/**
|
|
2390
|
-
* Collects all structures reachable from the given structure into the given set.
|
|
2391
|
-
*
|
|
2392
|
-
* @param structure The root structure
|
|
2393
|
-
* @param liveStructures The set of reachable structures to populate
|
|
2394
|
-
*/
|
|
2395
|
-
markStructuresLive(structure, liveStructures) {
|
|
2396
|
-
liveStructures.add(structure);
|
|
2397
|
-
for (const child of structure.children()) {
|
|
2398
|
-
this.markStructuresLive(child.structure, liveStructures);
|
|
2399
|
-
}
|
|
1716
|
+
injector;
|
|
1717
|
+
rootName;
|
|
1718
|
+
constructor(injector, rootName) {
|
|
1719
|
+
this.injector = injector;
|
|
1720
|
+
this.rootName = rootName ?? `${this.injector.get(APP_ID)}.form${nextFormId++}`;
|
|
1721
|
+
}
|
|
1722
|
+
structures = new Set();
|
|
1723
|
+
createFieldManagementEffect(root) {
|
|
1724
|
+
effect(() => {
|
|
1725
|
+
const liveStructures = new Set();
|
|
1726
|
+
this.markStructuresLive(root, liveStructures);
|
|
1727
|
+
for (const structure of this.structures) {
|
|
1728
|
+
if (!liveStructures.has(structure)) {
|
|
1729
|
+
this.structures.delete(structure);
|
|
1730
|
+
untracked(() => structure.destroy());
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}, {
|
|
1734
|
+
injector: this.injector
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
markStructuresLive(structure, liveStructures) {
|
|
1738
|
+
liveStructures.add(structure);
|
|
1739
|
+
for (const child of structure.children()) {
|
|
1740
|
+
this.markStructuresLive(child.structure, liveStructures);
|
|
2400
1741
|
}
|
|
1742
|
+
}
|
|
2401
1743
|
}
|
|
2402
1744
|
let nextFormId = 0;
|
|
2403
1745
|
|
|
2404
|
-
/** Extracts the model, schema, and options from the arguments passed to `form()`. */
|
|
2405
1746
|
function normalizeFormArgs(args) {
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
[model] = args;
|
|
2422
|
-
}
|
|
2423
|
-
return [model, schema, options];
|
|
1747
|
+
let model;
|
|
1748
|
+
let schema;
|
|
1749
|
+
let options;
|
|
1750
|
+
if (args.length === 3) {
|
|
1751
|
+
[model, schema, options] = args;
|
|
1752
|
+
} else if (args.length === 2) {
|
|
1753
|
+
if (isSchemaOrSchemaFn(args[1])) {
|
|
1754
|
+
[model, schema] = args;
|
|
1755
|
+
} else {
|
|
1756
|
+
[model, options] = args;
|
|
1757
|
+
}
|
|
1758
|
+
} else {
|
|
1759
|
+
[model] = args;
|
|
1760
|
+
}
|
|
1761
|
+
return [model, schema, options];
|
|
2424
1762
|
}
|
|
2425
1763
|
function form(...args) {
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
1764
|
+
const [model, schema, options] = normalizeFormArgs(args);
|
|
1765
|
+
const injector = options?.injector ?? inject(Injector);
|
|
1766
|
+
const pathNode = runInInjectionContext(injector, () => SchemaImpl.rootCompile(schema));
|
|
1767
|
+
const fieldManager = new FormFieldManager(injector, options?.name);
|
|
1768
|
+
const adapter = options?.adapter ?? new BasicFieldAdapter();
|
|
1769
|
+
const fieldRoot = FieldNode.newRoot(fieldManager, model, pathNode, adapter);
|
|
1770
|
+
fieldManager.createFieldManagementEffect(fieldRoot.structure);
|
|
1771
|
+
return fieldRoot.fieldProxy;
|
|
2434
1772
|
}
|
|
2435
|
-
/**
|
|
2436
|
-
* Applies a schema to each item of an array.
|
|
2437
|
-
*
|
|
2438
|
-
* @example
|
|
2439
|
-
* ```
|
|
2440
|
-
* const nameSchema = schema<{first: string, last: string}>((name) => {
|
|
2441
|
-
* required(name.first);
|
|
2442
|
-
* required(name.last);
|
|
2443
|
-
* });
|
|
2444
|
-
* const namesForm = form(signal([{first: '', last: ''}]), (names) => {
|
|
2445
|
-
* applyEach(names, nameSchema);
|
|
2446
|
-
* });
|
|
2447
|
-
* ```
|
|
2448
|
-
*
|
|
2449
|
-
* When binding logic to the array items, the `FieldTree` for the array item is passed as an
|
|
2450
|
-
* additional argument. This can be used to reference other properties on the item.
|
|
2451
|
-
*
|
|
2452
|
-
* @example
|
|
2453
|
-
* ```
|
|
2454
|
-
* const namesForm = form(signal([{first: '', last: ''}]), (names) => {
|
|
2455
|
-
* applyEach(names, (name) => {
|
|
2456
|
-
* error(
|
|
2457
|
-
* name.last,
|
|
2458
|
-
* (value, nameField) => value === nameField.first().value(),
|
|
2459
|
-
* 'Last name must be different than first name',
|
|
2460
|
-
* );
|
|
2461
|
-
* });
|
|
2462
|
-
* });
|
|
2463
|
-
* ```
|
|
2464
|
-
*
|
|
2465
|
-
* @param path The target path for an array field whose items the schema will be applied to.
|
|
2466
|
-
* @param schema A schema for an element of the array, or function that binds logic to an
|
|
2467
|
-
* element of the array.
|
|
2468
|
-
* @template TValue The data type of the item field to apply the schema to.
|
|
2469
|
-
*
|
|
2470
|
-
* @category structure
|
|
2471
|
-
* @experimental 21.0.0
|
|
2472
|
-
*/
|
|
2473
1773
|
function applyEach(path, schema) {
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
1774
|
+
assertPathIsCurrent(path);
|
|
1775
|
+
const elementPath = FieldPathNode.unwrapFieldPath(path).element.fieldPathProxy;
|
|
1776
|
+
apply(elementPath, schema);
|
|
2477
1777
|
}
|
|
2478
|
-
/**
|
|
2479
|
-
* Applies a predefined schema to a given `FieldPath`.
|
|
2480
|
-
*
|
|
2481
|
-
* @example
|
|
2482
|
-
* ```
|
|
2483
|
-
* const nameSchema = schema<{first: string, last: string}>((name) => {
|
|
2484
|
-
* required(name.first);
|
|
2485
|
-
* required(name.last);
|
|
2486
|
-
* });
|
|
2487
|
-
* const profileForm = form(signal({name: {first: '', last: ''}, age: 0}), (profile) => {
|
|
2488
|
-
* apply(profile.name, nameSchema);
|
|
2489
|
-
* });
|
|
2490
|
-
* ```
|
|
2491
|
-
*
|
|
2492
|
-
* @param path The target path to apply the schema to.
|
|
2493
|
-
* @param schema The schema to apply to the property
|
|
2494
|
-
* @template TValue The data type of the field to apply the schema to.
|
|
2495
|
-
*
|
|
2496
|
-
* @category structure
|
|
2497
|
-
* @experimental 21.0.0
|
|
2498
|
-
*/
|
|
2499
1778
|
function apply(path, schema) {
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
1779
|
+
assertPathIsCurrent(path);
|
|
1780
|
+
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
|
1781
|
+
pathNode.mergeIn(SchemaImpl.create(schema));
|
|
2503
1782
|
}
|
|
2504
|
-
/**
|
|
2505
|
-
* Conditionally applies a predefined schema to a given `FieldPath`.
|
|
2506
|
-
*
|
|
2507
|
-
* @param path The target path to apply the schema to.
|
|
2508
|
-
* @param logic A `LogicFn<T, boolean>` that returns `true` when the schema should be applied.
|
|
2509
|
-
* @param schema The schema to apply to the field when the `logic` function returns `true`.
|
|
2510
|
-
* @template TValue The data type of the field to apply the schema to.
|
|
2511
|
-
*
|
|
2512
|
-
* @category structure
|
|
2513
|
-
* @experimental 21.0.0
|
|
2514
|
-
*/
|
|
2515
1783
|
function applyWhen(path, logic, schema) {
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
1784
|
+
assertPathIsCurrent(path);
|
|
1785
|
+
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
|
1786
|
+
pathNode.mergeIn(SchemaImpl.create(schema), {
|
|
1787
|
+
fn: logic,
|
|
1788
|
+
path
|
|
1789
|
+
});
|
|
2519
1790
|
}
|
|
2520
1791
|
function applyWhenValue(path, predicate, schema) {
|
|
2521
|
-
|
|
1792
|
+
applyWhen(path, ({
|
|
1793
|
+
value
|
|
1794
|
+
}) => predicate(value()), schema);
|
|
2522
1795
|
}
|
|
2523
|
-
/**
|
|
2524
|
-
* Submits a given `FieldTree` using the given action function and applies any server errors
|
|
2525
|
-
* resulting from the action to the field. Server errors returned by the `action` will be integrated
|
|
2526
|
-
* into the field as a `ValidationError` on the sub-field indicated by the `field` property of the
|
|
2527
|
-
* server error.
|
|
2528
|
-
*
|
|
2529
|
-
* @example
|
|
2530
|
-
* ```
|
|
2531
|
-
* async function registerNewUser(registrationForm: FieldTree<{username: string, password: string}>) {
|
|
2532
|
-
* const result = await myClient.registerNewUser(registrationForm().value());
|
|
2533
|
-
* if (result.errorCode === myClient.ErrorCode.USERNAME_TAKEN) {
|
|
2534
|
-
* return [{
|
|
2535
|
-
* field: registrationForm.username,
|
|
2536
|
-
* error: {kind: 'server', message: 'Username already taken'}
|
|
2537
|
-
* }];
|
|
2538
|
-
* }
|
|
2539
|
-
* return undefined;
|
|
2540
|
-
* }
|
|
2541
|
-
*
|
|
2542
|
-
* const registrationForm = form(signal({username: 'god', password: ''}));
|
|
2543
|
-
* submit(registrationForm, async (f) => {
|
|
2544
|
-
* return registerNewUser(registrationForm);
|
|
2545
|
-
* });
|
|
2546
|
-
* registrationForm.username().errors(); // [{kind: 'server', message: 'Username already taken'}]
|
|
2547
|
-
* ```
|
|
2548
|
-
*
|
|
2549
|
-
* @param form The field to submit.
|
|
2550
|
-
* @param action An asynchronous action used to submit the field. The action may return server
|
|
2551
|
-
* errors.
|
|
2552
|
-
* @template TValue The data type of the field being submitted.
|
|
2553
|
-
*
|
|
2554
|
-
* @category submission
|
|
2555
|
-
* @experimental 21.0.0
|
|
2556
|
-
*/
|
|
2557
1796
|
async function submit(form, action) {
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
node.submitState.selfSubmitting.set(false);
|
|
2571
|
-
}
|
|
1797
|
+
const node = form();
|
|
1798
|
+
markAllAsTouched(node);
|
|
1799
|
+
if (node.invalid()) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
node.submitState.selfSubmitting.set(true);
|
|
1803
|
+
try {
|
|
1804
|
+
const errors = await action(form);
|
|
1805
|
+
errors && setServerErrors(node, errors);
|
|
1806
|
+
} finally {
|
|
1807
|
+
node.submitState.selfSubmitting.set(false);
|
|
1808
|
+
}
|
|
2572
1809
|
}
|
|
2573
|
-
/**
|
|
2574
|
-
* Sets a list of server errors to their individual fields.
|
|
2575
|
-
*
|
|
2576
|
-
* @param submittedField The field that was submitted, resulting in the errors.
|
|
2577
|
-
* @param errors The errors to set.
|
|
2578
|
-
*/
|
|
2579
1810
|
function setServerErrors(submittedField, errors) {
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
1811
|
+
if (!isArray(errors)) {
|
|
1812
|
+
errors = [errors];
|
|
1813
|
+
}
|
|
1814
|
+
const errorsByField = new Map();
|
|
1815
|
+
for (const error of errors) {
|
|
1816
|
+
const errorWithField = addDefaultField(error, submittedField.fieldProxy);
|
|
1817
|
+
const field = errorWithField.field();
|
|
1818
|
+
let fieldErrors = errorsByField.get(field);
|
|
1819
|
+
if (!fieldErrors) {
|
|
1820
|
+
fieldErrors = [];
|
|
1821
|
+
errorsByField.set(field, fieldErrors);
|
|
1822
|
+
}
|
|
1823
|
+
fieldErrors.push(errorWithField);
|
|
1824
|
+
}
|
|
1825
|
+
for (const [field, fieldErrors] of errorsByField) {
|
|
1826
|
+
field.submitState.serverErrors.set(fieldErrors);
|
|
1827
|
+
}
|
|
2597
1828
|
}
|
|
2598
|
-
/**
|
|
2599
|
-
* Creates a `Schema` that adds logic rules to a form.
|
|
2600
|
-
* @param fn A **non-reactive** function that sets up reactive logic rules for the form.
|
|
2601
|
-
* @returns A schema object that implements the given logic.
|
|
2602
|
-
* @template TValue The value type of a `FieldTree` that this schema binds to.
|
|
2603
|
-
*
|
|
2604
|
-
* @category structure
|
|
2605
|
-
* @experimental 21.0.0
|
|
2606
|
-
*/
|
|
2607
1829
|
function schema(fn) {
|
|
2608
|
-
|
|
1830
|
+
return SchemaImpl.create(fn);
|
|
2609
1831
|
}
|
|
2610
|
-
/** Marks a {@link node} and its descendants as touched. */
|
|
2611
1832
|
function markAllAsTouched(node) {
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
function requiredError(options) {
|
|
2619
|
-
return new RequiredValidationError(options);
|
|
2620
|
-
}
|
|
2621
|
-
function minError(min, options) {
|
|
2622
|
-
return new MinValidationError(min, options);
|
|
2623
|
-
}
|
|
2624
|
-
function maxError(max, options) {
|
|
2625
|
-
return new MaxValidationError(max, options);
|
|
2626
|
-
}
|
|
2627
|
-
function minLengthError(minLength, options) {
|
|
2628
|
-
return new MinLengthValidationError(minLength, options);
|
|
2629
|
-
}
|
|
2630
|
-
function maxLengthError(maxLength, options) {
|
|
2631
|
-
return new MaxLengthValidationError(maxLength, options);
|
|
2632
|
-
}
|
|
2633
|
-
function patternError(pattern, options) {
|
|
2634
|
-
return new PatternValidationError(pattern, options);
|
|
2635
|
-
}
|
|
2636
|
-
function emailError(options) {
|
|
2637
|
-
return new EmailValidationError(options);
|
|
2638
|
-
}
|
|
2639
|
-
function standardSchemaError(issue, options) {
|
|
2640
|
-
return new StandardSchemaValidationError(issue, options);
|
|
2641
|
-
}
|
|
2642
|
-
function customError(obj) {
|
|
2643
|
-
return new CustomValidationError(obj);
|
|
2644
|
-
}
|
|
2645
|
-
/**
|
|
2646
|
-
* A custom error that may contain additional properties
|
|
2647
|
-
*
|
|
2648
|
-
* @category validation
|
|
2649
|
-
* @experimental 21.0.0
|
|
2650
|
-
*/
|
|
2651
|
-
class CustomValidationError {
|
|
2652
|
-
/** Brand the class to avoid Typescript structural matching */
|
|
2653
|
-
__brand = undefined;
|
|
2654
|
-
/** Identifies the kind of error. */
|
|
2655
|
-
kind = '';
|
|
2656
|
-
/** The field associated with this error. */
|
|
2657
|
-
field;
|
|
2658
|
-
/** Human readable error message. */
|
|
2659
|
-
message;
|
|
2660
|
-
constructor(options) {
|
|
2661
|
-
if (options) {
|
|
2662
|
-
Object.assign(this, options);
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
/**
|
|
2667
|
-
* Internal version of `NgValidationError`, we create this separately so we can change its type on
|
|
2668
|
-
* the exported version to a type union of the possible sub-classes.
|
|
2669
|
-
*
|
|
2670
|
-
* @experimental 21.0.0
|
|
2671
|
-
*/
|
|
2672
|
-
class _NgValidationError {
|
|
2673
|
-
/** Brand the class to avoid Typescript structural matching */
|
|
2674
|
-
__brand = undefined;
|
|
2675
|
-
/** Identifies the kind of error. */
|
|
2676
|
-
kind = '';
|
|
2677
|
-
/** The field associated with this error. */
|
|
2678
|
-
field;
|
|
2679
|
-
/** Human readable error message. */
|
|
2680
|
-
message;
|
|
2681
|
-
constructor(options) {
|
|
2682
|
-
if (options) {
|
|
2683
|
-
Object.assign(this, options);
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
/**
|
|
2688
|
-
* An error used to indicate that a required field is empty.
|
|
2689
|
-
*
|
|
2690
|
-
* @category validation
|
|
2691
|
-
* @experimental 21.0.0
|
|
2692
|
-
*/
|
|
2693
|
-
class RequiredValidationError extends _NgValidationError {
|
|
2694
|
-
kind = 'required';
|
|
2695
|
-
}
|
|
2696
|
-
/**
|
|
2697
|
-
* An error used to indicate that a value is lower than the minimum allowed.
|
|
2698
|
-
*
|
|
2699
|
-
* @category validation
|
|
2700
|
-
* @experimental 21.0.0
|
|
2701
|
-
*/
|
|
2702
|
-
class MinValidationError extends _NgValidationError {
|
|
2703
|
-
min;
|
|
2704
|
-
kind = 'min';
|
|
2705
|
-
constructor(min, options) {
|
|
2706
|
-
super(options);
|
|
2707
|
-
this.min = min;
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
|
-
/**
|
|
2711
|
-
* An error used to indicate that a value is higher than the maximum allowed.
|
|
2712
|
-
*
|
|
2713
|
-
* @category validation
|
|
2714
|
-
* @experimental 21.0.0
|
|
2715
|
-
*/
|
|
2716
|
-
class MaxValidationError extends _NgValidationError {
|
|
2717
|
-
max;
|
|
2718
|
-
kind = 'max';
|
|
2719
|
-
constructor(max, options) {
|
|
2720
|
-
super(options);
|
|
2721
|
-
this.max = max;
|
|
2722
|
-
}
|
|
2723
|
-
}
|
|
2724
|
-
/**
|
|
2725
|
-
* An error used to indicate that a value is shorter than the minimum allowed length.
|
|
2726
|
-
*
|
|
2727
|
-
* @category validation
|
|
2728
|
-
* @experimental 21.0.0
|
|
2729
|
-
*/
|
|
2730
|
-
class MinLengthValidationError extends _NgValidationError {
|
|
2731
|
-
minLength;
|
|
2732
|
-
kind = 'minLength';
|
|
2733
|
-
constructor(minLength, options) {
|
|
2734
|
-
super(options);
|
|
2735
|
-
this.minLength = minLength;
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
/**
|
|
2739
|
-
* An error used to indicate that a value is longer than the maximum allowed length.
|
|
2740
|
-
*
|
|
2741
|
-
* @category validation
|
|
2742
|
-
* @experimental 21.0.0
|
|
2743
|
-
*/
|
|
2744
|
-
class MaxLengthValidationError extends _NgValidationError {
|
|
2745
|
-
maxLength;
|
|
2746
|
-
kind = 'maxLength';
|
|
2747
|
-
constructor(maxLength, options) {
|
|
2748
|
-
super(options);
|
|
2749
|
-
this.maxLength = maxLength;
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2752
|
-
/**
|
|
2753
|
-
* An error used to indicate that a value does not match the required pattern.
|
|
2754
|
-
*
|
|
2755
|
-
* @category validation
|
|
2756
|
-
* @experimental 21.0.0
|
|
2757
|
-
*/
|
|
2758
|
-
class PatternValidationError extends _NgValidationError {
|
|
2759
|
-
pattern;
|
|
2760
|
-
kind = 'pattern';
|
|
2761
|
-
constructor(pattern, options) {
|
|
2762
|
-
super(options);
|
|
2763
|
-
this.pattern = pattern;
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
|
-
/**
|
|
2767
|
-
* An error used to indicate that a value is not a valid email.
|
|
2768
|
-
*
|
|
2769
|
-
* @category validation
|
|
2770
|
-
* @experimental 21.0.0
|
|
2771
|
-
*/
|
|
2772
|
-
class EmailValidationError extends _NgValidationError {
|
|
2773
|
-
kind = 'email';
|
|
2774
|
-
}
|
|
2775
|
-
/**
|
|
2776
|
-
* An error used to indicate an issue validating against a standard schema.
|
|
2777
|
-
*
|
|
2778
|
-
* @category validation
|
|
2779
|
-
* @experimental 21.0.0
|
|
2780
|
-
*/
|
|
2781
|
-
class StandardSchemaValidationError extends _NgValidationError {
|
|
2782
|
-
issue;
|
|
2783
|
-
kind = 'standardSchema';
|
|
2784
|
-
constructor(issue, options) {
|
|
2785
|
-
super(options);
|
|
2786
|
-
this.issue = issue;
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
/**
|
|
2790
|
-
* The base class for all built-in, non-custom errors. This class can be used to check if an error
|
|
2791
|
-
* is one of the standard kinds, allowing you to switch on the kind to further narrow the type.
|
|
2792
|
-
*
|
|
2793
|
-
* @example
|
|
2794
|
-
* ```
|
|
2795
|
-
* const f = form(...);
|
|
2796
|
-
* for (const e of form().errors()) {
|
|
2797
|
-
* if (e instanceof NgValidationError) {
|
|
2798
|
-
* switch(e.kind) {
|
|
2799
|
-
* case 'required':
|
|
2800
|
-
* console.log('This is required!');
|
|
2801
|
-
* break;
|
|
2802
|
-
* case 'min':
|
|
2803
|
-
* console.log(`Must be at least ${e.min}`);
|
|
2804
|
-
* break;
|
|
2805
|
-
* ...
|
|
2806
|
-
* }
|
|
2807
|
-
* }
|
|
2808
|
-
* }
|
|
2809
|
-
* ```
|
|
2810
|
-
*
|
|
2811
|
-
* @category validation
|
|
2812
|
-
* @experimental 21.0.0
|
|
2813
|
-
*/
|
|
2814
|
-
const NgValidationError = _NgValidationError;
|
|
2815
|
-
|
|
2816
|
-
/** Gets the length or size of the given value. */
|
|
2817
|
-
function getLengthOrSize(value) {
|
|
2818
|
-
const v = value;
|
|
2819
|
-
return typeof v.length === 'number' ? v.length : v.size;
|
|
2820
|
-
}
|
|
2821
|
-
/**
|
|
2822
|
-
* Gets the value for an option that may be either a static value or a logic function that produces
|
|
2823
|
-
* the option value.
|
|
2824
|
-
*
|
|
2825
|
-
* @param opt The option from BaseValidatorConfig.
|
|
2826
|
-
* @param ctx The current FieldContext.
|
|
2827
|
-
* @returns The value for the option.
|
|
2828
|
-
*/
|
|
2829
|
-
function getOption(opt, ctx) {
|
|
2830
|
-
return opt instanceof Function ? opt(ctx) : opt;
|
|
2831
|
-
}
|
|
2832
|
-
/**
|
|
2833
|
-
* Checks if the given value is considered empty. Empty values are: null, undefined, '', false, NaN.
|
|
2834
|
-
*/
|
|
2835
|
-
function isEmpty(value) {
|
|
2836
|
-
if (typeof value === 'number') {
|
|
2837
|
-
return isNaN(value);
|
|
2838
|
-
}
|
|
2839
|
-
return value === '' || value === false || value == null;
|
|
1833
|
+
node.markAsTouched();
|
|
1834
|
+
for (const child of node.structure.children()) {
|
|
1835
|
+
markAllAsTouched(child);
|
|
1836
|
+
}
|
|
2840
1837
|
}
|
|
2841
1838
|
|
|
2842
|
-
/**
|
|
2843
|
-
* A regular expression that matches valid e-mail addresses.
|
|
2844
|
-
*
|
|
2845
|
-
* At a high level, this regexp matches e-mail addresses of the format `local-part@tld`, where:
|
|
2846
|
-
* - `local-part` consists of one or more of the allowed characters (alphanumeric and some
|
|
2847
|
-
* punctuation symbols).
|
|
2848
|
-
* - `local-part` cannot begin or end with a period (`.`).
|
|
2849
|
-
* - `local-part` cannot be longer than 64 characters.
|
|
2850
|
-
* - `tld` consists of one or more `labels` separated by periods (`.`). For example `localhost` or
|
|
2851
|
-
* `foo.com`.
|
|
2852
|
-
* - A `label` consists of one or more of the allowed characters (alphanumeric, dashes (`-`) and
|
|
2853
|
-
* periods (`.`)).
|
|
2854
|
-
* - A `label` cannot begin or end with a dash (`-`) or a period (`.`).
|
|
2855
|
-
* - A `label` cannot be longer than 63 characters.
|
|
2856
|
-
* - The whole address cannot be longer than 254 characters.
|
|
2857
|
-
*
|
|
2858
|
-
* ## Implementation background
|
|
2859
|
-
*
|
|
2860
|
-
* This regexp was ported over from AngularJS (see there for git history):
|
|
2861
|
-
* https://github.com/angular/angular.js/blob/c133ef836/src/ng/directive/input.js#L27
|
|
2862
|
-
* It is based on the
|
|
2863
|
-
* [WHATWG version](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) with
|
|
2864
|
-
* some enhancements to incorporate more RFC rules (such as rules related to domain names and the
|
|
2865
|
-
* lengths of different parts of the address). The main differences from the WHATWG version are:
|
|
2866
|
-
* - Disallow `local-part` to begin or end with a period (`.`).
|
|
2867
|
-
* - Disallow `local-part` length to exceed 64 characters.
|
|
2868
|
-
* - Disallow total address length to exceed 254 characters.
|
|
2869
|
-
*
|
|
2870
|
-
* See [this commit](https://github.com/angular/angular.js/commit/f3f5cf72e) for more details.
|
|
2871
|
-
*/
|
|
2872
1839
|
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])?)*$/;
|
|
2873
|
-
/**
|
|
2874
|
-
* Binds a validator to the given path that requires the value to match the standard email format.
|
|
2875
|
-
* This function can only be called on string paths.
|
|
2876
|
-
*
|
|
2877
|
-
* @param path Path of the field to validate
|
|
2878
|
-
* @param config Optional, allows providing any of the following options:
|
|
2879
|
-
* - `error`: Custom validation error(s) to be used instead of the default `ValidationError.email()`
|
|
2880
|
-
* or a function that receives the `FieldContext` and returns custom validation error(s).
|
|
2881
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
2882
|
-
*
|
|
2883
|
-
* @category validation
|
|
2884
|
-
* @experimental 21.0.0
|
|
2885
|
-
*/
|
|
2886
1840
|
function email(path, config) {
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
1841
|
+
validate(path, ctx => {
|
|
1842
|
+
if (isEmpty(ctx.value())) {
|
|
1843
|
+
return undefined;
|
|
1844
|
+
}
|
|
1845
|
+
if (!EMAIL_REGEXP.test(ctx.value())) {
|
|
1846
|
+
if (config?.error) {
|
|
1847
|
+
return getOption(config.error, ctx);
|
|
1848
|
+
} else {
|
|
1849
|
+
return emailError({
|
|
1850
|
+
message: getOption(config?.message, ctx)
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
return undefined;
|
|
1855
|
+
});
|
|
2901
1856
|
}
|
|
2902
1857
|
|
|
2903
|
-
/**
|
|
2904
|
-
* Binds a validator to the given path that requires the value to be less than or equal to the
|
|
2905
|
-
* given `maxValue`.
|
|
2906
|
-
* This function can only be called on number paths.
|
|
2907
|
-
* In addition to binding a validator, this function adds `MAX` property to the field.
|
|
2908
|
-
*
|
|
2909
|
-
* @param path Path of the field to validate
|
|
2910
|
-
* @param maxValue The maximum value, or a LogicFn that returns the maximum value.
|
|
2911
|
-
* @param config Optional, allows providing any of the following options:
|
|
2912
|
-
* - `error`: Custom validation error(s) to be used instead of the default `ValidationError.max(maxValue)`
|
|
2913
|
-
* or a function that receives the `FieldContext` and returns custom validation error(s).
|
|
2914
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
2915
|
-
*
|
|
2916
|
-
* @category validation
|
|
2917
|
-
* @experimental 21.0.0
|
|
2918
|
-
*/
|
|
2919
1858
|
function max(path, maxValue, config) {
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
1859
|
+
const MAX_MEMO = metadata(path, ctx => computed(() => typeof maxValue === 'number' ? maxValue : maxValue(ctx)));
|
|
1860
|
+
aggregateMetadata(path, MAX, ({
|
|
1861
|
+
state
|
|
1862
|
+
}) => state.metadata(MAX_MEMO)());
|
|
1863
|
+
validate(path, ctx => {
|
|
1864
|
+
if (isEmpty(ctx.value())) {
|
|
1865
|
+
return undefined;
|
|
1866
|
+
}
|
|
1867
|
+
const max = ctx.state.metadata(MAX_MEMO)();
|
|
1868
|
+
if (max === undefined || Number.isNaN(max)) {
|
|
1869
|
+
return undefined;
|
|
1870
|
+
}
|
|
1871
|
+
if (ctx.value() > max) {
|
|
1872
|
+
if (config?.error) {
|
|
1873
|
+
return getOption(config.error, ctx);
|
|
1874
|
+
} else {
|
|
1875
|
+
return maxError(max, {
|
|
1876
|
+
message: getOption(config?.message, ctx)
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return undefined;
|
|
1881
|
+
});
|
|
2940
1882
|
}
|
|
2941
1883
|
|
|
2942
|
-
/**
|
|
2943
|
-
* Binds a validator to the given path that requires the length of the value to be less than or
|
|
2944
|
-
* equal to the given `maxLength`.
|
|
2945
|
-
* This function can only be called on string or array paths.
|
|
2946
|
-
* In addition to binding a validator, this function adds `MAX_LENGTH` property to the field.
|
|
2947
|
-
*
|
|
2948
|
-
* @param path Path of the field to validate
|
|
2949
|
-
* @param maxLength The maximum length, or a LogicFn that returns the maximum length.
|
|
2950
|
-
* @param config Optional, allows providing any of the following options:
|
|
2951
|
-
* - `error`: Custom validation error(s) to be used instead of the default `ValidationError.maxLength(maxLength)`
|
|
2952
|
-
* or a function that receives the `FieldContext` and returns custom validation error(s).
|
|
2953
|
-
* @template TValue The type of value stored in the field the logic is bound to.
|
|
2954
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
2955
|
-
*
|
|
2956
|
-
* @category validation
|
|
2957
|
-
* @experimental 21.0.0
|
|
2958
|
-
*/
|
|
2959
1884
|
function maxLength(path, maxLength, config) {
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
1885
|
+
const MAX_LENGTH_MEMO = metadata(path, ctx => computed(() => typeof maxLength === 'number' ? maxLength : maxLength(ctx)));
|
|
1886
|
+
aggregateMetadata(path, MAX_LENGTH, ({
|
|
1887
|
+
state
|
|
1888
|
+
}) => state.metadata(MAX_LENGTH_MEMO)());
|
|
1889
|
+
validate(path, ctx => {
|
|
1890
|
+
if (isEmpty(ctx.value())) {
|
|
1891
|
+
return undefined;
|
|
1892
|
+
}
|
|
1893
|
+
const maxLength = ctx.state.metadata(MAX_LENGTH_MEMO)();
|
|
1894
|
+
if (maxLength === undefined) {
|
|
1895
|
+
return undefined;
|
|
1896
|
+
}
|
|
1897
|
+
if (getLengthOrSize(ctx.value()) > maxLength) {
|
|
1898
|
+
if (config?.error) {
|
|
1899
|
+
return getOption(config.error, ctx);
|
|
1900
|
+
} else {
|
|
1901
|
+
return maxLengthError(maxLength, {
|
|
1902
|
+
message: getOption(config?.message, ctx)
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
return undefined;
|
|
1907
|
+
});
|
|
2980
1908
|
}
|
|
2981
1909
|
|
|
2982
|
-
/**
|
|
2983
|
-
* Binds a validator to the given path that requires the value to be greater than or equal to
|
|
2984
|
-
* the given `minValue`.
|
|
2985
|
-
* This function can only be called on number paths.
|
|
2986
|
-
* In addition to binding a validator, this function adds `MIN` property to the field.
|
|
2987
|
-
*
|
|
2988
|
-
* @param path Path of the field to validate
|
|
2989
|
-
* @param minValue The minimum value, or a LogicFn that returns the minimum value.
|
|
2990
|
-
* @param config Optional, allows providing any of the following options:
|
|
2991
|
-
* - `error`: Custom validation error(s) to be used instead of the default `ValidationError.min(minValue)`
|
|
2992
|
-
* or a function that receives the `FieldContext` and returns custom validation error(s).
|
|
2993
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
2994
|
-
*
|
|
2995
|
-
* @category validation
|
|
2996
|
-
* @experimental 21.0.0
|
|
2997
|
-
*/
|
|
2998
1910
|
function min(path, minValue, config) {
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
1911
|
+
const MIN_MEMO = metadata(path, ctx => computed(() => typeof minValue === 'number' ? minValue : minValue(ctx)));
|
|
1912
|
+
aggregateMetadata(path, MIN, ({
|
|
1913
|
+
state
|
|
1914
|
+
}) => state.metadata(MIN_MEMO)());
|
|
1915
|
+
validate(path, ctx => {
|
|
1916
|
+
if (isEmpty(ctx.value())) {
|
|
1917
|
+
return undefined;
|
|
1918
|
+
}
|
|
1919
|
+
const min = ctx.state.metadata(MIN_MEMO)();
|
|
1920
|
+
if (min === undefined || Number.isNaN(min)) {
|
|
1921
|
+
return undefined;
|
|
1922
|
+
}
|
|
1923
|
+
if (ctx.value() < min) {
|
|
1924
|
+
if (config?.error) {
|
|
1925
|
+
return getOption(config.error, ctx);
|
|
1926
|
+
} else {
|
|
1927
|
+
return minError(min, {
|
|
1928
|
+
message: getOption(config?.message, ctx)
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
return undefined;
|
|
1933
|
+
});
|
|
3019
1934
|
}
|
|
3020
1935
|
|
|
3021
|
-
/**
|
|
3022
|
-
* Binds a validator to the given path that requires the length of the value to be greater than or
|
|
3023
|
-
* equal to the given `minLength`.
|
|
3024
|
-
* This function can only be called on string or array paths.
|
|
3025
|
-
* In addition to binding a validator, this function adds `MIN_LENGTH` property to the field.
|
|
3026
|
-
*
|
|
3027
|
-
* @param path Path of the field to validate
|
|
3028
|
-
* @param minLength The minimum length, or a LogicFn that returns the minimum length.
|
|
3029
|
-
* @param config Optional, allows providing any of the following options:
|
|
3030
|
-
* - `error`: Custom validation error(s) to be used instead of the default `ValidationError.minLength(minLength)`
|
|
3031
|
-
* or a function that receives the `FieldContext` and returns custom validation error(s).
|
|
3032
|
-
* @template TValue The type of value stored in the field the logic is bound to.
|
|
3033
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
3034
|
-
*
|
|
3035
|
-
* @category validation
|
|
3036
|
-
* @experimental 21.0.0
|
|
3037
|
-
*/
|
|
3038
1936
|
function minLength(path, minLength, config) {
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
1937
|
+
const MIN_LENGTH_MEMO = metadata(path, ctx => computed(() => typeof minLength === 'number' ? minLength : minLength(ctx)));
|
|
1938
|
+
aggregateMetadata(path, MIN_LENGTH, ({
|
|
1939
|
+
state
|
|
1940
|
+
}) => state.metadata(MIN_LENGTH_MEMO)());
|
|
1941
|
+
validate(path, ctx => {
|
|
1942
|
+
if (isEmpty(ctx.value())) {
|
|
1943
|
+
return undefined;
|
|
1944
|
+
}
|
|
1945
|
+
const minLength = ctx.state.metadata(MIN_LENGTH_MEMO)();
|
|
1946
|
+
if (minLength === undefined) {
|
|
1947
|
+
return undefined;
|
|
1948
|
+
}
|
|
1949
|
+
if (getLengthOrSize(ctx.value()) < minLength) {
|
|
1950
|
+
if (config?.error) {
|
|
1951
|
+
return getOption(config.error, ctx);
|
|
1952
|
+
} else {
|
|
1953
|
+
return minLengthError(minLength, {
|
|
1954
|
+
message: getOption(config?.message, ctx)
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
return undefined;
|
|
1959
|
+
});
|
|
3059
1960
|
}
|
|
3060
1961
|
|
|
3061
|
-
/**
|
|
3062
|
-
* Binds a validator to the given path that requires the value to match a specific regex pattern.
|
|
3063
|
-
* This function can only be called on string paths.
|
|
3064
|
-
* In addition to binding a validator, this function adds `PATTERN` property to the field.
|
|
3065
|
-
*
|
|
3066
|
-
* @param path Path of the field to validate
|
|
3067
|
-
* @param pattern The RegExp pattern to match, or a LogicFn that returns the RegExp pattern.
|
|
3068
|
-
* @param config Optional, allows providing any of the following options:
|
|
3069
|
-
* - `error`: Custom validation error(s) to be used instead of the default `ValidationError.pattern(pattern)`
|
|
3070
|
-
* or a function that receives the `FieldContext` and returns custom validation error(s).
|
|
3071
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
3072
|
-
*
|
|
3073
|
-
* @category validation
|
|
3074
|
-
* @experimental 21.0.0
|
|
3075
|
-
*/
|
|
3076
1962
|
function pattern(path, pattern, config) {
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
1963
|
+
const PATTERN_MEMO = metadata(path, ctx => computed(() => pattern instanceof RegExp ? pattern : pattern(ctx)));
|
|
1964
|
+
aggregateMetadata(path, PATTERN, ({
|
|
1965
|
+
state
|
|
1966
|
+
}) => state.metadata(PATTERN_MEMO)());
|
|
1967
|
+
validate(path, ctx => {
|
|
1968
|
+
if (isEmpty(ctx.value())) {
|
|
1969
|
+
return undefined;
|
|
1970
|
+
}
|
|
1971
|
+
const pattern = ctx.state.metadata(PATTERN_MEMO)();
|
|
1972
|
+
if (pattern === undefined) {
|
|
1973
|
+
return undefined;
|
|
1974
|
+
}
|
|
1975
|
+
if (!pattern.test(ctx.value())) {
|
|
1976
|
+
if (config?.error) {
|
|
1977
|
+
return getOption(config.error, ctx);
|
|
1978
|
+
} else {
|
|
1979
|
+
return patternError(pattern, {
|
|
1980
|
+
message: getOption(config?.message, ctx)
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
return undefined;
|
|
1985
|
+
});
|
|
3097
1986
|
}
|
|
3098
1987
|
|
|
3099
|
-
/**
|
|
3100
|
-
* Binds a validator to the given path that requires the value to be non-empty.
|
|
3101
|
-
* This function can only be called on any type of path.
|
|
3102
|
-
* In addition to binding a validator, this function adds `REQUIRED` property to the field.
|
|
3103
|
-
*
|
|
3104
|
-
* @param path Path of the field to validate
|
|
3105
|
-
* @param config Optional, allows providing any of the following options:
|
|
3106
|
-
* - `message`: A user-facing message for the error.
|
|
3107
|
-
* - `error`: Custom validation error(s) to be used instead of the default `ValidationError.required()`
|
|
3108
|
-
* or a function that receives the `FieldContext` and returns custom validation error(s).
|
|
3109
|
-
* - `when`: A function that receives the `FieldContext` and returns true if the field is required
|
|
3110
|
-
* @template TValue The type of value stored in the field the logic is bound to.
|
|
3111
|
-
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
|
|
3112
|
-
*
|
|
3113
|
-
* @category validation
|
|
3114
|
-
* @experimental 21.0.0
|
|
3115
|
-
*/
|
|
3116
1988
|
function required(path, config) {
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
1989
|
+
const REQUIRED_MEMO = metadata(path, ctx => computed(() => config?.when ? config.when(ctx) : true));
|
|
1990
|
+
aggregateMetadata(path, REQUIRED, ({
|
|
1991
|
+
state
|
|
1992
|
+
}) => state.metadata(REQUIRED_MEMO)());
|
|
1993
|
+
validate(path, ctx => {
|
|
1994
|
+
if (ctx.state.metadata(REQUIRED_MEMO)() && isEmpty(ctx.value())) {
|
|
1995
|
+
if (config?.error) {
|
|
1996
|
+
return getOption(config.error, ctx);
|
|
1997
|
+
} else {
|
|
1998
|
+
return requiredError({
|
|
1999
|
+
message: getOption(config?.message, ctx)
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
return undefined;
|
|
2004
|
+
});
|
|
3130
2005
|
}
|
|
3131
2006
|
|
|
3132
|
-
/**
|
|
3133
|
-
* Validates a field using a `StandardSchemaV1` compatible validator (e.g. a Zod validator).
|
|
3134
|
-
*
|
|
3135
|
-
* See https://github.com/standard-schema/standard-schema for more about standard schema.
|
|
3136
|
-
*
|
|
3137
|
-
* @param path The `FieldPath` to the field to validate.
|
|
3138
|
-
* @param schema The standard schema compatible validator to use for validation.
|
|
3139
|
-
* @template TSchema The type validated by the schema. This may be either the full `TValue` type,
|
|
3140
|
-
* or a partial of it.
|
|
3141
|
-
* @template TValue The type of value stored in the field being validated.
|
|
3142
|
-
*
|
|
3143
|
-
* @category validation
|
|
3144
|
-
* @experimental 21.0.0
|
|
3145
|
-
*/
|
|
3146
2007
|
function validateStandardSchema(path, schema) {
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
2008
|
+
const VALIDATOR_MEMO = metadata(path, ({
|
|
2009
|
+
value
|
|
2010
|
+
}) => {
|
|
2011
|
+
return computed(() => schema['~standard'].validate(value()));
|
|
2012
|
+
});
|
|
2013
|
+
validateTree(path, ({
|
|
2014
|
+
state,
|
|
2015
|
+
fieldOf
|
|
2016
|
+
}) => {
|
|
2017
|
+
const result = state.metadata(VALIDATOR_MEMO)();
|
|
2018
|
+
if (_isPromise(result)) {
|
|
2019
|
+
return [];
|
|
2020
|
+
}
|
|
2021
|
+
return result.issues?.map(issue => standardIssueToFormTreeError(fieldOf(path), issue)) ?? [];
|
|
2022
|
+
});
|
|
2023
|
+
validateAsync(path, {
|
|
2024
|
+
params: ({
|
|
2025
|
+
state
|
|
2026
|
+
}) => {
|
|
2027
|
+
const result = state.metadata(VALIDATOR_MEMO)();
|
|
2028
|
+
return _isPromise(result) ? result : undefined;
|
|
2029
|
+
},
|
|
2030
|
+
factory: params => {
|
|
2031
|
+
return resource({
|
|
2032
|
+
params,
|
|
2033
|
+
loader: async ({
|
|
2034
|
+
params
|
|
2035
|
+
}) => (await params)?.issues ?? []
|
|
2036
|
+
});
|
|
2037
|
+
},
|
|
2038
|
+
onSuccess: (issues, {
|
|
2039
|
+
fieldOf
|
|
2040
|
+
}) => {
|
|
2041
|
+
return issues.map(issue => standardIssueToFormTreeError(fieldOf(path), issue));
|
|
2042
|
+
},
|
|
2043
|
+
onError: () => {}
|
|
2044
|
+
});
|
|
3179
2045
|
}
|
|
3180
|
-
/**
|
|
3181
|
-
* Converts a `StandardSchemaV1.Issue` to a `FormTreeError`.
|
|
3182
|
-
*
|
|
3183
|
-
* @param field The root field to which the issue's path is relative.
|
|
3184
|
-
* @param issue The `StandardSchemaV1.Issue` to convert.
|
|
3185
|
-
* @returns A `ValidationError` representing the issue.
|
|
3186
|
-
*/
|
|
3187
2046
|
function standardIssueToFormTreeError(field, issue) {
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
2047
|
+
let target = field;
|
|
2048
|
+
for (const pathPart of issue.path ?? []) {
|
|
2049
|
+
const pathKey = typeof pathPart === 'object' ? pathPart.key : pathPart;
|
|
2050
|
+
target = target[pathKey];
|
|
2051
|
+
}
|
|
2052
|
+
return addDefaultField(standardSchemaError(issue), target);
|
|
3194
2053
|
}
|
|
3195
2054
|
|
|
3196
|
-
export {
|
|
2055
|
+
export { AggregateMetadataKey, CustomValidationError, EmailValidationError, FIELD, Field, MAX, MAX_LENGTH, MIN, MIN_LENGTH, MaxLengthValidationError, MaxValidationError, MetadataKey, MinLengthValidationError, MinValidationError, NgValidationError, PATTERN, PatternValidationError, REQUIRED, RequiredValidationError, StandardSchemaValidationError, aggregateMetadata, andMetadataKey, apply, applyEach, applyWhen, applyWhenValue, createMetadataKey, customError, disabled, email, emailError, form, hidden, listMetadataKey, max, maxError, maxLength, maxLengthError, maxMetadataKey, metadata, min, minError, minLength, minLengthError, minMetadataKey, orMetadataKey, pattern, patternError, readonly, reducedMetadataKey, required, requiredError, schema, standardSchemaError, submit, validate, validateAsync, validateHttp, validateStandardSchema, validateTree };
|
|
3197
2056
|
//# sourceMappingURL=signals.mjs.map
|