@ng-forge/dynamic-forms 0.9.0-next.1 → 0.9.0-next.11
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/{ng-forge-dynamic-forms-array-field.component-CWxCWVdz.mjs → ng-forge-dynamic-forms-array-field.component-DJ-LiLm_.mjs} +17 -4
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-DJ-LiLm_.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-PozYj2JC.mjs → ng-forge-dynamic-forms-container-field.component-Pcxg5PQu.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-PozYj2JC.mjs.map → ng-forge-dynamic-forms-container-field.component-Pcxg5PQu.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-DW_P7w-b.mjs → ng-forge-dynamic-forms-container-utils-a4RstR2Z.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-DW_P7w-b.mjs.map → ng-forge-dynamic-forms-container-utils-a4RstR2Z.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-DrtHSaVV.mjs → ng-forge-dynamic-forms-css-wrapper.component-CJACCIEK.mjs} +2 -2
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-CJACCIEK.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-CSx0gwFc.mjs → ng-forge-dynamic-forms-emit-initialization-Bjf_I0D_.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-CSx0gwFc.mjs.map → ng-forge-dynamic-forms-emit-initialization-Bjf_I0D_.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-2yRmHAUp.mjs → ng-forge-dynamic-forms-group-field.component-fzKwPrQr.mjs} +24 -4
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-fzKwPrQr.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +629 -54
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-Ll4W2Z_n.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-o3rJX4vG.mjs} +979 -180
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-o3rJX4vG.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-B_CxX0T-.mjs → ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-B_CxX0T-.mjs.map → ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-rJMtKX2X.mjs → ng-forge-dynamic-forms-text-field.component-B9SK8Vun.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-rJMtKX2X.mjs.map → ng-forge-dynamic-forms-text-field.component-B9SK8Vun.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +1 -1
- package/types/ng-forge-dynamic-forms-integration.d.ts +380 -8
- package/types/ng-forge-dynamic-forms.d.ts +1905 -1588
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-CWxCWVdz.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-DrtHSaVV.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-2yRmHAUp.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-Ll4W2Z_n.mjs.map +0 -1
|
@@ -19,462 +19,828 @@ type Prettify<T> = {
|
|
|
19
19
|
type DynamicText = string | Observable<string> | Signal<string>;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* All string values in `queryParams` are treated as expressions evaluated by ExpressionParser.
|
|
25
|
-
* When `evaluateBodyExpressions` is true, top-level string values in `body` are also evaluated as expressions (shallow only).
|
|
22
|
+
* Interface for dynamic forms logger implementations.
|
|
23
|
+
* Allows custom logging integrations (Sentry, DataDog, etc.)
|
|
26
24
|
*/
|
|
27
|
-
interface
|
|
28
|
-
/** URL to send the request to */
|
|
29
|
-
url: string;
|
|
30
|
-
/** HTTP method. Defaults to 'GET' */
|
|
31
|
-
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
25
|
+
interface Logger {
|
|
32
26
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* ```typescript
|
|
38
|
-
* {
|
|
39
|
-
* url: '/api/users/:userId/orders/:orderId',
|
|
40
|
-
* params: { userId: 'formValue.userId', orderId: 'fieldValue' }
|
|
41
|
-
* }
|
|
42
|
-
* // With userId=42 and fieldValue='abc' → '/api/users/42/orders/abc'
|
|
43
|
-
* ```
|
|
27
|
+
* Log a debug message.
|
|
28
|
+
* Use for detailed diagnostic information during development.
|
|
44
29
|
*/
|
|
45
|
-
|
|
30
|
+
debug(message: string, ...args: unknown[]): void;
|
|
46
31
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
32
|
+
* Log an info message.
|
|
33
|
+
* Use for general operational information.
|
|
49
34
|
*/
|
|
50
|
-
|
|
51
|
-
/** Request body (for POST/PUT/PATCH) */
|
|
52
|
-
body?: Record<string, unknown>;
|
|
35
|
+
info(message: string, ...args: unknown[]): void;
|
|
53
36
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
37
|
+
* Log a warning message.
|
|
38
|
+
* Use for potentially problematic situations that don't prevent operation.
|
|
56
39
|
*/
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
|
|
40
|
+
warn(message: string, ...args: unknown[]): void;
|
|
41
|
+
/**
|
|
42
|
+
* Log an error message.
|
|
43
|
+
* Use for error conditions that may affect functionality.
|
|
44
|
+
*/
|
|
45
|
+
error(message: string, ...args: unknown[]): void;
|
|
60
46
|
}
|
|
61
47
|
|
|
62
48
|
/**
|
|
63
|
-
*
|
|
49
|
+
* Shared shape for DI-scoped warning trackers that dedupe log output.
|
|
64
50
|
*
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Condition that compares a specific field's value against an expected value.
|
|
51
|
+
* Used by both the derivation-warning tracker (keys are field paths) and the
|
|
52
|
+
* deprecation-warning tracker (keys are deprecation IDs). The property is
|
|
53
|
+
* generic so the same shape works for any string-keyed warning domain.
|
|
70
54
|
*
|
|
71
|
-
* @
|
|
55
|
+
* @internal
|
|
72
56
|
*/
|
|
73
|
-
interface
|
|
74
|
-
|
|
75
|
-
/** Path to the field whose value to compare */
|
|
76
|
-
fieldPath: string;
|
|
77
|
-
/** Comparison operator */
|
|
78
|
-
operator: ComparisonOperator;
|
|
79
|
-
/** Value to compare against */
|
|
80
|
-
value?: unknown;
|
|
57
|
+
interface WarningTracker {
|
|
58
|
+
warnedKeys: Set<string>;
|
|
81
59
|
}
|
|
60
|
+
|
|
82
61
|
/**
|
|
83
|
-
*
|
|
62
|
+
* Represents the state information of a single form field.
|
|
84
63
|
*
|
|
85
|
-
*
|
|
64
|
+
* Used in evaluation contexts to allow expressions and conditions
|
|
65
|
+
* to reference field state (e.g., `fieldState.touched`, `formFieldState.email.dirty`).
|
|
86
66
|
*
|
|
87
67
|
* @public
|
|
88
68
|
*/
|
|
89
|
-
interface
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
69
|
+
interface FieldStateInfo {
|
|
70
|
+
readonly touched: boolean;
|
|
71
|
+
readonly dirty: boolean;
|
|
72
|
+
/** Convenience property: equivalent to `!dirty` */
|
|
73
|
+
readonly pristine: boolean;
|
|
74
|
+
readonly valid: boolean;
|
|
75
|
+
readonly invalid: boolean;
|
|
76
|
+
readonly pending: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Whether the field is currently hidden.
|
|
79
|
+
*
|
|
80
|
+
* Note: This is a library-managed property, not a native Angular Signal Forms
|
|
81
|
+
* property. Returns `false` if the field instance does not expose a `hidden` signal.
|
|
82
|
+
*/
|
|
83
|
+
readonly hidden: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the field is currently readonly.
|
|
86
|
+
*
|
|
87
|
+
* Note: This is a library-managed property, not a native Angular Signal Forms
|
|
88
|
+
* property. Returns `false` if the field instance does not expose a `readonly` signal.
|
|
89
|
+
*/
|
|
90
|
+
readonly readonly: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Whether the field is currently disabled.
|
|
93
|
+
*
|
|
94
|
+
* Note: This is a library-managed property, not a native Angular Signal Forms
|
|
95
|
+
* property. Returns `false` if the field instance does not expose a `disabled` signal.
|
|
96
|
+
*/
|
|
97
|
+
readonly disabled: boolean;
|
|
93
98
|
}
|
|
94
99
|
/**
|
|
95
|
-
*
|
|
100
|
+
* Map of field keys to their state information.
|
|
96
101
|
*
|
|
97
|
-
*
|
|
102
|
+
* Used as `formFieldState` in evaluation contexts to access
|
|
103
|
+
* state of any field in the form by key.
|
|
98
104
|
*
|
|
99
105
|
* @public
|
|
100
106
|
*/
|
|
101
|
-
|
|
102
|
-
type: 'javascript';
|
|
103
|
-
/** JavaScript expression string to evaluate */
|
|
104
|
-
expression: string;
|
|
105
|
-
}
|
|
107
|
+
type FormFieldStateMap = Record<string, FieldStateInfo | undefined>;
|
|
106
108
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* The HTTP request is resolved reactively — when dependent form values change,
|
|
110
|
-
* the request is re-evaluated (with debouncing). The result is cached per
|
|
111
|
-
* resolved request to avoid redundant network calls.
|
|
109
|
+
* Field state context for the current field being evaluated.
|
|
112
110
|
*
|
|
113
|
-
*
|
|
114
|
-
* a signal-based async resolution pattern internally.
|
|
111
|
+
* Used as `fieldState` in evaluation contexts.
|
|
115
112
|
*
|
|
116
113
|
* @public
|
|
117
114
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
type FieldStateContext = FieldStateInfo;
|
|
116
|
+
|
|
117
|
+
interface EvaluationContext<TValue = unknown, TFormValue extends Record<string, unknown> = any> {
|
|
118
|
+
/** Current field value */
|
|
119
|
+
fieldValue: TValue;
|
|
122
120
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
121
|
+
* Form value for the current evaluation scope.
|
|
122
|
+
*
|
|
123
|
+
* For regular (non-array) derivations, this contains the complete form value.
|
|
124
|
+
* For array item derivations, this is scoped to the current array item.
|
|
125
|
+
* Use `rootFormValue` to access the complete form when inside an array context.
|
|
126
126
|
*/
|
|
127
|
-
|
|
127
|
+
formValue: TFormValue;
|
|
128
|
+
/** Field path for relative references */
|
|
129
|
+
fieldPath: string;
|
|
130
|
+
/** Custom evaluation functions */
|
|
131
|
+
customFunctions?: Record<string, (context: EvaluationContext) => unknown>;
|
|
132
|
+
/** Logger for diagnostic output */
|
|
133
|
+
logger: Logger;
|
|
128
134
|
/**
|
|
129
|
-
* Value
|
|
135
|
+
* Value of the field's nearest parent **group** (or array item, when the
|
|
136
|
+
* field has no inner group above the array boundary).
|
|
130
137
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
138
|
+
* Complements `formValue` and `fieldValue` for derivations on fields nested
|
|
139
|
+
* inside groups built by factory helpers, where the parent group's key
|
|
140
|
+
* isn't known at config-authoring time. Mirrors the runtime semantics of
|
|
141
|
+
* the `'$group'` token used in `dependsOn`.
|
|
135
142
|
*
|
|
136
|
-
*
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
*
|
|
143
|
-
*
|
|
143
|
+
* Resolution at evaluation time:
|
|
144
|
+
* - Inside a group at form root: the parent group's value object (e.g.,
|
|
145
|
+
* for `address.state`, `groupValue === formValue.address`).
|
|
146
|
+
* - Inside nested groups: the innermost parent group's value object.
|
|
147
|
+
* - Directly inside an array item (no inner group): the array item itself
|
|
148
|
+
* (same shape as `formValue` in array context).
|
|
149
|
+
* - Inside a group inside an array item: the inner group's value object
|
|
150
|
+
* within the current item.
|
|
151
|
+
* - Field at form root with no parent container: `undefined`.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* // Field 'state' inside group 'address':
|
|
156
|
+
* deriveStateFromCountry: (ctx) => {
|
|
157
|
+
* const country = ctx.groupValue?.country; // no need to know parent key
|
|
158
|
+
* return country === 'usa' ? 'NY' : '';
|
|
159
|
+
* }
|
|
160
|
+
* ```
|
|
144
161
|
*/
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Condition that evaluates based on a registered async function.
|
|
149
|
-
*
|
|
150
|
-
* The function is resolved reactively — when dependent form values change,
|
|
151
|
-
* the function is re-evaluated (with debouncing). The result is cached per
|
|
152
|
-
* evaluation to avoid redundant calls.
|
|
153
|
-
*
|
|
154
|
-
* Since `LogicFn` must return `boolean` synchronously, this condition uses
|
|
155
|
-
* a signal-based async resolution pattern internally.
|
|
156
|
-
*
|
|
157
|
-
* @public
|
|
158
|
-
*/
|
|
159
|
-
interface AsyncCondition {
|
|
160
|
-
type: 'async';
|
|
161
|
-
/** Name of the registered async condition function */
|
|
162
|
-
asyncFunctionName: string;
|
|
162
|
+
groupValue?: unknown;
|
|
163
163
|
/**
|
|
164
|
-
*
|
|
164
|
+
* Root form value when inside an array context.
|
|
165
165
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
166
|
+
* This provides access to values outside the current array item.
|
|
167
|
+
* When a derivation targets an array item field (e.g., `items.$.lineTotal`),
|
|
168
|
+
* `formValue` is scoped to the current array item, while `rootFormValue`
|
|
169
|
+
* provides access to the entire form value including fields outside the array.
|
|
170
170
|
*
|
|
171
|
-
* @
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* // In an array item derivation (on the lineTotal field inside lineItems array):
|
|
174
|
+
* {
|
|
175
|
+
* key: 'lineTotal',
|
|
176
|
+
* type: 'input',
|
|
177
|
+
* // formValue = current array item { quantity: 2, unitPrice: 50 }
|
|
178
|
+
* // rootFormValue = entire form { globalDiscount: 0.1, lineItems: [...] }
|
|
179
|
+
* derivation: 'formValue.quantity * formValue.unitPrice * (1 - rootFormValue.globalDiscount)'
|
|
180
|
+
* }
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* For non-array derivations, `rootFormValue` is not set and `formValue`
|
|
184
|
+
* contains the entire form value.
|
|
172
185
|
*/
|
|
173
|
-
|
|
186
|
+
rootFormValue?: Record<string, unknown>;
|
|
174
187
|
/**
|
|
175
|
-
*
|
|
176
|
-
* @default 300
|
|
188
|
+
* Current array index when inside an array derivation.
|
|
177
189
|
*/
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Logical AND — all sub-conditions must be true.
|
|
182
|
-
*
|
|
183
|
-
* @public
|
|
184
|
-
*/
|
|
185
|
-
interface AndCondition {
|
|
186
|
-
type: 'and';
|
|
187
|
-
/** Sub-conditions that must all evaluate to true */
|
|
188
|
-
conditions: ConditionalExpression[];
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Logical OR — at least one sub-condition must be true.
|
|
192
|
-
*
|
|
193
|
-
* @public
|
|
194
|
-
*/
|
|
195
|
-
interface OrCondition {
|
|
196
|
-
type: 'or';
|
|
197
|
-
/** Sub-conditions where at least one must evaluate to true */
|
|
198
|
-
conditions: ConditionalExpression[];
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Discriminated union of all conditional expression types.
|
|
202
|
-
*
|
|
203
|
-
* Each variant only allows the properties relevant to its type,
|
|
204
|
-
* providing compile-time safety against invalid property combinations.
|
|
205
|
-
*
|
|
206
|
-
* @public
|
|
207
|
-
*/
|
|
208
|
-
type ConditionalExpression = FieldValueCondition | CustomCondition | JavascriptCondition | HttpCondition | AsyncCondition | AndCondition | OrCondition;
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Logic configuration for container fields (page, group, row, array).
|
|
212
|
-
* Containers only support the 'hidden' logic type since they are layout containers,
|
|
213
|
-
* not form controls (they can't be readonly, disabled, or required).
|
|
214
|
-
*/
|
|
215
|
-
interface ContainerLogicConfig {
|
|
216
|
-
/** Only 'hidden' is supported for container fields */
|
|
217
|
-
type: 'hidden';
|
|
190
|
+
arrayIndex?: number;
|
|
218
191
|
/**
|
|
219
|
-
*
|
|
220
|
-
* Can be a boolean or a conditional expression evaluated against form values.
|
|
192
|
+
* Path to the array field when inside an array derivation.
|
|
221
193
|
*/
|
|
222
|
-
|
|
194
|
+
arrayPath?: string;
|
|
195
|
+
/**
|
|
196
|
+
* External data signals resolved to their current values.
|
|
197
|
+
*
|
|
198
|
+
* This allows forms to reference data from outside the form context
|
|
199
|
+
* in conditional logic, derivations, and other expressions.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```typescript
|
|
203
|
+
* // In form config:
|
|
204
|
+
* externalData: {
|
|
205
|
+
* userRole: computed(() => this.userService.currentRole()),
|
|
206
|
+
* permissions: computed(() => this.authService.permissions()),
|
|
207
|
+
* }
|
|
208
|
+
*
|
|
209
|
+
* // In JavaScript expression:
|
|
210
|
+
* condition: {
|
|
211
|
+
* type: 'javascript',
|
|
212
|
+
* expression: "externalData.userRole === 'admin'"
|
|
213
|
+
* }
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
externalData?: Record<string, unknown>;
|
|
217
|
+
/**
|
|
218
|
+
* DI-scoped tracker for deprecation warnings.
|
|
219
|
+
* Used by the condition evaluator to deduplicate deprecation warnings.
|
|
220
|
+
*/
|
|
221
|
+
deprecationTracker?: WarningTracker;
|
|
222
|
+
/**
|
|
223
|
+
* State of the current field being evaluated.
|
|
224
|
+
*
|
|
225
|
+
* Provides access to field state properties like `touched`, `dirty`, `valid`, etc.
|
|
226
|
+
* Uses a Proxy for lazy evaluation — properties are only read when accessed.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* // In a JavaScript expression:
|
|
231
|
+
* condition: {
|
|
232
|
+
* type: 'javascript',
|
|
233
|
+
* expression: "fieldState.touched && fieldState.dirty"
|
|
234
|
+
* }
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
fieldState?: FieldStateContext;
|
|
238
|
+
/**
|
|
239
|
+
* State of all fields in the form, keyed by field key.
|
|
240
|
+
*
|
|
241
|
+
* Provides access to any field's state properties by key.
|
|
242
|
+
* Uses a Proxy for lazy evaluation — field state is only read when accessed.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```typescript
|
|
246
|
+
* // In a JavaScript expression:
|
|
247
|
+
* condition: {
|
|
248
|
+
* type: 'javascript',
|
|
249
|
+
* expression: "formFieldState.email.dirty && formFieldState.email.valid"
|
|
250
|
+
* }
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
formFieldState?: FormFieldStateMap;
|
|
254
|
+
/** Allow additional properties for flexible expression evaluation */
|
|
255
|
+
[key: string]: unknown;
|
|
223
256
|
}
|
|
224
257
|
|
|
225
258
|
/**
|
|
226
|
-
*
|
|
227
|
-
* Used for object arrays where each item is `{ key1: value1, key2: value2, ... }`.
|
|
228
|
-
*/
|
|
229
|
-
type ArrayItemTemplate = readonly ArrayAllowedChildren[];
|
|
230
|
-
/**
|
|
231
|
-
* An array item definition can be either:
|
|
232
|
-
* - A single field (ArrayAllowedChildren) for PRIMITIVE array items - extracts field value directly
|
|
233
|
-
* - An array of fields (ArrayItemTemplate) for OBJECT array items - merges fields into object
|
|
259
|
+
* Custom function signature for conditional expressions
|
|
234
260
|
*
|
|
235
|
-
*
|
|
236
|
-
* -
|
|
237
|
-
* -
|
|
238
|
-
* -
|
|
239
|
-
|
|
240
|
-
type ArrayItemDefinition = ArrayAllowedChildren | ArrayItemTemplate;
|
|
241
|
-
/**
|
|
242
|
-
* Array field interface for creating dynamic field collections that map to array values.
|
|
261
|
+
* Custom functions are used for conditional logic in:
|
|
262
|
+
* - `when` conditions (field visibility, conditional validation)
|
|
263
|
+
* - `readonly` logic (dynamic readonly state)
|
|
264
|
+
* - `disabled` logic (dynamic disabled state)
|
|
265
|
+
* - Dynamic value calculations
|
|
243
266
|
*
|
|
244
|
-
*
|
|
245
|
-
* -
|
|
246
|
-
* -
|
|
247
|
-
*
|
|
248
|
-
* - An array of FieldDefs (object item) - fields are merged into an object
|
|
249
|
-
* - Items can have different field configurations (heterogeneous arrays supported)
|
|
250
|
-
* - Empty `fields: []` means no initial items - user adds via buttons with explicit templates
|
|
251
|
-
* - Initial values are declared via `value` property on each field definition
|
|
267
|
+
* Unlike validators, custom functions:
|
|
268
|
+
* - Return any value (typically boolean for conditions, but could be strings, numbers, etc.)
|
|
269
|
+
* - Do NOT return ValidationError objects
|
|
270
|
+
* - Receive EvaluationContext (field values) not FieldContext (field state)
|
|
252
271
|
*
|
|
253
272
|
* @example
|
|
254
|
-
*
|
|
273
|
+
* ```typescript
|
|
274
|
+
* // Boolean condition function
|
|
275
|
+
* const isAdult: CustomFunction = (ctx) => {
|
|
276
|
+
* return ctx.age >= 18;
|
|
277
|
+
* };
|
|
278
|
+
*
|
|
279
|
+
* // Used in field configuration:
|
|
255
280
|
* {
|
|
256
|
-
* key: '
|
|
257
|
-
* type: '
|
|
258
|
-
*
|
|
259
|
-
* { key: 'tag', type: 'input', value: 'angular' }, // Single field = primitive
|
|
260
|
-
* { key: 'tag', type: 'input', value: 'typescript' },
|
|
261
|
-
* ]
|
|
281
|
+
* key: 'alcoholPreference',
|
|
282
|
+
* type: 'select',
|
|
283
|
+
* when: { function: 'isAdult' } // Only show if isAdult returns true
|
|
262
284
|
* }
|
|
285
|
+
* ```
|
|
263
286
|
*
|
|
264
287
|
* @example
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
288
|
+
* ```typescript
|
|
289
|
+
* // Calculation function
|
|
290
|
+
* const calculateDiscount: CustomFunction = (ctx) => {
|
|
291
|
+
* const price = ctx.price || 0;
|
|
292
|
+
* const isVip = ctx.isVip || false;
|
|
293
|
+
* return isVip ? price * 0.8 : price * 0.9;
|
|
294
|
+
* };
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
type CustomFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => unknown;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Async function for derivations — returns any value.
|
|
301
|
+
*
|
|
302
|
+
* The function receives the evaluation context and may return a Promise or Observable.
|
|
303
|
+
* Register functions in `customFnConfig.asyncDerivations`.
|
|
276
304
|
*
|
|
277
305
|
* @example
|
|
278
|
-
*
|
|
279
|
-
* {
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
306
|
+
* ```typescript
|
|
307
|
+
* const fetchPrice: AsyncDerivationFunction = async (context) => {
|
|
308
|
+
* const response = await fetch(`/api/price?product=${context.formValue.productId}`);
|
|
309
|
+
* const data = await response.json();
|
|
310
|
+
* return data.price;
|
|
311
|
+
* };
|
|
312
|
+
* ```
|
|
313
|
+
*
|
|
314
|
+
* @public
|
|
315
|
+
*/
|
|
316
|
+
type AsyncDerivationFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => Promise<unknown> | Observable<unknown>;
|
|
317
|
+
/**
|
|
318
|
+
* Async function for conditions — returns boolean.
|
|
319
|
+
*
|
|
320
|
+
* The function receives the evaluation context and may return a Promise or Observable.
|
|
321
|
+
* Register functions in `customFnConfig.asyncConditions`.
|
|
287
322
|
*
|
|
288
323
|
* @example
|
|
289
|
-
*
|
|
290
|
-
* {
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* }
|
|
324
|
+
* ```typescript
|
|
325
|
+
* const checkPermission: AsyncConditionFunction = async (context) => {
|
|
326
|
+
* const response = await fetch(`/api/permissions?user=${context.formValue.userId}`);
|
|
327
|
+
* const data = await response.json();
|
|
328
|
+
* return data.canEdit;
|
|
329
|
+
* };
|
|
330
|
+
* ```
|
|
295
331
|
*
|
|
296
|
-
*
|
|
297
|
-
|
|
332
|
+
* @public
|
|
333
|
+
*/
|
|
334
|
+
type AsyncConditionFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => Promise<boolean> | Observable<boolean>;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Configuration for an HTTP request used by declarative HTTP validators, derivations, and conditions.
|
|
298
338
|
*
|
|
299
|
-
*
|
|
339
|
+
* All string values in `queryParams` are treated as expressions evaluated by ExpressionParser.
|
|
340
|
+
* When `evaluateBodyExpressions` is true, top-level string values in `body` are also evaluated as expressions (shallow only).
|
|
300
341
|
*/
|
|
301
|
-
interface
|
|
302
|
-
|
|
342
|
+
interface HttpRequestConfig {
|
|
343
|
+
/** URL to send the request to */
|
|
344
|
+
url: string;
|
|
345
|
+
/** HTTP method. Defaults to 'GET' */
|
|
346
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
303
347
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
348
|
+
* URL path parameters. Keys correspond to `:key` placeholders in the URL.
|
|
349
|
+
* Values are expressions evaluated by ExpressionParser against the current EvaluationContext.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```typescript
|
|
353
|
+
* {
|
|
354
|
+
* url: '/api/users/:userId/orders/:orderId',
|
|
355
|
+
* params: { userId: 'formValue.userId', orderId: 'fieldValue' }
|
|
356
|
+
* }
|
|
357
|
+
* // With userId=42 and fieldValue='abc' → '/api/users/42/orders/abc'
|
|
358
|
+
* ```
|
|
310
359
|
*/
|
|
311
|
-
|
|
312
|
-
/** Array fields do not have a label property **/
|
|
313
|
-
readonly label?: never;
|
|
314
|
-
/** Arrays do not support meta - they have no native form element **/
|
|
315
|
-
readonly meta?: never;
|
|
360
|
+
params?: Record<string, string>;
|
|
316
361
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
362
|
+
* Query parameters appended to the URL.
|
|
363
|
+
* Values are expressions evaluated by ExpressionParser against the current EvaluationContext.
|
|
319
364
|
*/
|
|
320
|
-
|
|
365
|
+
queryParams?: Record<string, string>;
|
|
366
|
+
/** Request body (for POST/PUT/PATCH) */
|
|
367
|
+
body?: Record<string, unknown>;
|
|
321
368
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
369
|
+
* When true, top-level string values in `body` are treated as expressions
|
|
370
|
+
* and evaluated by ExpressionParser (shallow only — nested objects are passed through as-is).
|
|
324
371
|
*/
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
|
|
328
|
-
* Validation fails if the array has more items than this value.
|
|
329
|
-
*/
|
|
330
|
-
readonly maxLength?: number;
|
|
372
|
+
evaluateBodyExpressions?: boolean;
|
|
373
|
+
/** HTTP headers to include in the request */
|
|
374
|
+
headers?: Record<string, string>;
|
|
331
375
|
}
|
|
376
|
+
|
|
332
377
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
378
|
+
* Comparison operators for field value and form value conditions.
|
|
379
|
+
*
|
|
380
|
+
* @public
|
|
336
381
|
*/
|
|
337
|
-
|
|
382
|
+
type ComparisonOperator = 'equals' | 'notEquals' | 'greater' | 'less' | 'greaterOrEqual' | 'lessOrEqual' | 'contains' | 'startsWith' | 'endsWith' | 'matches';
|
|
338
383
|
/**
|
|
339
|
-
*
|
|
384
|
+
* Condition that compares a specific field's value against an expected value.
|
|
385
|
+
*
|
|
386
|
+
* @public
|
|
340
387
|
*/
|
|
341
|
-
interface
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
388
|
+
interface FieldValueCondition {
|
|
389
|
+
type: 'fieldValue';
|
|
390
|
+
/** Path to the field whose value to compare */
|
|
391
|
+
fieldPath: string;
|
|
392
|
+
/** Comparison operator */
|
|
393
|
+
operator: ComparisonOperator;
|
|
394
|
+
/** Value to compare against */
|
|
395
|
+
value?: unknown;
|
|
346
396
|
}
|
|
347
397
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* Instead of manually specifying each item in `fields`, provide a `template` that defines
|
|
351
|
-
* the structure of a single item, and a `value` array with initial data.
|
|
352
|
-
* The library auto-generates add/remove buttons.
|
|
398
|
+
* Condition that invokes a custom function.
|
|
353
399
|
*
|
|
354
|
-
*
|
|
400
|
+
* Two mutually exclusive forms:
|
|
401
|
+
* - `functionName`: name of a function registered in `customFnConfig.customFunctions`.
|
|
402
|
+
* JSON-serializable; suitable for configs loaded from APIs, databases, or OpenAPI.
|
|
403
|
+
* - `fn`: inline function. NOT JSON-serializable; for code-only configs.
|
|
355
404
|
*
|
|
356
|
-
*
|
|
357
|
-
* // Primitive array: ['angular', 'typescript']
|
|
358
|
-
* {
|
|
359
|
-
* key: 'tags',
|
|
360
|
-
* type: 'array',
|
|
361
|
-
* template: { key: 'value', type: 'input', label: 'Tag', required: true },
|
|
362
|
-
* value: ['angular', 'typescript']
|
|
363
|
-
* }
|
|
405
|
+
* Exactly one of `functionName` or `fn` must be set.
|
|
364
406
|
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* type: 'array',
|
|
370
|
-
* template: [
|
|
371
|
-
* { key: 'name', type: 'input', label: 'Contact Name', required: true },
|
|
372
|
-
* { key: 'phone', type: 'input', label: 'Phone Number' },
|
|
373
|
-
* ],
|
|
374
|
-
* value: [{ name: 'Jane', phone: '555' }]
|
|
375
|
-
* }
|
|
407
|
+
* Encoded as a strict discriminated union (XOR via `?: never`). `CustomValidatorConfig`
|
|
408
|
+
* intentionally uses a permissive interface for the same `fn`/`functionName` split
|
|
409
|
+
* because validators have a third source (`expression`) and the historical interface
|
|
410
|
+
* was already runtime-checked — the asymmetry is by design, not an oversight.
|
|
376
411
|
*
|
|
377
|
-
* @
|
|
378
|
-
* // Button customization / opt-out
|
|
379
|
-
* {
|
|
380
|
-
* key: 'tags',
|
|
381
|
-
* type: 'array',
|
|
382
|
-
* template: { key: 'value', type: 'input', label: 'Tag' },
|
|
383
|
-
* addButton: { label: 'Add Tag', props: { color: 'primary' } },
|
|
384
|
-
* removeButton: false
|
|
385
|
-
* }
|
|
412
|
+
* @public
|
|
386
413
|
*/
|
|
387
|
-
|
|
388
|
-
type: '
|
|
414
|
+
type CustomCondition = {
|
|
415
|
+
type: 'custom';
|
|
416
|
+
/** Name of the registered custom function to invoke */
|
|
417
|
+
functionName: string;
|
|
418
|
+
/** Inline form is forbidden when `functionName` is set */
|
|
419
|
+
fn?: never;
|
|
420
|
+
} | {
|
|
421
|
+
type: 'custom';
|
|
389
422
|
/**
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* -
|
|
423
|
+
* Inline custom function. Mutually exclusive with `functionName`.
|
|
424
|
+
*
|
|
425
|
+
* NOT JSON-serializable — for code-only configs. For configs loaded
|
|
426
|
+
* from JSON / OpenAPI / databases, use `functionName` to reference
|
|
427
|
+
* a function registered in `customFnConfig.customFunctions`.
|
|
393
428
|
*/
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
|
|
429
|
+
fn: CustomFunction;
|
|
430
|
+
/** Registered form is forbidden when `fn` is set */
|
|
431
|
+
functionName?: never;
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* Condition that evaluates a JavaScript expression using the secure AST-based parser.
|
|
435
|
+
*
|
|
436
|
+
* The expression has access to `formValue`, `fieldValue`, `externalData`, etc.
|
|
437
|
+
*
|
|
438
|
+
* @public
|
|
439
|
+
*/
|
|
440
|
+
interface JavascriptCondition {
|
|
441
|
+
type: 'javascript';
|
|
442
|
+
/** JavaScript expression string to evaluate */
|
|
443
|
+
expression: string;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Condition that evaluates based on an HTTP response from a remote server.
|
|
447
|
+
*
|
|
448
|
+
* The HTTP request is resolved reactively — when dependent form values change,
|
|
449
|
+
* the request is re-evaluated (with debouncing). The result is cached per
|
|
450
|
+
* resolved request to avoid redundant network calls.
|
|
451
|
+
*
|
|
452
|
+
* Since `LogicFn` must return `boolean` synchronously, this condition uses
|
|
453
|
+
* a signal-based async resolution pattern internally.
|
|
454
|
+
*
|
|
455
|
+
* @public
|
|
456
|
+
*/
|
|
457
|
+
interface HttpCondition {
|
|
458
|
+
type: 'http';
|
|
459
|
+
/** HTTP request configuration */
|
|
460
|
+
http: HttpRequestConfig;
|
|
397
461
|
/**
|
|
398
|
-
*
|
|
399
|
-
*
|
|
462
|
+
* Expression to extract boolean from response (scope: `{ response }`).
|
|
463
|
+
* e.g. `'response.allowed'`, `'response.permissions.canEdit'`
|
|
464
|
+
* When omitted, the entire response is coerced to boolean: `!!response`
|
|
400
465
|
*/
|
|
401
|
-
|
|
466
|
+
responseExpression?: string;
|
|
402
467
|
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
468
|
+
* Value to return while the HTTP request is in-flight.
|
|
469
|
+
*
|
|
470
|
+
* Choose based on the logic type and desired UX:
|
|
471
|
+
* - For `hidden`: `false` = visible while loading, `true` = hidden while loading
|
|
472
|
+
* - For `disabled`: `false` = enabled while loading, `true` = disabled while loading
|
|
473
|
+
* - For `required`: `false` = optional while loading, `true` = required while loading
|
|
474
|
+
*
|
|
475
|
+
* @default false
|
|
405
476
|
*/
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
|
|
409
|
-
/** Simplified arrays do not support meta */
|
|
410
|
-
readonly meta?: never;
|
|
477
|
+
pendingValue?: boolean;
|
|
478
|
+
/** Cache duration in ms for HTTP responses. @default 30000 */
|
|
479
|
+
cacheDurationMs?: number;
|
|
411
480
|
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
481
|
+
* Debounce time in milliseconds for re-evaluation when dependent form values change.
|
|
482
|
+
* @default 300
|
|
414
483
|
*/
|
|
415
|
-
|
|
484
|
+
debounceMs?: number;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Shared fields for both branches of {@link AsyncCondition}.
|
|
488
|
+
*
|
|
489
|
+
* @internal
|
|
490
|
+
*/
|
|
491
|
+
interface AsyncConditionBase {
|
|
492
|
+
type: 'async';
|
|
416
493
|
/**
|
|
417
|
-
*
|
|
418
|
-
*
|
|
494
|
+
* Value to return while async resolution is pending.
|
|
495
|
+
*
|
|
496
|
+
* Choose based on the logic type and desired UX:
|
|
497
|
+
* - For `hidden`: `false` = visible while loading, `true` = hidden while loading
|
|
498
|
+
* - For `disabled`: `false` = enabled while loading, `true` = disabled while loading
|
|
499
|
+
* - For `required`: `false` = optional while loading, `true` = required while loading
|
|
500
|
+
*
|
|
501
|
+
* @default false
|
|
419
502
|
*/
|
|
420
|
-
|
|
503
|
+
pendingValue?: boolean;
|
|
421
504
|
/**
|
|
422
|
-
*
|
|
423
|
-
*
|
|
505
|
+
* Debounce ms for re-evaluation.
|
|
506
|
+
* @default 300
|
|
424
507
|
*/
|
|
425
|
-
|
|
426
|
-
/** Mutually exclusive with `template` — use `fields` for the full API instead */
|
|
427
|
-
readonly fields?: never;
|
|
508
|
+
debounceMs?: number;
|
|
428
509
|
}
|
|
429
510
|
/**
|
|
430
|
-
*
|
|
431
|
-
* Checks for `type: 'array'` with a `template` property (discriminant from full ArrayField).
|
|
432
|
-
*/
|
|
433
|
-
declare function isSimplifiedArrayField(field: FieldDef<any>): field is SimplifiedArrayField;
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Group field interface for creating logical field groupings that map to object values
|
|
437
|
-
* Groups create nested form structures where child field values are collected into an object
|
|
438
|
-
* This is a programmatic grouping only - users cannot customize this field type
|
|
511
|
+
* Condition that resolves asynchronously via a custom function.
|
|
439
512
|
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
513
|
+
* Two mutually exclusive forms:
|
|
514
|
+
* - `asyncFunctionName`: name of a function registered in `customFnConfig.asyncConditions`.
|
|
515
|
+
* JSON-serializable; suitable for configs loaded from APIs, databases, or OpenAPI.
|
|
516
|
+
* - `asyncFn`: inline async function. NOT JSON-serializable; for code-only configs.
|
|
443
517
|
*
|
|
444
|
-
*
|
|
518
|
+
* Exactly one of `asyncFunctionName` or `asyncFn` must be set.
|
|
519
|
+
*
|
|
520
|
+
* The function is resolved reactively — when dependent form values change,
|
|
521
|
+
* the function is re-evaluated (with debouncing). The result is cached per
|
|
522
|
+
* evaluation to avoid redundant calls.
|
|
523
|
+
*
|
|
524
|
+
* Since `LogicFn` must return `boolean` synchronously, this condition uses
|
|
525
|
+
* a signal-based async resolution pattern internally.
|
|
526
|
+
*
|
|
527
|
+
* @public
|
|
445
528
|
*/
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
readonly meta?: never;
|
|
529
|
+
type AsyncCondition = (AsyncConditionBase & {
|
|
530
|
+
/** Name of the registered async condition function */
|
|
531
|
+
asyncFunctionName: string;
|
|
532
|
+
/** Inline form is forbidden when `asyncFunctionName` is set */
|
|
533
|
+
asyncFn?: never;
|
|
534
|
+
}) | (AsyncConditionBase & {
|
|
453
535
|
/**
|
|
454
|
-
*
|
|
455
|
-
*
|
|
536
|
+
* Inline async condition function. Mutually exclusive with `asyncFunctionName`.
|
|
537
|
+
*
|
|
538
|
+
* NOT JSON-serializable — for code-only configs. For configs loaded
|
|
539
|
+
* from JSON / OpenAPI / databases, use `asyncFunctionName` to reference
|
|
540
|
+
* a function registered in `customFnConfig.asyncConditions`.
|
|
456
541
|
*/
|
|
457
|
-
|
|
542
|
+
asyncFn: AsyncConditionFunction;
|
|
543
|
+
/** Registered form is forbidden when `asyncFn` is set */
|
|
544
|
+
asyncFunctionName?: never;
|
|
545
|
+
});
|
|
546
|
+
/**
|
|
547
|
+
* Logical AND — all sub-conditions must be true.
|
|
548
|
+
*
|
|
549
|
+
* @public
|
|
550
|
+
*/
|
|
551
|
+
interface AndCondition {
|
|
552
|
+
type: 'and';
|
|
553
|
+
/** Sub-conditions that must all evaluate to true */
|
|
554
|
+
conditions: ConditionalExpression[];
|
|
458
555
|
}
|
|
459
556
|
/**
|
|
460
|
-
*
|
|
557
|
+
* Logical OR — at least one sub-condition must be true.
|
|
558
|
+
*
|
|
559
|
+
* @public
|
|
461
560
|
*/
|
|
462
|
-
|
|
463
|
-
|
|
561
|
+
interface OrCondition {
|
|
562
|
+
type: 'or';
|
|
563
|
+
/** Sub-conditions where at least one must evaluate to true */
|
|
564
|
+
conditions: ConditionalExpression[];
|
|
565
|
+
}
|
|
464
566
|
/**
|
|
465
|
-
*
|
|
466
|
-
*
|
|
567
|
+
* Discriminated union of all conditional expression types.
|
|
568
|
+
*
|
|
569
|
+
* Each variant only allows the properties relevant to its type,
|
|
570
|
+
* providing compile-time safety against invalid property combinations.
|
|
571
|
+
*
|
|
572
|
+
* @public
|
|
467
573
|
*/
|
|
468
|
-
type
|
|
574
|
+
type ConditionalExpression = FieldValueCondition | CustomCondition | JavascriptCondition | HttpCondition | AsyncCondition | AndCondition | OrCondition;
|
|
575
|
+
|
|
469
576
|
/**
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
577
|
+
* Logic configuration for container fields (page, group, row, array).
|
|
578
|
+
* Containers only support the 'hidden' logic type since they are layout containers,
|
|
579
|
+
* not form controls (they can't be readonly, disabled, or required).
|
|
473
580
|
*/
|
|
474
|
-
|
|
581
|
+
interface ContainerLogicConfig {
|
|
582
|
+
/** Only 'hidden' is supported for container fields */
|
|
583
|
+
type: 'hidden';
|
|
584
|
+
/**
|
|
585
|
+
* Condition that determines when this container is hidden.
|
|
586
|
+
* Can be a boolean or a conditional expression evaluated against form values.
|
|
587
|
+
*/
|
|
588
|
+
condition: ConditionalExpression | boolean;
|
|
589
|
+
}
|
|
590
|
+
|
|
475
591
|
/**
|
|
476
|
-
*
|
|
477
|
-
*
|
|
592
|
+
* An array item template is an array of allowed children that defines one OBJECT array item's fields.
|
|
593
|
+
* Used for object arrays where each item is `{ key1: value1, key2: value2, ... }`.
|
|
594
|
+
*/
|
|
595
|
+
type ArrayItemTemplate = readonly ArrayAllowedChildren[];
|
|
596
|
+
/**
|
|
597
|
+
* An array item definition can be either:
|
|
598
|
+
* - A single field (ArrayAllowedChildren) for PRIMITIVE array items - extracts field value directly
|
|
599
|
+
* - An array of fields (ArrayItemTemplate) for OBJECT array items - merges fields into object
|
|
600
|
+
*
|
|
601
|
+
* This allows support for:
|
|
602
|
+
* - Primitive arrays: `['tag1', 'tag2']`
|
|
603
|
+
* - Object arrays: `[{ name: 'Alice', email: '...' }]`
|
|
604
|
+
* - Heterogeneous arrays: `[{ value: 'x' }, 'y']` (mixed primitives and objects)
|
|
605
|
+
*/
|
|
606
|
+
type ArrayItemDefinition = ArrayAllowedChildren | ArrayItemTemplate;
|
|
607
|
+
/**
|
|
608
|
+
* Array field interface for creating dynamic field collections that map to array values.
|
|
609
|
+
*
|
|
610
|
+
* Key concepts:
|
|
611
|
+
* - The outer `fields` array defines INITIAL ITEMS (each element is one array item to render)
|
|
612
|
+
* - Each element can be either:
|
|
613
|
+
* - A single FieldDef (primitive item) - the field's value is extracted directly
|
|
614
|
+
* - An array of FieldDefs (object item) - fields are merged into an object
|
|
615
|
+
* - Items can have different field configurations (heterogeneous arrays supported)
|
|
616
|
+
* - Empty `fields: []` means no initial items - user adds via buttons with explicit templates
|
|
617
|
+
* - Initial values are declared via `value` property on each field definition
|
|
618
|
+
*
|
|
619
|
+
* @example
|
|
620
|
+
* // Primitive array: ['angular', 'typescript']
|
|
621
|
+
* {
|
|
622
|
+
* key: 'tags',
|
|
623
|
+
* type: 'array',
|
|
624
|
+
* fields: [
|
|
625
|
+
* { key: 'tag', type: 'input', value: 'angular' }, // Single field = primitive
|
|
626
|
+
* { key: 'tag', type: 'input', value: 'typescript' },
|
|
627
|
+
* ]
|
|
628
|
+
* }
|
|
629
|
+
*
|
|
630
|
+
* @example
|
|
631
|
+
* // Object array: [{ name: 'Alice', email: '...' }]
|
|
632
|
+
* {
|
|
633
|
+
* key: 'contacts',
|
|
634
|
+
* type: 'array',
|
|
635
|
+
* fields: [
|
|
636
|
+
* [ // Array = object
|
|
637
|
+
* { key: 'name', type: 'input', value: 'Alice' },
|
|
638
|
+
* { key: 'email', type: 'input', value: 'alice@example.com' }
|
|
639
|
+
* ],
|
|
640
|
+
* ]
|
|
641
|
+
* }
|
|
642
|
+
*
|
|
643
|
+
* @example
|
|
644
|
+
* // Heterogeneous array: [{ label: 'Structured' }, 'Simple']
|
|
645
|
+
* {
|
|
646
|
+
* key: 'items',
|
|
647
|
+
* type: 'array',
|
|
648
|
+
* fields: [
|
|
649
|
+
* [{ key: 'label', type: 'input', value: 'Structured' }], // Object item
|
|
650
|
+
* { key: 'value', type: 'input', value: 'Simple' }, // Primitive item
|
|
651
|
+
* ]
|
|
652
|
+
* }
|
|
653
|
+
*
|
|
654
|
+
* @example
|
|
655
|
+
* // Empty array (add items via buttons)
|
|
656
|
+
* {
|
|
657
|
+
* key: 'tags',
|
|
658
|
+
* type: 'array',
|
|
659
|
+
* fields: [] // No initial items - user adds via button with template
|
|
660
|
+
* }
|
|
661
|
+
*
|
|
662
|
+
* Nesting constraints: Arrays can contain rows, leaf fields, or groups (for object arrays),
|
|
663
|
+
* but NOT pages or other arrays. Runtime validation enforces these rules.
|
|
664
|
+
*
|
|
665
|
+
* Note: Arrays are container fields and do not support `meta` since they have no native form element.
|
|
666
|
+
*/
|
|
667
|
+
interface ArrayField<TFields extends readonly ArrayItemDefinition[] = readonly ArrayItemDefinition[]> extends FieldDef<never> {
|
|
668
|
+
type: 'array';
|
|
669
|
+
/**
|
|
670
|
+
* Array of item definitions. Each element defines one array item:
|
|
671
|
+
* - Single FieldDef = primitive item (field's value is extracted directly)
|
|
672
|
+
* - Array of FieldDefs = object item (fields are merged into an object)
|
|
673
|
+
* - Empty array `[]` = no initial items (add via buttons with explicit templates)
|
|
674
|
+
* - Items can have different configurations (heterogeneous arrays supported)
|
|
675
|
+
* - Initial values are set via `value` property on each field definition
|
|
676
|
+
*/
|
|
677
|
+
readonly fields: TFields;
|
|
678
|
+
/** Array fields do not have a label property **/
|
|
679
|
+
readonly label?: never;
|
|
680
|
+
/** Arrays do not support meta - they have no native form element **/
|
|
681
|
+
readonly meta?: never;
|
|
682
|
+
/**
|
|
683
|
+
* Logic configurations for conditional array visibility.
|
|
684
|
+
* Only 'hidden' type logic is supported for arrays.
|
|
685
|
+
*/
|
|
686
|
+
readonly logic?: ContainerLogicConfig[];
|
|
687
|
+
/**
|
|
688
|
+
* Minimum number of items required in the array.
|
|
689
|
+
* Validation fails if the array has fewer items than this value.
|
|
690
|
+
*/
|
|
691
|
+
readonly minLength?: number;
|
|
692
|
+
/**
|
|
693
|
+
* Maximum number of items allowed in the array.
|
|
694
|
+
* Validation fails if the array has more items than this value.
|
|
695
|
+
*/
|
|
696
|
+
readonly maxLength?: number;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Type guard for ArrayField with proper type narrowing.
|
|
700
|
+
* Validates that the field is an array type with a fields property that is an array.
|
|
701
|
+
* Fields can contain either single FieldDefs (primitive items) or arrays of FieldDefs (object items).
|
|
702
|
+
*/
|
|
703
|
+
declare function isArrayField(field: FieldDef<any>): field is ArrayField;
|
|
704
|
+
/**
|
|
705
|
+
* Configuration for auto-generated add/remove buttons in simplified array fields.
|
|
706
|
+
*/
|
|
707
|
+
interface ArrayButtonConfig {
|
|
708
|
+
/** Custom label for the button */
|
|
709
|
+
readonly label?: string;
|
|
710
|
+
/** Additional properties passed to the button component */
|
|
711
|
+
readonly props?: Record<string, unknown>;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Simplified array field interface for common use cases.
|
|
715
|
+
*
|
|
716
|
+
* Instead of manually specifying each item in `fields`, provide a `template` that defines
|
|
717
|
+
* the structure of a single item, and a `value` array with initial data.
|
|
718
|
+
* The library auto-generates add/remove buttons.
|
|
719
|
+
*
|
|
720
|
+
* Discriminant: `template` presence → simplified API; `Array.isArray(template)` → object vs primitive.
|
|
721
|
+
*
|
|
722
|
+
* @example
|
|
723
|
+
* // Primitive array: ['angular', 'typescript']
|
|
724
|
+
* {
|
|
725
|
+
* key: 'tags',
|
|
726
|
+
* type: 'array',
|
|
727
|
+
* template: { key: 'value', type: 'input', label: 'Tag', required: true },
|
|
728
|
+
* value: ['angular', 'typescript']
|
|
729
|
+
* }
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* // Object array: [{ name: 'Jane', phone: '555' }]
|
|
733
|
+
* {
|
|
734
|
+
* key: 'contacts',
|
|
735
|
+
* type: 'array',
|
|
736
|
+
* template: [
|
|
737
|
+
* { key: 'name', type: 'input', label: 'Contact Name', required: true },
|
|
738
|
+
* { key: 'phone', type: 'input', label: 'Phone Number' },
|
|
739
|
+
* ],
|
|
740
|
+
* value: [{ name: 'Jane', phone: '555' }]
|
|
741
|
+
* }
|
|
742
|
+
*
|
|
743
|
+
* @example
|
|
744
|
+
* // Button customization / opt-out
|
|
745
|
+
* {
|
|
746
|
+
* key: 'tags',
|
|
747
|
+
* type: 'array',
|
|
748
|
+
* template: { key: 'value', type: 'input', label: 'Tag' },
|
|
749
|
+
* addButton: { label: 'Add Tag', props: { color: 'primary' } },
|
|
750
|
+
* removeButton: false
|
|
751
|
+
* }
|
|
752
|
+
*/
|
|
753
|
+
interface SimplifiedArrayField extends FieldDef<never> {
|
|
754
|
+
type: 'array';
|
|
755
|
+
/**
|
|
756
|
+
* Template defining the structure of a single array item.
|
|
757
|
+
* - Single field (ArrayAllowedChildren) → primitive array (each item is a single value)
|
|
758
|
+
* - Array of fields (readonly ArrayAllowedChildren[]) → object array (each item is an object)
|
|
759
|
+
*/
|
|
760
|
+
readonly template: ArrayAllowedChildren | readonly ArrayAllowedChildren[];
|
|
761
|
+
/** Initial values for the array. Each element creates one array item. */
|
|
762
|
+
readonly value?: readonly unknown[];
|
|
763
|
+
/**
|
|
764
|
+
* Configuration for the auto-generated "Add" button, or `false` to disable it.
|
|
765
|
+
* Defaults to a button with label "Add".
|
|
766
|
+
*/
|
|
767
|
+
readonly addButton?: ArrayButtonConfig | false;
|
|
768
|
+
/**
|
|
769
|
+
* Configuration for the auto-generated "Remove" button on each item, or `false` to disable it.
|
|
770
|
+
* Defaults to a button with label "Remove".
|
|
771
|
+
*/
|
|
772
|
+
readonly removeButton?: ArrayButtonConfig | false;
|
|
773
|
+
/** Simplified arrays do not support the label property */
|
|
774
|
+
readonly label?: never;
|
|
775
|
+
/** Simplified arrays do not support meta */
|
|
776
|
+
readonly meta?: never;
|
|
777
|
+
/**
|
|
778
|
+
* Logic configurations for conditional array visibility.
|
|
779
|
+
* Only 'hidden' type logic is supported for arrays.
|
|
780
|
+
*/
|
|
781
|
+
readonly logic?: ContainerLogicConfig[];
|
|
782
|
+
/**
|
|
783
|
+
* Minimum number of items required in the array.
|
|
784
|
+
* Validation fails if the array has fewer items than this value.
|
|
785
|
+
*/
|
|
786
|
+
readonly minLength?: number;
|
|
787
|
+
/**
|
|
788
|
+
* Maximum number of items allowed in the array.
|
|
789
|
+
* Validation fails if the array has more items than this value.
|
|
790
|
+
*/
|
|
791
|
+
readonly maxLength?: number;
|
|
792
|
+
/** Mutually exclusive with `template` — use `fields` for the full API instead */
|
|
793
|
+
readonly fields?: never;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Type guard for SimplifiedArrayField.
|
|
797
|
+
* Checks for `type: 'array'` with a `template` property (discriminant from full ArrayField).
|
|
798
|
+
*/
|
|
799
|
+
declare function isSimplifiedArrayField(field: FieldDef<any>): field is SimplifiedArrayField;
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Group field interface for creating logical field groupings that map to object values
|
|
803
|
+
* Groups create nested form structures where child field values are collected into an object
|
|
804
|
+
* This is a programmatic grouping only - users cannot customize this field type
|
|
805
|
+
*
|
|
806
|
+
* TypeScript cannot enforce field nesting rules due to circular dependency limitations.
|
|
807
|
+
* For documentation: Groups should contain rows and leaf fields, but NOT pages or other groups.
|
|
808
|
+
* Runtime validation enforces these rules.
|
|
809
|
+
*
|
|
810
|
+
* Note: Groups are container fields and do not support `meta` since they have no native form element.
|
|
811
|
+
*/
|
|
812
|
+
interface GroupField<TFields extends readonly GroupAllowedChildren[] = readonly GroupAllowedChildren[]> extends FieldDef<never> {
|
|
813
|
+
type: 'group';
|
|
814
|
+
readonly fields: TFields;
|
|
815
|
+
/** Groups do not have a label property - they are logical containers only **/
|
|
816
|
+
readonly label?: never;
|
|
817
|
+
/** Groups do not support meta - they have no native form element **/
|
|
818
|
+
readonly meta?: never;
|
|
819
|
+
/**
|
|
820
|
+
* Logic configurations for conditional group visibility.
|
|
821
|
+
* Only 'hidden' type logic is supported for groups.
|
|
822
|
+
*/
|
|
823
|
+
readonly logic?: ContainerLogicConfig[];
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Type guard for GroupField with proper type narrowing
|
|
827
|
+
*/
|
|
828
|
+
declare function isGroupField(field: FieldDef<any>): field is GroupField;
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Scalar types supported by hidden fields.
|
|
832
|
+
* These are the primitive value types that can be stored.
|
|
833
|
+
*/
|
|
834
|
+
type HiddenScalar = string | number | boolean;
|
|
835
|
+
/**
|
|
836
|
+
* All value types supported by hidden fields.
|
|
837
|
+
* Supports both scalar values and arrays of scalars for storing
|
|
838
|
+
* multiple IDs or similar data.
|
|
839
|
+
*/
|
|
840
|
+
type HiddenValue = HiddenScalar | HiddenScalar[];
|
|
841
|
+
/**
|
|
842
|
+
* Hidden field definition for storing values without rendering UI.
|
|
843
|
+
*
|
|
478
844
|
* Hidden fields participate in the form schema and contribute to form values,
|
|
479
845
|
* but do not render any visible component. They are useful for:
|
|
480
846
|
* - Storing IDs when updating existing records
|
|
@@ -722,1442 +1088,1252 @@ type ExtractFields<T> = T extends {
|
|
|
722
1088
|
type InferFormValue$1<T> = ExtractFields<T> extends readonly RegisteredFieldTypes[] ? InferFormValueWithDepth<ExtractFields<T> extends RegisteredFieldTypes[] ? ExtractFields<T> : [...ExtractFields<T>], 5> : never;
|
|
723
1089
|
|
|
724
1090
|
/**
|
|
725
|
-
*
|
|
1091
|
+
* Custom validator function signature using Angular's public FieldContext API
|
|
726
1092
|
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
* This is intentional for v1: response mapping is purely about interpreting the HTTP response.
|
|
730
|
-
*/
|
|
731
|
-
interface HttpValidationResponseMapping {
|
|
732
|
-
/**
|
|
733
|
-
* Expression evaluated with scope `{ response }`. Must evaluate to `true` (strict boolean) to be valid.
|
|
734
|
-
*
|
|
735
|
-
* NOTE: This is the OPPOSITE of the function-based HttpCustomValidator.onSuccess
|
|
736
|
-
* convention, which maps successful HTTP responses to validation errors.
|
|
737
|
-
* Here, `=== true` means "validation passed". Non-boolean results are treated as invalid and trigger a warning.
|
|
738
|
-
*/
|
|
739
|
-
validWhen: string;
|
|
740
|
-
/** Error kind returned when validation fails — maps to field-level `validationMessages` */
|
|
741
|
-
errorKind: string;
|
|
742
|
-
/**
|
|
743
|
-
* Parameters to include in the validation error for message interpolation.
|
|
744
|
-
* Map of parameter names to expressions evaluated against `{ response }`.
|
|
745
|
-
*/
|
|
746
|
-
errorParams?: Record<string, string>;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
* Base configuration shared by all validators
|
|
751
|
-
*/
|
|
752
|
-
interface BaseValidatorConfig {
|
|
753
|
-
/** Conditional logic for when validator applies */
|
|
754
|
-
when?: ConditionalExpression;
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Built-in validator configuration (required, email, min, max, etc.)
|
|
758
|
-
*/
|
|
759
|
-
interface BuiltInValidatorConfig extends BaseValidatorConfig {
|
|
760
|
-
/** Validator type identifier */
|
|
761
|
-
type: 'required' | 'email' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern';
|
|
762
|
-
/** Static value for the validator (e.g., min value, pattern) */
|
|
763
|
-
value?: number | string | RegExp;
|
|
764
|
-
/** Dynamic value expression that evaluates to validator parameter */
|
|
765
|
-
expression?: string;
|
|
766
|
-
}
|
|
767
|
-
/**
|
|
768
|
-
* Custom validator configuration using Angular's public FieldContext API
|
|
769
|
-
* Returns ValidationError | ValidationError[] | null synchronously
|
|
770
|
-
*
|
|
771
|
-
* Supports two patterns:
|
|
772
|
-
* 1. Function-based: { type: 'custom', functionName: 'myValidator' }
|
|
773
|
-
* 2. Expression-based: { type: 'custom', expression: 'fieldValue === formValue.password', kind: 'passwordMismatch' }
|
|
774
|
-
*/
|
|
775
|
-
interface CustomValidatorConfig extends BaseValidatorConfig {
|
|
776
|
-
/** Validator type identifier */
|
|
777
|
-
type: 'custom';
|
|
778
|
-
/** Name of registered validator function (function-based pattern) */
|
|
779
|
-
functionName?: string;
|
|
780
|
-
/** Optional parameters to pass to validator function */
|
|
781
|
-
params?: Record<string, unknown>;
|
|
782
|
-
/** JavaScript expression to evaluate (expression-based pattern) */
|
|
783
|
-
expression?: string;
|
|
784
|
-
/** Error kind for expression-based validators - links to validationMessages */
|
|
785
|
-
kind?: string;
|
|
786
|
-
/**
|
|
787
|
-
* Parameters to include in error object for message interpolation (expression-based pattern)
|
|
788
|
-
* Map of parameter names to expressions that will be evaluated in the same context as the validation expression
|
|
789
|
-
*
|
|
790
|
-
* @example
|
|
791
|
-
* errorParams: {
|
|
792
|
-
* minValue: 'formValue.minValue',
|
|
793
|
-
* maxValue: 'formValue.maxValue'
|
|
794
|
-
* }
|
|
795
|
-
* // Allows message template: "Value must be between {{minValue}} and {{maxValue}}"
|
|
796
|
-
*/
|
|
797
|
-
errorParams?: Record<string, string>;
|
|
798
|
-
}
|
|
799
|
-
/**
|
|
800
|
-
* Async custom validator configuration using Angular's validateAsync API.
|
|
801
|
-
* Returns Observable<ValidationError | ValidationError[] | null>.
|
|
1093
|
+
* Takes FieldContext (full Angular context) and optional params, returns validation error(s) or null.
|
|
1094
|
+
* Provides access to field state, form hierarchy, and other fields for validation logic.
|
|
802
1095
|
*
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
params?: Record<string, unknown>;
|
|
811
|
-
}
|
|
812
|
-
/**
|
|
813
|
-
* Function-based HTTP validator configuration — requires a registered function.
|
|
1096
|
+
* **Use FieldContext public APIs to access:**
|
|
1097
|
+
* - Current field value: `ctx.value()`
|
|
1098
|
+
* - Field state: `ctx.state` (errors, touched, dirty, etc.)
|
|
1099
|
+
* - Other field values: `ctx.valueOf(path)` where path is a FieldPath
|
|
1100
|
+
* - Other field states: `ctx.stateOf(path)`
|
|
1101
|
+
* - Other fields: `ctx.fieldTreeOf(path)`
|
|
1102
|
+
* - Current field tree: `ctx.fieldTree`
|
|
814
1103
|
*
|
|
815
|
-
*
|
|
816
|
-
* `
|
|
1104
|
+
* **Return Types:**
|
|
1105
|
+
* - Single error: `{ kind: 'errorKind' }` for field-level validation
|
|
1106
|
+
* - Multiple errors: `[{ kind: 'error1' }, { kind: 'error2' }]` for cross-field validation
|
|
1107
|
+
* - No error: `null` when validation passes
|
|
817
1108
|
*
|
|
818
|
-
*
|
|
1109
|
+
* **Best Practice - Return Only Error Kind:**
|
|
1110
|
+
* Validators should focus on validation logic, not presentation.
|
|
1111
|
+
* Return just the error `kind` and configure messages at field level for i18n support.
|
|
819
1112
|
*
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
/**
|
|
830
|
-
* Declarative HTTP validator configuration — fully JSON-serializable, no function registration required.
|
|
1113
|
+
* @example Single Field Validation
|
|
1114
|
+
* ```typescript
|
|
1115
|
+
* const noSpaces: CustomValidator<string> = (ctx) => {
|
|
1116
|
+
* const value = ctx.value();
|
|
1117
|
+
* if (typeof value === 'string' && value.includes(' ')) {
|
|
1118
|
+
* return { kind: 'noSpaces' };
|
|
1119
|
+
* }
|
|
1120
|
+
* return null;
|
|
1121
|
+
* };
|
|
831
1122
|
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
1123
|
+
* // Field configuration
|
|
1124
|
+
* {
|
|
1125
|
+
* key: 'username',
|
|
1126
|
+
* validators: [{ type: 'custom', functionName: 'noSpaces' }],
|
|
1127
|
+
* validationMessages: {
|
|
1128
|
+
* noSpaces: 'Spaces are not allowed'
|
|
1129
|
+
* }
|
|
1130
|
+
* }
|
|
1131
|
+
* ```
|
|
834
1132
|
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
/** HTTP request configuration with expression-based query params and body */
|
|
842
|
-
http: HttpRequestConfig;
|
|
843
|
-
/** Mapping that interprets the HTTP response as a validation result */
|
|
844
|
-
responseMapping: HttpValidationResponseMapping;
|
|
845
|
-
}
|
|
846
|
-
/**
|
|
847
|
-
* Configuration for signal forms validator functions that can be serialized from API.
|
|
848
|
-
* Discriminated union type for type-safe validator configuration.
|
|
1133
|
+
* @example Cross-Field Validation with valueOf()
|
|
1134
|
+
* ```typescript
|
|
1135
|
+
* // Compare two fields using public API
|
|
1136
|
+
* const lessThan: CustomValidator<number> = (ctx, params) => {
|
|
1137
|
+
* const value = ctx.value();
|
|
1138
|
+
* const compareToPath = params?.field as string;
|
|
849
1139
|
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
* Use `isFunctionHttpValidator()` for type-safe narrowing when `type` alone is insufficient.
|
|
853
|
-
*/
|
|
854
|
-
type ValidatorConfig = BuiltInValidatorConfig | CustomValidatorConfig | AsyncValidatorConfig | FunctionHttpValidatorConfig | DeclarativeHttpValidatorConfig;
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* Special form-state conditions for button disabled logic.
|
|
1140
|
+
* // Use valueOf() to access other field - public API!
|
|
1141
|
+
* const otherValue = ctx.valueOf(compareToPath as any);
|
|
858
1142
|
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
1143
|
+
* if (otherValue !== undefined && value >= otherValue) {
|
|
1144
|
+
* return { kind: 'notLessThan' };
|
|
1145
|
+
* }
|
|
1146
|
+
* return null;
|
|
1147
|
+
* };
|
|
1148
|
+
* ```
|
|
861
1149
|
*
|
|
862
|
-
* @example
|
|
1150
|
+
* @example Multiple Errors (Cross-Field Validation)
|
|
863
1151
|
* ```typescript
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
* key: 'submit',
|
|
867
|
-
* type: 'submit',
|
|
868
|
-
* label: 'Submit',
|
|
869
|
-
* logic: [
|
|
870
|
-
* { type: 'disabled', condition: 'formInvalid' },
|
|
871
|
-
* { type: 'disabled', condition: 'formSubmitting' },
|
|
872
|
-
* ]
|
|
873
|
-
* }
|
|
1152
|
+
* const validateDateRange: CustomValidator = (ctx) => {
|
|
1153
|
+
* const errors: ValidationError[] = [];
|
|
874
1154
|
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
1155
|
+
* const startDate = ctx.valueOf('startDate' as any);
|
|
1156
|
+
* const endDate = ctx.valueOf('endDate' as any);
|
|
1157
|
+
*
|
|
1158
|
+
* if (!startDate) errors.push({ kind: 'startDateRequired' });
|
|
1159
|
+
* if (!endDate) errors.push({ kind: 'endDateRequired' });
|
|
1160
|
+
* if (startDate && endDate && startDate > endDate) {
|
|
1161
|
+
* errors.push({ kind: 'invalidDateRange' });
|
|
1162
|
+
* }
|
|
1163
|
+
*
|
|
1164
|
+
* return errors.length > 0 ? errors : null;
|
|
1165
|
+
* };
|
|
883
1166
|
* ```
|
|
884
1167
|
*
|
|
885
|
-
*
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
'formInvalid'
|
|
890
|
-
/** True when form.submitting() === true */
|
|
891
|
-
| 'formSubmitting'
|
|
892
|
-
/** True when fields on the current page are invalid (for paged forms) */
|
|
893
|
-
| 'pageInvalid';
|
|
894
|
-
/**
|
|
895
|
-
* Logic type for controlling field state (hidden, readonly, disabled, required).
|
|
1168
|
+
* **Message Resolution Priority:**
|
|
1169
|
+
* 1. Field-level `validationMessages[kind]` (highest priority - per-field customization)
|
|
1170
|
+
* 2. Form-level `defaultValidationMessages[kind]` (fallback for common messages)
|
|
1171
|
+
* 3. **No message configured = Warning logged + error NOT displayed**
|
|
896
1172
|
*
|
|
897
|
-
*
|
|
898
|
-
*/
|
|
899
|
-
type StateLogicType = 'hidden' | 'readonly' | 'disabled' | 'required';
|
|
900
|
-
/**
|
|
901
|
-
* Base configuration for conditional field state logic.
|
|
1173
|
+
* **Important:** Validator-returned messages are NOT used. All messages MUST be explicitly configured.
|
|
902
1174
|
*
|
|
903
|
-
* @
|
|
1175
|
+
* @template TValue The type of value stored in the field being validated
|
|
904
1176
|
*/
|
|
905
|
-
|
|
906
|
-
/**
|
|
907
|
-
* Logic type identifier for field state.
|
|
908
|
-
*
|
|
909
|
-
* - `hidden`: Hide the field from view (still participates in form state)
|
|
910
|
-
* - `readonly`: Make the field read-only
|
|
911
|
-
* - `disabled`: Disable user interaction
|
|
912
|
-
* - `required`: Make the field required
|
|
913
|
-
*/
|
|
914
|
-
type: StateLogicType;
|
|
915
|
-
/**
|
|
916
|
-
* Condition that determines when this logic applies.
|
|
917
|
-
*
|
|
918
|
-
* Can be:
|
|
919
|
-
* - `boolean`: Static value (always applies or never applies)
|
|
920
|
-
* - `ConditionalExpression`: Expression evaluated against field/form values
|
|
921
|
-
* - `FormStateCondition`: Special form/page state check (for buttons)
|
|
922
|
-
*
|
|
923
|
-
* @example
|
|
924
|
-
* ```typescript
|
|
925
|
-
* // Static condition
|
|
926
|
-
* condition: true
|
|
927
|
-
*
|
|
928
|
-
* // Field value condition
|
|
929
|
-
* condition: {
|
|
930
|
-
* type: 'fieldValue',
|
|
931
|
-
* fieldPath: 'status',
|
|
932
|
-
* operator: 'equals',
|
|
933
|
-
* value: 'locked'
|
|
934
|
-
* }
|
|
935
|
-
*
|
|
936
|
-
* // Form state condition (for buttons)
|
|
937
|
-
* condition: 'formSubmitting'
|
|
938
|
-
* ```
|
|
939
|
-
*/
|
|
940
|
-
condition: ConditionalExpression | boolean | FormStateCondition;
|
|
941
|
-
}
|
|
1177
|
+
type CustomValidator<TValue = unknown> = (ctx: FieldContext<TValue>, params?: Record<string, unknown>) => ValidationError$1 | ValidationError$1[] | null;
|
|
942
1178
|
/**
|
|
943
|
-
*
|
|
1179
|
+
* Async custom validator configuration using Angular's resource-based API
|
|
944
1180
|
*
|
|
945
|
-
*
|
|
946
|
-
|
|
947
|
-
interface ImmediateStateLogicConfig extends BaseStateLogicConfig {
|
|
948
|
-
/**
|
|
949
|
-
* Trigger for immediate evaluation.
|
|
950
|
-
* @default 'onChange'
|
|
951
|
-
*/
|
|
952
|
-
trigger?: 'onChange';
|
|
953
|
-
/** Not allowed for onChange trigger */
|
|
954
|
-
debounceMs?: never;
|
|
955
|
-
}
|
|
956
|
-
/**
|
|
957
|
-
* State logic that evaluates after a debounce period.
|
|
958
|
-
*
|
|
959
|
-
* @internal
|
|
960
|
-
*/
|
|
961
|
-
interface DebouncedStateLogicConfig extends BaseStateLogicConfig {
|
|
962
|
-
/**
|
|
963
|
-
* Trigger for debounced evaluation.
|
|
964
|
-
* Evaluates after the value has stabilized for the debounce duration.
|
|
965
|
-
*/
|
|
966
|
-
trigger: 'debounced';
|
|
967
|
-
/**
|
|
968
|
-
* Debounce duration in milliseconds.
|
|
969
|
-
* @default 500
|
|
970
|
-
*/
|
|
971
|
-
debounceMs?: number;
|
|
972
|
-
}
|
|
973
|
-
/**
|
|
974
|
-
* Configuration for conditional field state logic.
|
|
975
|
-
*
|
|
976
|
-
* Defines how field behavior changes based on conditions.
|
|
977
|
-
* Supports hiding, disabling, making readonly, or requiring fields
|
|
978
|
-
* based on form state or field values.
|
|
979
|
-
*
|
|
980
|
-
* @example
|
|
981
|
-
* ```typescript
|
|
982
|
-
* // Hide email field when contact method is not email
|
|
983
|
-
* {
|
|
984
|
-
* type: 'hidden',
|
|
985
|
-
* condition: {
|
|
986
|
-
* type: 'fieldValue',
|
|
987
|
-
* fieldPath: 'contactMethod',
|
|
988
|
-
* operator: 'notEquals',
|
|
989
|
-
* value: 'email'
|
|
990
|
-
* }
|
|
991
|
-
* }
|
|
992
|
-
*
|
|
993
|
-
* // Disable button when form is submitting
|
|
994
|
-
* {
|
|
995
|
-
* type: 'disabled',
|
|
996
|
-
* condition: 'formSubmitting'
|
|
997
|
-
* }
|
|
998
|
-
*
|
|
999
|
-
* // Debounced visibility (avoids flicker during rapid typing)
|
|
1000
|
-
* {
|
|
1001
|
-
* type: 'hidden',
|
|
1002
|
-
* trigger: 'debounced',
|
|
1003
|
-
* debounceMs: 300,
|
|
1004
|
-
* condition: {
|
|
1005
|
-
* type: 'fieldValue',
|
|
1006
|
-
* fieldPath: 'search',
|
|
1007
|
-
* operator: 'isEmpty'
|
|
1008
|
-
* }
|
|
1009
|
-
* }
|
|
1010
|
-
* ```
|
|
1011
|
-
*
|
|
1012
|
-
* @public
|
|
1013
|
-
*/
|
|
1014
|
-
type StateLogicConfig = ImmediateStateLogicConfig | DebouncedStateLogicConfig;
|
|
1015
|
-
/**
|
|
1016
|
-
* Shared fields that appear on all derivation logic config variants.
|
|
1181
|
+
* Angular's validateAsync uses the resource API for async validation,
|
|
1182
|
+
* which provides better integration with Signal Forms lifecycle management.
|
|
1017
1183
|
*
|
|
1018
|
-
*
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
*/
|
|
1024
|
-
type: 'derivation';
|
|
1025
|
-
/**
|
|
1026
|
-
* Target property name for property derivation.
|
|
1027
|
-
*
|
|
1028
|
-
* When set, this derivation targets a field property (like `minDate`, `options`,
|
|
1029
|
-
* `label`, `placeholder`) instead of the field's value. The derivation is routed
|
|
1030
|
-
* to the property derivation pipeline.
|
|
1031
|
-
*
|
|
1032
|
-
* When absent, the derivation targets the field's value (default behavior).
|
|
1033
|
-
*
|
|
1034
|
-
* **Depth limit (max 2 levels):** Only simple and single-dot-nested paths are
|
|
1035
|
-
* supported. Paths with 2+ dots (e.g., `'a.b.c'`) will throw at runtime.
|
|
1036
|
-
*
|
|
1037
|
-
* @example
|
|
1038
|
-
* ```typescript
|
|
1039
|
-
* // Property derivation (new unified API)
|
|
1040
|
-
* {
|
|
1041
|
-
* key: 'endDate',
|
|
1042
|
-
* type: 'datepicker',
|
|
1043
|
-
* logic: [{
|
|
1044
|
-
* type: 'derivation',
|
|
1045
|
-
* targetProperty: 'minDate',
|
|
1046
|
-
* expression: 'formValue.startDate'
|
|
1047
|
-
* }]
|
|
1048
|
-
* }
|
|
1049
|
-
*
|
|
1050
|
-
* // Value derivation (no targetProperty — existing behavior)
|
|
1051
|
-
* {
|
|
1052
|
-
* key: 'total',
|
|
1053
|
-
* type: 'input',
|
|
1054
|
-
* logic: [{
|
|
1055
|
-
* type: 'derivation',
|
|
1056
|
-
* expression: 'formValue.quantity * formValue.unitPrice'
|
|
1057
|
-
* }]
|
|
1058
|
-
* }
|
|
1059
|
-
* ```
|
|
1060
|
-
*/
|
|
1061
|
-
targetProperty?: string;
|
|
1062
|
-
/**
|
|
1063
|
-
* Optional name for this derivation for debugging purposes.
|
|
1064
|
-
*
|
|
1065
|
-
* When provided, this name appears in derivation debug logs,
|
|
1066
|
-
* making it easier to identify specific derivations in complex forms.
|
|
1067
|
-
*
|
|
1068
|
-
* @example
|
|
1069
|
-
* ```typescript
|
|
1070
|
-
* {
|
|
1071
|
-
* key: 'lineTotal',
|
|
1072
|
-
* type: 'input',
|
|
1073
|
-
* logic: [{
|
|
1074
|
-
* type: 'derivation',
|
|
1075
|
-
* debugName: 'Calculate line total',
|
|
1076
|
-
* expression: 'formValue.quantity * formValue.unitPrice'
|
|
1077
|
-
* }]
|
|
1078
|
-
* }
|
|
1079
|
-
* ```
|
|
1080
|
-
*/
|
|
1081
|
-
debugName?: string;
|
|
1082
|
-
/**
|
|
1083
|
-
* Condition that determines when this derivation applies.
|
|
1084
|
-
*
|
|
1085
|
-
* Can be:
|
|
1086
|
-
* - `boolean`: Static value (always applies or never applies)
|
|
1087
|
-
* - `ConditionalExpression`: Expression evaluated against field/form values
|
|
1088
|
-
*
|
|
1089
|
-
* Defaults to `true` (always apply).
|
|
1090
|
-
*
|
|
1091
|
-
* Note: FormStateCondition is not supported for derivations.
|
|
1092
|
-
*
|
|
1093
|
-
* @example
|
|
1094
|
-
* ```typescript
|
|
1095
|
-
* // Always compute (default)
|
|
1096
|
-
* condition: true
|
|
1097
|
-
*
|
|
1098
|
-
* // Conditional derivation
|
|
1099
|
-
* condition: {
|
|
1100
|
-
* type: 'fieldValue',
|
|
1101
|
-
* fieldPath: 'country',
|
|
1102
|
-
* operator: 'equals',
|
|
1103
|
-
* value: 'USA'
|
|
1104
|
-
* }
|
|
1105
|
-
* ```
|
|
1106
|
-
*/
|
|
1107
|
-
condition?: ConditionalExpression | boolean;
|
|
1108
|
-
/**
|
|
1109
|
-
* When true, the derivation stops running after the user manually
|
|
1110
|
-
* edits the target field.
|
|
1111
|
-
*
|
|
1112
|
-
* This is useful for "smart defaults" — values that should be
|
|
1113
|
-
* auto-filled initially but respected once the user explicitly changes them.
|
|
1114
|
-
*
|
|
1115
|
-
* Uses the field's `dirty()` signal to detect user modification.
|
|
1116
|
-
* Derivations write directly to `value.set()` which does not trigger
|
|
1117
|
-
* `markAsDirty()`, so `dirty === true` reliably indicates a user edit.
|
|
1118
|
-
*
|
|
1119
|
-
* @example
|
|
1120
|
-
* ```typescript
|
|
1121
|
-
* // Auto-fill display name from first + last name, but stop if user edits it
|
|
1122
|
-
* {
|
|
1123
|
-
* key: 'displayName',
|
|
1124
|
-
* logic: [{
|
|
1125
|
-
* type: 'derivation',
|
|
1126
|
-
* expression: 'formValue.firstName + " " + formValue.lastName',
|
|
1127
|
-
* stopOnUserOverride: true
|
|
1128
|
-
* }]
|
|
1129
|
-
* }
|
|
1130
|
-
* ```
|
|
1131
|
-
*/
|
|
1132
|
-
stopOnUserOverride?: boolean;
|
|
1133
|
-
/**
|
|
1134
|
-
* When true (and `stopOnUserOverride` is also true), clears the
|
|
1135
|
-
* user-override flag when any dependency of this derivation changes,
|
|
1136
|
-
* allowing the derivation to run again.
|
|
1137
|
-
*
|
|
1138
|
-
* This is useful when a user override should only persist until the
|
|
1139
|
-
* underlying data changes — e.g., when switching countries, the
|
|
1140
|
-
* phone prefix should re-derive even if the user previously edited it.
|
|
1141
|
-
*
|
|
1142
|
-
* @example
|
|
1143
|
-
* ```typescript
|
|
1144
|
-
* {
|
|
1145
|
-
* key: 'phonePrefix',
|
|
1146
|
-
* logic: [{
|
|
1147
|
-
* type: 'derivation',
|
|
1148
|
-
* value: '+1',
|
|
1149
|
-
* condition: { type: 'fieldValue', fieldPath: 'country', operator: 'equals', value: 'USA' },
|
|
1150
|
-
* stopOnUserOverride: true,
|
|
1151
|
-
* reEngageOnDependencyChange: true,
|
|
1152
|
-
* dependsOn: ['country']
|
|
1153
|
-
* }]
|
|
1154
|
-
* }
|
|
1155
|
-
* ```
|
|
1156
|
-
*/
|
|
1157
|
-
reEngageOnDependencyChange?: boolean;
|
|
1158
|
-
}
|
|
1159
|
-
/**
|
|
1160
|
-
* Trigger variants for derivation timing.
|
|
1161
|
-
* @internal
|
|
1162
|
-
*/
|
|
1163
|
-
type ImmediateDerivationTrigger = {
|
|
1164
|
-
trigger?: 'onChange';
|
|
1165
|
-
debounceMs?: never;
|
|
1166
|
-
};
|
|
1167
|
-
type DebouncedDerivationTrigger = {
|
|
1168
|
-
trigger: 'debounced';
|
|
1169
|
-
debounceMs?: number;
|
|
1170
|
-
};
|
|
1171
|
-
/**
|
|
1172
|
-
* Base for HTTP derivations. `source: 'http'` is required.
|
|
1173
|
-
* TypeScript enforces `dependsOn` and `responseExpression` at the type level.
|
|
1184
|
+
* **Structure:**
|
|
1185
|
+
* - `params`: Function that computes params from field context
|
|
1186
|
+
* - `factory`: Function that creates a ResourceRef from params signal
|
|
1187
|
+
* - `onSuccess`: Maps successful resource result to validation errors
|
|
1188
|
+
* - `onError`: Optional handler for resource errors (network failures, etc.)
|
|
1174
1189
|
*
|
|
1175
|
-
*
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
source: 'http';
|
|
1180
|
-
/**
|
|
1181
|
-
* HTTP request configuration for server-driven derivations.
|
|
1182
|
-
*
|
|
1183
|
-
* The request is sent when dependencies change, with automatic
|
|
1184
|
-
* debouncing and cancellation of in-flight requests.
|
|
1185
|
-
*
|
|
1186
|
-
* Configure debounce via the `trigger: 'debounced'` + `debounceMs` mechanism.
|
|
1187
|
-
*
|
|
1188
|
-
* @example
|
|
1189
|
-
* ```typescript
|
|
1190
|
-
* {
|
|
1191
|
-
* key: 'exchangeRate',
|
|
1192
|
-
* logic: [{
|
|
1193
|
-
* type: 'derivation',
|
|
1194
|
-
* source: 'http',
|
|
1195
|
-
* http: {
|
|
1196
|
-
* url: '/api/exchange-rate',
|
|
1197
|
-
* method: 'GET',
|
|
1198
|
-
* queryParams: {
|
|
1199
|
-
* from: 'formValue.sourceCurrency',
|
|
1200
|
-
* to: 'formValue.targetCurrency',
|
|
1201
|
-
* },
|
|
1202
|
-
* },
|
|
1203
|
-
* responseExpression: 'response.rate',
|
|
1204
|
-
* dependsOn: ['sourceCurrency', 'targetCurrency'],
|
|
1205
|
-
* }]
|
|
1206
|
-
* }
|
|
1207
|
-
* ```
|
|
1208
|
-
*/
|
|
1209
|
-
http: HttpRequestConfig;
|
|
1210
|
-
/**
|
|
1211
|
-
* Explicit field dependencies. Required for HTTP derivations to prevent
|
|
1212
|
-
* wildcard triggering on every keystroke.
|
|
1213
|
-
*
|
|
1214
|
-
* The wildcard token `'*'` is rejected. The structural token `'$group'`
|
|
1215
|
-
* (and `'$group.X'`) is allowed and resolves to the field's parent container
|
|
1216
|
-
* path — be aware this fires whenever any sibling under that group changes,
|
|
1217
|
-
* so the caller owns the request frequency (consider `trigger: 'debounced'`).
|
|
1218
|
-
*/
|
|
1219
|
-
dependsOn: string[];
|
|
1220
|
-
/**
|
|
1221
|
-
* Expression to extract the derived value from the HTTP response.
|
|
1222
|
-
*
|
|
1223
|
-
* Evaluated via `ExpressionParser` with `{ response }` as the evaluation scope.
|
|
1224
|
-
*
|
|
1225
|
-
* @example
|
|
1226
|
-
* ```typescript
|
|
1227
|
-
* responseExpression: 'response.rate'
|
|
1228
|
-
* responseExpression: 'response.data.suggestedPrice'
|
|
1229
|
-
* ```
|
|
1230
|
-
*/
|
|
1231
|
-
responseExpression: string;
|
|
1232
|
-
value?: never;
|
|
1233
|
-
expression?: never;
|
|
1234
|
-
functionName?: never;
|
|
1235
|
-
asyncFunctionName?: never;
|
|
1236
|
-
}
|
|
1237
|
-
/**
|
|
1238
|
-
* Base for async function derivations. `source: 'asyncFunction'` is required.
|
|
1239
|
-
* TypeScript enforces `dependsOn` at the type level.
|
|
1190
|
+
* **Use Cases:**
|
|
1191
|
+
* - Database lookups via services with resource API
|
|
1192
|
+
* - Complex async business logic with Angular resources
|
|
1193
|
+
* - Any async operation using Angular's resource pattern
|
|
1240
1194
|
*
|
|
1241
|
-
*
|
|
1242
|
-
|
|
1243
|
-
interface AsyncFunctionDerivationBase extends SharedDerivationFields {
|
|
1244
|
-
/** Identifies this derivation as async-function-driven. */
|
|
1245
|
-
source: 'asyncFunction';
|
|
1246
|
-
/**
|
|
1247
|
-
* Name of a registered async derivation function.
|
|
1248
|
-
*
|
|
1249
|
-
* The function receives the evaluation context and returns a Promise or Observable
|
|
1250
|
-
* of the derived value. Register functions in `customFnConfig.asyncDerivations`.
|
|
1251
|
-
*
|
|
1252
|
-
* @example
|
|
1253
|
-
* ```typescript
|
|
1254
|
-
* {
|
|
1255
|
-
* key: 'suggestedPrice',
|
|
1256
|
-
* logic: [{
|
|
1257
|
-
* type: 'derivation',
|
|
1258
|
-
* source: 'asyncFunction',
|
|
1259
|
-
* asyncFunctionName: 'fetchSuggestedPrice',
|
|
1260
|
-
* dependsOn: ['productId', 'quantity'],
|
|
1261
|
-
* }]
|
|
1262
|
-
* }
|
|
1263
|
-
* ```
|
|
1264
|
-
*/
|
|
1265
|
-
asyncFunctionName: string;
|
|
1266
|
-
/**
|
|
1267
|
-
* Explicit field dependencies. Required for async derivations to prevent
|
|
1268
|
-
* wildcard triggering on every form change.
|
|
1269
|
-
*
|
|
1270
|
-
* The wildcard token `'*'` is rejected. The structural token `'$group'`
|
|
1271
|
-
* (and `'$group.X'`) is allowed and resolves to the field's parent container
|
|
1272
|
-
* path — be aware this fires whenever any sibling under that group changes,
|
|
1273
|
-
* so the caller owns the invocation frequency (handle this via
|
|
1274
|
-
* `trigger: 'debounced'` or via debouncing inside the async function).
|
|
1275
|
-
*/
|
|
1276
|
-
dependsOn: string[];
|
|
1277
|
-
value?: never;
|
|
1278
|
-
expression?: never;
|
|
1279
|
-
functionName?: never;
|
|
1280
|
-
http?: never;
|
|
1281
|
-
responseExpression?: never;
|
|
1282
|
-
}
|
|
1283
|
-
/**
|
|
1284
|
-
* Base for expression derivations.
|
|
1195
|
+
* **Note:** For HTTP validation, prefer `HttpCustomValidator` which provides
|
|
1196
|
+
* a simpler API specifically designed for HTTP requests.
|
|
1285
1197
|
*
|
|
1286
|
-
* @
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
* @example
|
|
1312
|
-
* ```typescript
|
|
1313
|
-
* // Override automatic detection for complex expressions
|
|
1314
|
-
* {
|
|
1315
|
-
* key: 'total',
|
|
1316
|
-
* logic: [{
|
|
1317
|
-
* type: 'derivation',
|
|
1318
|
-
* expression: 'calculateTotal(formValue)',
|
|
1319
|
-
* dependsOn: ['quantity', 'unitPrice', 'discount']
|
|
1320
|
-
* }]
|
|
1321
|
-
* }
|
|
1322
|
-
* ```
|
|
1323
|
-
*/
|
|
1324
|
-
dependsOn?: string[];
|
|
1325
|
-
value?: never;
|
|
1326
|
-
functionName?: never;
|
|
1327
|
-
http?: never;
|
|
1328
|
-
asyncFunctionName?: never;
|
|
1329
|
-
responseExpression?: never;
|
|
1330
|
-
}
|
|
1331
|
-
/**
|
|
1332
|
-
* Base for value derivations.
|
|
1198
|
+
* @example Database Lookup with Resource
|
|
1199
|
+
* ```typescript
|
|
1200
|
+
* const checkUsernameAvailable: AsyncCustomValidator<string> = {
|
|
1201
|
+
* params: (ctx) => ({ username: ctx.value() }),
|
|
1202
|
+
* factory: (params) => {
|
|
1203
|
+
* const injector = inject(Injector);
|
|
1204
|
+
* return resource({
|
|
1205
|
+
* request: () => params(),
|
|
1206
|
+
* loader: ({ request }) => {
|
|
1207
|
+
* if (!request?.username) return null;
|
|
1208
|
+
* const service = injector.get(UserService);
|
|
1209
|
+
* return firstValueFrom(service.checkAvailability(request.username));
|
|
1210
|
+
* }
|
|
1211
|
+
* });
|
|
1212
|
+
* },
|
|
1213
|
+
* onSuccess: (result, ctx) => {
|
|
1214
|
+
* if (!result) return null;
|
|
1215
|
+
* return result.available ? null : { kind: 'usernameTaken' };
|
|
1216
|
+
* },
|
|
1217
|
+
* onError: (error, ctx) => {
|
|
1218
|
+
* console.error('Availability check failed:', error);
|
|
1219
|
+
* return null; // Don't block form on network errors
|
|
1220
|
+
* }
|
|
1221
|
+
* };
|
|
1222
|
+
* ```
|
|
1333
1223
|
*
|
|
1334
|
-
* @
|
|
1224
|
+
* @template TValue The type of value stored in the field being validated
|
|
1225
|
+
* @template TParams The type of params passed to the resource
|
|
1226
|
+
* @template TResult The type of result returned by the resource
|
|
1335
1227
|
*/
|
|
1336
|
-
interface
|
|
1337
|
-
source?: never;
|
|
1228
|
+
interface AsyncCustomValidator<TValue = unknown, TParams = unknown, TResult = unknown> {
|
|
1338
1229
|
/**
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
* Use when the derived value is a constant.
|
|
1342
|
-
*
|
|
1343
|
-
* @example
|
|
1344
|
-
* ```typescript
|
|
1345
|
-
* value: '+1' // String
|
|
1346
|
-
* value: 100 // Number
|
|
1347
|
-
* value: true // Boolean
|
|
1348
|
-
* value: { code: 'US' } // Object
|
|
1349
|
-
* ```
|
|
1230
|
+
* Function that receives field context and returns resource params.
|
|
1231
|
+
* Params will be tracked as a signal and trigger resource reload when changed.
|
|
1350
1232
|
*/
|
|
1351
|
-
|
|
1233
|
+
readonly params: (ctx: FieldContext<TValue>, config?: Record<string, unknown>) => TParams;
|
|
1352
1234
|
/**
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1355
|
-
* For `value` (static), no dependencies are needed.
|
|
1356
|
-
* Provide `dependsOn` to conditionally re-evaluate when specific fields change.
|
|
1235
|
+
* Function that creates a ResourceRef from the params signal.
|
|
1236
|
+
* The resource will be automatically managed by Angular's lifecycle.
|
|
1357
1237
|
*/
|
|
1358
|
-
|
|
1359
|
-
expression?: never;
|
|
1360
|
-
functionName?: never;
|
|
1361
|
-
http?: never;
|
|
1362
|
-
asyncFunctionName?: never;
|
|
1363
|
-
responseExpression?: never;
|
|
1364
|
-
}
|
|
1365
|
-
/**
|
|
1366
|
-
* Base for custom sync function derivations.
|
|
1367
|
-
*
|
|
1368
|
-
* @internal
|
|
1369
|
-
*/
|
|
1370
|
-
interface FunctionDerivationBase extends SharedDerivationFields {
|
|
1371
|
-
source?: never;
|
|
1238
|
+
readonly factory: (params: Signal<TParams | undefined>) => ResourceRef<TResult | undefined>;
|
|
1372
1239
|
/**
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
* The function receives the evaluation context and returns the derived value.
|
|
1376
|
-
* Register functions in `customFnConfig.derivations`.
|
|
1377
|
-
*
|
|
1378
|
-
* @example
|
|
1379
|
-
* ```typescript
|
|
1380
|
-
* functionName: 'getCurrencyForCountry'
|
|
1381
|
-
* functionName: 'calculateTax'
|
|
1382
|
-
* functionName: 'formatPhoneNumber'
|
|
1383
|
-
* ```
|
|
1240
|
+
* Optional function to map successful resource result to validation errors.
|
|
1241
|
+
* Return null if validation passes, or ValidationError/ValidationError[] if validation fails.
|
|
1384
1242
|
*/
|
|
1385
|
-
|
|
1243
|
+
readonly onSuccess?: (result: TResult, ctx: FieldContext<TValue>) => TreeValidationResult;
|
|
1386
1244
|
/**
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
1389
|
-
* If not provided, defaults to all fields ('*').
|
|
1390
|
-
* Specify `dependsOn` for better performance when the function only
|
|
1391
|
-
* depends on a subset of fields.
|
|
1392
|
-
*
|
|
1393
|
-
* @example
|
|
1394
|
-
* ```typescript
|
|
1395
|
-
* // Only re-evaluate when country changes
|
|
1396
|
-
* {
|
|
1397
|
-
* key: 'currency',
|
|
1398
|
-
* logic: [{
|
|
1399
|
-
* type: 'derivation',
|
|
1400
|
-
* functionName: 'getCurrencyForCountry',
|
|
1401
|
-
* dependsOn: ['country']
|
|
1402
|
-
* }]
|
|
1403
|
-
* }
|
|
1404
|
-
* ```
|
|
1245
|
+
* Optional error handler for resource errors (network failures, etc.).
|
|
1246
|
+
* Return null to ignore the error, or ValidationError to display it to the user.
|
|
1405
1247
|
*/
|
|
1406
|
-
|
|
1407
|
-
value?: never;
|
|
1408
|
-
expression?: never;
|
|
1409
|
-
http?: never;
|
|
1410
|
-
asyncFunctionName?: never;
|
|
1411
|
-
responseExpression?: never;
|
|
1248
|
+
readonly onError?: (error: unknown, ctx: FieldContext<TValue>) => ValidationError$1 | ValidationError$1[] | null;
|
|
1412
1249
|
}
|
|
1413
1250
|
/**
|
|
1414
|
-
* HTTP
|
|
1415
|
-
* @internal
|
|
1416
|
-
*/
|
|
1417
|
-
type OnChangeHttpDerivationLogicConfig = HttpDerivationBase & ImmediateDerivationTrigger;
|
|
1418
|
-
/**
|
|
1419
|
-
* HTTP derivation that evaluates after a debounce period.
|
|
1420
|
-
* @internal
|
|
1421
|
-
*/
|
|
1422
|
-
type DebouncedHttpDerivationLogicConfig = HttpDerivationBase & DebouncedDerivationTrigger;
|
|
1423
|
-
/**
|
|
1424
|
-
* Async function derivation that evaluates immediately on change (default).
|
|
1425
|
-
* @internal
|
|
1426
|
-
*/
|
|
1427
|
-
type OnChangeAsyncFunctionDerivationLogicConfig = AsyncFunctionDerivationBase & ImmediateDerivationTrigger;
|
|
1428
|
-
/**
|
|
1429
|
-
* Async function derivation that evaluates after a debounce period.
|
|
1430
|
-
* @internal
|
|
1431
|
-
*/
|
|
1432
|
-
type DebouncedAsyncFunctionDerivationLogicConfig = AsyncFunctionDerivationBase & DebouncedDerivationTrigger;
|
|
1433
|
-
/**
|
|
1434
|
-
* Expression derivation that evaluates immediately on change (default).
|
|
1435
|
-
* @internal
|
|
1436
|
-
*/
|
|
1437
|
-
type OnChangeExpressionDerivationLogicConfig = ExpressionDerivationBase & ImmediateDerivationTrigger;
|
|
1438
|
-
/**
|
|
1439
|
-
* Expression derivation that evaluates after a debounce period.
|
|
1440
|
-
* @internal
|
|
1441
|
-
*/
|
|
1442
|
-
type DebouncedExpressionDerivationLogicConfig = ExpressionDerivationBase & DebouncedDerivationTrigger;
|
|
1443
|
-
/**
|
|
1444
|
-
* Value derivation that evaluates immediately on change (default).
|
|
1445
|
-
* @internal
|
|
1446
|
-
*/
|
|
1447
|
-
type OnChangeValueDerivationLogicConfig = ValueDerivationBase & ImmediateDerivationTrigger;
|
|
1448
|
-
/**
|
|
1449
|
-
* Value derivation that evaluates after a debounce period.
|
|
1450
|
-
* @internal
|
|
1451
|
-
*/
|
|
1452
|
-
type DebouncedValueDerivationLogicConfig = ValueDerivationBase & DebouncedDerivationTrigger;
|
|
1453
|
-
/**
|
|
1454
|
-
* Function derivation that evaluates immediately on change (default).
|
|
1455
|
-
* @internal
|
|
1456
|
-
*/
|
|
1457
|
-
type OnChangeFunctionDerivationLogicConfig = FunctionDerivationBase & ImmediateDerivationTrigger;
|
|
1458
|
-
/**
|
|
1459
|
-
* Function derivation that evaluates after a debounce period.
|
|
1460
|
-
* @internal
|
|
1251
|
+
* HTTP request configuration for validateHttp API
|
|
1461
1252
|
*/
|
|
1462
|
-
|
|
1253
|
+
interface HttpResourceRequest {
|
|
1254
|
+
url: string;
|
|
1255
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
1256
|
+
body?: unknown;
|
|
1257
|
+
headers?: Record<string, string | string[]>;
|
|
1258
|
+
}
|
|
1463
1259
|
/**
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
* Enables programmatic value derivation based on conditions.
|
|
1467
|
-
* Derivations are self-targeting: the logic is placed on the field
|
|
1468
|
-
* that should receive the computed value.
|
|
1260
|
+
* HTTP-based validator configuration for Angular's validateHttp API
|
|
1469
1261
|
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
* // Set phone prefix based on country selection
|
|
1473
|
-
* {
|
|
1474
|
-
* key: 'phonePrefix',
|
|
1475
|
-
* logic: [{
|
|
1476
|
-
* type: 'derivation',
|
|
1477
|
-
* value: '+1',
|
|
1478
|
-
* condition: {
|
|
1479
|
-
* type: 'fieldValue',
|
|
1480
|
-
* fieldPath: 'country',
|
|
1481
|
-
* operator: 'equals',
|
|
1482
|
-
* value: 'USA'
|
|
1483
|
-
* }
|
|
1484
|
-
* }]
|
|
1485
|
-
* }
|
|
1262
|
+
* Angular's validateHttp provides optimized HTTP validation with automatic
|
|
1263
|
+
* request cancellation and integration with the resource API.
|
|
1486
1264
|
*
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
* }
|
|
1265
|
+
* **Benefits:**
|
|
1266
|
+
* - Automatic request cancellation when field value changes
|
|
1267
|
+
* - Built-in integration with Angular's resource management
|
|
1268
|
+
* - Simpler than AsyncCustomValidator for HTTP use cases
|
|
1492
1269
|
*
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
* type: 'derivation',
|
|
1498
|
-
* functionName: 'getCurrencyForCountry'
|
|
1499
|
-
* }]
|
|
1500
|
-
* }
|
|
1270
|
+
* **Structure:**
|
|
1271
|
+
* - `request`: Function that returns URL string or HttpResourceRequest
|
|
1272
|
+
* - `onSuccess`: REQUIRED - Maps HTTP response to validation errors (inverted logic!)
|
|
1273
|
+
* - `onError`: Optional handler for HTTP errors (network failures, 4xx/5xx)
|
|
1501
1274
|
*
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
1504
|
-
* {
|
|
1505
|
-
* key: 'email',
|
|
1506
|
-
* logic: [{
|
|
1507
|
-
* type: 'derivation',
|
|
1508
|
-
* expression: 'formValue.email.toLowerCase()',
|
|
1509
|
-
* trigger: 'debounced',
|
|
1510
|
-
* debounceMs: 500
|
|
1511
|
-
* }]
|
|
1512
|
-
* }
|
|
1275
|
+
* **Important:** `onSuccess` uses inverted logic - it maps SUCCESSFUL HTTP responses
|
|
1276
|
+
* to validation errors. For example, if the API returns `{ available: false }`,
|
|
1277
|
+
* your `onSuccess` should return `{ kind: 'usernameTaken' }`.
|
|
1513
1278
|
*
|
|
1514
|
-
*
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1279
|
+
* @example Username Availability Check (GET)
|
|
1280
|
+
* ```typescript
|
|
1281
|
+
* const checkUsername: HttpCustomValidator<string> = {
|
|
1282
|
+
* request: (ctx) => {
|
|
1283
|
+
* const username = ctx.value();
|
|
1284
|
+
* if (!username) return undefined; // Skip validation if empty
|
|
1285
|
+
* return `/api/users/check-username?username=${encodeURIComponent(username)}`;
|
|
1286
|
+
* },
|
|
1287
|
+
* onSuccess: (response, ctx) => {
|
|
1288
|
+
* // Inverted logic: successful response may indicate validation failure
|
|
1289
|
+
* return response.available ? null : { kind: 'usernameTaken' };
|
|
1290
|
+
* },
|
|
1291
|
+
* onError: (error, ctx) => {
|
|
1292
|
+
* console.error('Availability check failed:', error);
|
|
1293
|
+
* return null; // Don't block form on network errors
|
|
1294
|
+
* }
|
|
1295
|
+
* };
|
|
1519
1296
|
* ```
|
|
1520
1297
|
*
|
|
1521
|
-
* @
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
*
|
|
1526
|
-
*
|
|
1527
|
-
* - `StateLogicConfig`: For field state changes (hidden, readonly, disabled, required)
|
|
1528
|
-
* - `DerivationLogicConfig`: For value derivation (including property derivation via `targetProperty`)
|
|
1529
|
-
*
|
|
1530
|
-
* @public
|
|
1531
|
-
*/
|
|
1532
|
-
type LogicConfig = StateLogicConfig | DerivationLogicConfig;
|
|
1533
|
-
/**
|
|
1534
|
-
* Log level for derivation debug output.
|
|
1298
|
+
* @example Address Validation (POST with Body)
|
|
1299
|
+
* ```typescript
|
|
1300
|
+
* const validateAddress: HttpCustomValidator = {
|
|
1301
|
+
* request: (ctx) => {
|
|
1302
|
+
* const zipCode = ctx.value();
|
|
1303
|
+
* if (!zipCode) return undefined;
|
|
1535
1304
|
*
|
|
1536
|
-
*
|
|
1537
|
-
* -
|
|
1538
|
-
*
|
|
1305
|
+
* return {
|
|
1306
|
+
* url: '/api/validate-address',
|
|
1307
|
+
* method: 'POST',
|
|
1308
|
+
* body: {
|
|
1309
|
+
* street: ctx.valueOf('street' as any),
|
|
1310
|
+
* city: ctx.valueOf('city' as any),
|
|
1311
|
+
* zipCode: zipCode
|
|
1312
|
+
* },
|
|
1313
|
+
* headers: { 'Content-Type': 'application/json' }
|
|
1314
|
+
* };
|
|
1315
|
+
* },
|
|
1316
|
+
* onSuccess: (response) => {
|
|
1317
|
+
* return response.valid ? null : { kind: 'invalidAddress' };
|
|
1318
|
+
* }
|
|
1319
|
+
* };
|
|
1320
|
+
* ```
|
|
1539
1321
|
*
|
|
1540
|
-
* @
|
|
1322
|
+
* @template TValue The type of value stored in the field being validated
|
|
1323
|
+
* @template TResult The type of HTTP response
|
|
1541
1324
|
*/
|
|
1542
|
-
|
|
1325
|
+
interface HttpCustomValidator<TValue = unknown, TResult = unknown> {
|
|
1326
|
+
/**
|
|
1327
|
+
* Function that returns the HTTP request configuration.
|
|
1328
|
+
* Can return:
|
|
1329
|
+
* - `undefined` to skip validation (e.g., if field is empty)
|
|
1330
|
+
* - `string` for simple GET requests (just the URL)
|
|
1331
|
+
* - `HttpResourceRequest` for full control (method, body, headers)
|
|
1332
|
+
*/
|
|
1333
|
+
readonly request: (ctx: FieldContext<TValue>, config?: Record<string, unknown>) => string | HttpResourceRequest | undefined;
|
|
1334
|
+
/**
|
|
1335
|
+
* REQUIRED function to map successful HTTP response to validation errors.
|
|
1336
|
+
*
|
|
1337
|
+
* **Inverted Logic:** This is called on successful HTTP responses.
|
|
1338
|
+
* Return null if validation passes, or ValidationError/ValidationError[] if validation fails.
|
|
1339
|
+
*
|
|
1340
|
+
* Example: API returns `{ available: false }` → return `{ kind: 'usernameTaken' }`
|
|
1341
|
+
*/
|
|
1342
|
+
readonly onSuccess: (result: TResult, ctx: FieldContext<TValue>) => TreeValidationResult;
|
|
1343
|
+
/**
|
|
1344
|
+
* Optional error handler for HTTP errors (network failures, 4xx/5xx status codes).
|
|
1345
|
+
* Return null to ignore the error, or ValidationError to display it to the user.
|
|
1346
|
+
*
|
|
1347
|
+
* Common pattern: Log the error and return null to avoid blocking form submission
|
|
1348
|
+
* on network issues.
|
|
1349
|
+
*/
|
|
1350
|
+
readonly onError?: (error: unknown, ctx: FieldContext<TValue>) => ValidationError$1 | ValidationError$1[] | null;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1543
1353
|
/**
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
* @param condition - The condition to check
|
|
1547
|
-
* @returns true if the condition is a FormStateCondition
|
|
1354
|
+
* Mapping configuration for interpreting HTTP responses as validation results.
|
|
1548
1355
|
*
|
|
1549
|
-
*
|
|
1356
|
+
* Expressions in `validWhen` and `errorParams` are evaluated with scope `{ response }` only —
|
|
1357
|
+
* they do NOT have access to `formValue`, `fieldValue`, or the full `EvaluationContext`.
|
|
1358
|
+
* This is intentional for v1: response mapping is purely about interpreting the HTTP response.
|
|
1550
1359
|
*/
|
|
1551
|
-
|
|
1360
|
+
interface HttpValidationResponseMapping {
|
|
1361
|
+
/**
|
|
1362
|
+
* Expression evaluated with scope `{ response }`. Must evaluate to `true` (strict boolean) to be valid.
|
|
1363
|
+
*
|
|
1364
|
+
* NOTE: This is the OPPOSITE of the function-based HttpCustomValidator.onSuccess
|
|
1365
|
+
* convention, which maps successful HTTP responses to validation errors.
|
|
1366
|
+
* Here, `=== true` means "validation passed". Non-boolean results are treated as invalid and trigger a warning.
|
|
1367
|
+
*/
|
|
1368
|
+
validWhen: string;
|
|
1369
|
+
/** Error kind returned when validation fails — maps to field-level `validationMessages` */
|
|
1370
|
+
errorKind: string;
|
|
1371
|
+
/**
|
|
1372
|
+
* Parameters to include in the validation error for message interpolation.
|
|
1373
|
+
* Map of parameter names to expressions evaluated against `{ response }`.
|
|
1374
|
+
*/
|
|
1375
|
+
errorParams?: Record<string, string>;
|
|
1376
|
+
}
|
|
1552
1377
|
|
|
1553
1378
|
/**
|
|
1554
|
-
*
|
|
1379
|
+
* Base configuration shared by all validators
|
|
1555
1380
|
*/
|
|
1556
|
-
interface
|
|
1557
|
-
/**
|
|
1558
|
-
|
|
1559
|
-
/** Schema identifier or inline schema definition */
|
|
1560
|
-
schema: string | SchemaDefinition;
|
|
1561
|
-
/** Condition for conditional application */
|
|
1562
|
-
condition?: ConditionalExpression;
|
|
1563
|
-
/** Type predicate for applyWhenValue */
|
|
1564
|
-
typePredicate?: string;
|
|
1381
|
+
interface BaseValidatorConfig {
|
|
1382
|
+
/** Conditional logic for when validator applies */
|
|
1383
|
+
when?: ConditionalExpression;
|
|
1565
1384
|
}
|
|
1566
1385
|
/**
|
|
1567
|
-
*
|
|
1386
|
+
* Built-in validator configuration (required, email, min, max, etc.)
|
|
1568
1387
|
*/
|
|
1569
|
-
interface
|
|
1570
|
-
/**
|
|
1571
|
-
|
|
1572
|
-
/**
|
|
1573
|
-
|
|
1574
|
-
/**
|
|
1575
|
-
|
|
1576
|
-
/** Validators to apply */
|
|
1577
|
-
validators?: ValidatorConfig[];
|
|
1578
|
-
/** Logic rules to apply */
|
|
1579
|
-
logic?: LogicConfig[];
|
|
1580
|
-
/** Nested schema applications */
|
|
1581
|
-
subSchemas?: SchemaApplicationConfig[];
|
|
1388
|
+
interface BuiltInValidatorConfig extends BaseValidatorConfig {
|
|
1389
|
+
/** Validator type identifier */
|
|
1390
|
+
type: 'required' | 'email' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern';
|
|
1391
|
+
/** Static value for the validator (e.g., min value, pattern) */
|
|
1392
|
+
value?: number | string | RegExp;
|
|
1393
|
+
/** Dynamic value expression that evaluates to validator parameter */
|
|
1394
|
+
expression?: string;
|
|
1582
1395
|
}
|
|
1583
|
-
|
|
1584
1396
|
/**
|
|
1585
|
-
* Custom validator
|
|
1586
|
-
*
|
|
1587
|
-
* Takes FieldContext (full Angular context) and optional params, returns validation error(s) or null.
|
|
1588
|
-
* Provides access to field state, form hierarchy, and other fields for validation logic.
|
|
1589
|
-
*
|
|
1590
|
-
* **Use FieldContext public APIs to access:**
|
|
1591
|
-
* - Current field value: `ctx.value()`
|
|
1592
|
-
* - Field state: `ctx.state` (errors, touched, dirty, etc.)
|
|
1593
|
-
* - Other field values: `ctx.valueOf(path)` where path is a FieldPath
|
|
1594
|
-
* - Other field states: `ctx.stateOf(path)`
|
|
1595
|
-
* - Other fields: `ctx.fieldTreeOf(path)`
|
|
1596
|
-
* - Current field tree: `ctx.fieldTree`
|
|
1597
|
-
*
|
|
1598
|
-
* **Return Types:**
|
|
1599
|
-
* - Single error: `{ kind: 'errorKind' }` for field-level validation
|
|
1600
|
-
* - Multiple errors: `[{ kind: 'error1' }, { kind: 'error2' }]` for cross-field validation
|
|
1601
|
-
* - No error: `null` when validation passes
|
|
1397
|
+
* Custom validator configuration using Angular's public FieldContext API.
|
|
1398
|
+
* Returns ValidationError | ValidationError[] | null synchronously.
|
|
1602
1399
|
*
|
|
1603
|
-
*
|
|
1604
|
-
*
|
|
1605
|
-
*
|
|
1400
|
+
* Three authoring forms (mutually exclusive at runtime, not enforced by TypeScript):
|
|
1401
|
+
* 1. Registered function: `{ type: 'custom', functionName: 'myValidator' }`
|
|
1402
|
+
* 2. Inline function (code-only): `{ type: 'custom', fn: (ctx) => ... }`
|
|
1403
|
+
* 3. Expression-based: `{ type: 'custom', expression: 'fieldValue === formValue.password', kind: 'passwordMismatch' }`
|
|
1606
1404
|
*
|
|
1607
|
-
*
|
|
1608
|
-
*
|
|
1609
|
-
*
|
|
1610
|
-
*
|
|
1611
|
-
*
|
|
1612
|
-
*
|
|
1613
|
-
* }
|
|
1614
|
-
* return null;
|
|
1615
|
-
* };
|
|
1405
|
+
* Unlike `AsyncValidatorConfig` and `FunctionHttpValidatorConfig` (strict `fn` ↔
|
|
1406
|
+
* `functionName` XOR via discriminated unions), this surface keeps a permissive
|
|
1407
|
+
* interface — the historical expression-vs-functionName split was already
|
|
1408
|
+
* runtime-checked, so adding `fn` follows the same precedent. The runtime
|
|
1409
|
+
* resolver picks one source and warns if `fn` and `functionName` are both set;
|
|
1410
|
+
* inline `fn` wins.
|
|
1616
1411
|
*
|
|
1617
|
-
*
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1412
|
+
* `fn` is NOT JSON-serializable — for code-only configs. For configs loaded from
|
|
1413
|
+
* JSON / OpenAPI / databases, prefer `functionName` to reference a function
|
|
1414
|
+
* registered in `customFnConfig.validators`.
|
|
1415
|
+
*/
|
|
1416
|
+
interface CustomValidatorConfig extends BaseValidatorConfig {
|
|
1417
|
+
/** Validator type identifier */
|
|
1418
|
+
type: 'custom';
|
|
1419
|
+
/** Name of registered validator function (function-based pattern) */
|
|
1420
|
+
functionName?: string;
|
|
1421
|
+
/**
|
|
1422
|
+
* Inline custom validator (code-only authoring).
|
|
1423
|
+
* Mutually exclusive with `functionName`. NOT JSON-serializable.
|
|
1424
|
+
*/
|
|
1425
|
+
fn?: CustomValidator;
|
|
1426
|
+
/** Optional parameters to pass to validator function */
|
|
1427
|
+
params?: Record<string, unknown>;
|
|
1428
|
+
/** JavaScript expression to evaluate (expression-based pattern) */
|
|
1429
|
+
expression?: string;
|
|
1430
|
+
/** Error kind for expression-based validators - links to validationMessages */
|
|
1431
|
+
kind?: string;
|
|
1432
|
+
/**
|
|
1433
|
+
* Parameters to include in error object for message interpolation (expression-based pattern)
|
|
1434
|
+
* Map of parameter names to expressions that will be evaluated in the same context as the validation expression
|
|
1435
|
+
*
|
|
1436
|
+
* @example
|
|
1437
|
+
* errorParams: {
|
|
1438
|
+
* minValue: 'formValue.minValue',
|
|
1439
|
+
* maxValue: 'formValue.maxValue'
|
|
1440
|
+
* }
|
|
1441
|
+
* // Allows message template: "Value must be between {{minValue}} and {{maxValue}}"
|
|
1442
|
+
*/
|
|
1443
|
+
errorParams?: Record<string, string>;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Shared fields for both branches of {@link AsyncValidatorConfig}.
|
|
1626
1447
|
*
|
|
1627
|
-
* @
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1448
|
+
* @internal
|
|
1449
|
+
*/
|
|
1450
|
+
interface AsyncValidatorConfigShared extends BaseValidatorConfig {
|
|
1451
|
+
/** Validator type identifier. */
|
|
1452
|
+
type: 'async';
|
|
1453
|
+
/** Optional parameters to pass to validator function */
|
|
1454
|
+
params?: Record<string, unknown>;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Async custom validator configuration using Angular's `validateAsync` API.
|
|
1458
|
+
* Returns Observable<ValidationError | ValidationError[] | null>.
|
|
1633
1459
|
*
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1460
|
+
* Two mutually exclusive authoring forms:
|
|
1461
|
+
* - `functionName`: name of a function registered in `customFnConfig.asyncValidators`.
|
|
1462
|
+
* JSON-serializable; suitable for configs loaded from APIs, databases, or OpenAPI.
|
|
1463
|
+
* - `fn`: inline async validator. NOT JSON-serializable; for code-only configs.
|
|
1636
1464
|
*
|
|
1637
|
-
*
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1465
|
+
* Exactly one of `functionName` or `fn` must be set.
|
|
1466
|
+
*/
|
|
1467
|
+
type AsyncValidatorConfig = (AsyncValidatorConfigShared & {
|
|
1468
|
+
/** Name of registered async validator function */
|
|
1469
|
+
functionName: string;
|
|
1470
|
+
/** Inline form is forbidden when `functionName` is set */
|
|
1471
|
+
fn?: never;
|
|
1472
|
+
}) | (AsyncValidatorConfigShared & {
|
|
1473
|
+
/**
|
|
1474
|
+
* Inline async validator. Mutually exclusive with `functionName`.
|
|
1475
|
+
*
|
|
1476
|
+
* NOT JSON-serializable — for code-only configs. For configs loaded
|
|
1477
|
+
* from JSON / OpenAPI / databases, use `functionName` to reference
|
|
1478
|
+
* a validator registered in `customFnConfig.asyncValidators`.
|
|
1479
|
+
*/
|
|
1480
|
+
fn: AsyncCustomValidator;
|
|
1481
|
+
/** Registered form is forbidden when `fn` is set */
|
|
1482
|
+
functionName?: never;
|
|
1483
|
+
});
|
|
1484
|
+
/**
|
|
1485
|
+
* Shared fields for both branches of {@link FunctionHttpValidatorConfig}.
|
|
1643
1486
|
*
|
|
1644
|
-
* @
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1487
|
+
* @internal
|
|
1488
|
+
*/
|
|
1489
|
+
interface FunctionHttpValidatorConfigShared extends BaseValidatorConfig {
|
|
1490
|
+
/** Validator type identifier. */
|
|
1491
|
+
type: 'http';
|
|
1492
|
+
/** Optional parameters to pass to HTTP validator */
|
|
1493
|
+
params?: Record<string, unknown>;
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Function-based HTTP validator configuration. Uses Angular's `validateHttp` API.
|
|
1648
1497
|
*
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1498
|
+
* Two mutually exclusive authoring forms:
|
|
1499
|
+
* - `functionName`: name of an HTTP validator registered in `customFnConfig.httpValidators`.
|
|
1500
|
+
* JSON-serializable.
|
|
1501
|
+
* - `fn`: inline HTTP validator. NOT JSON-serializable; for code-only configs.
|
|
1651
1502
|
*
|
|
1652
|
-
*
|
|
1653
|
-
* if (!endDate) errors.push({ kind: 'endDateRequired' });
|
|
1654
|
-
* if (startDate && endDate && startDate > endDate) {
|
|
1655
|
-
* errors.push({ kind: 'invalidDateRange' });
|
|
1656
|
-
* }
|
|
1503
|
+
* Exactly one of `functionName` or `fn` must be set.
|
|
1657
1504
|
*
|
|
1658
|
-
*
|
|
1659
|
-
*
|
|
1660
|
-
|
|
1505
|
+
* Discriminated from `DeclarativeHttpValidatorConfig` by the presence of `functionName`
|
|
1506
|
+
* or `fn` (and absence of `http` + `responseMapping`).
|
|
1507
|
+
*/
|
|
1508
|
+
type FunctionHttpValidatorConfig = (FunctionHttpValidatorConfigShared & {
|
|
1509
|
+
/** Name of registered HTTP validator configuration */
|
|
1510
|
+
functionName: string;
|
|
1511
|
+
/** Inline form is forbidden when `functionName` is set */
|
|
1512
|
+
fn?: never;
|
|
1513
|
+
}) | (FunctionHttpValidatorConfigShared & {
|
|
1514
|
+
/**
|
|
1515
|
+
* Inline HTTP validator. Mutually exclusive with `functionName`.
|
|
1516
|
+
*
|
|
1517
|
+
* NOT JSON-serializable — for code-only configs. For configs loaded
|
|
1518
|
+
* from JSON / OpenAPI / databases, use `functionName` to reference
|
|
1519
|
+
* an HTTP validator registered in `customFnConfig.httpValidators`.
|
|
1520
|
+
*/
|
|
1521
|
+
fn: HttpCustomValidator;
|
|
1522
|
+
/** Registered form is forbidden when `fn` is set */
|
|
1523
|
+
functionName?: never;
|
|
1524
|
+
});
|
|
1525
|
+
/**
|
|
1526
|
+
* Declarative HTTP validator configuration — fully JSON-serializable, no function registration required.
|
|
1661
1527
|
*
|
|
1662
|
-
*
|
|
1663
|
-
*
|
|
1664
|
-
* 2. Form-level `defaultValidationMessages[kind]` (fallback for common messages)
|
|
1665
|
-
* 3. **No message configured = Warning logged + error NOT displayed**
|
|
1528
|
+
* Uses `HttpRequestConfig` to define the HTTP request and `HttpValidationResponseMapping`
|
|
1529
|
+
* to interpret the response as a validation result. Powered by Angular's `validateHttp` API.
|
|
1666
1530
|
*
|
|
1667
|
-
*
|
|
1531
|
+
* Discriminated from `FunctionHttpValidatorConfig` by the presence of `http` + `responseMapping`
|
|
1532
|
+
* (and absence of `functionName`).
|
|
1533
|
+
*/
|
|
1534
|
+
interface DeclarativeHttpValidatorConfig extends BaseValidatorConfig {
|
|
1535
|
+
/** Validator type identifier */
|
|
1536
|
+
type: 'http';
|
|
1537
|
+
/** HTTP request configuration with expression-based query params and body */
|
|
1538
|
+
http: HttpRequestConfig;
|
|
1539
|
+
/** Mapping that interprets the HTTP response as a validation result */
|
|
1540
|
+
responseMapping: HttpValidationResponseMapping;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Configuration for signal forms validator functions that can be serialized from API.
|
|
1544
|
+
* Discriminated union type for type-safe validator configuration.
|
|
1668
1545
|
*
|
|
1669
|
-
*
|
|
1546
|
+
* Note: `FunctionHttpValidatorConfig` and `DeclarativeHttpValidatorConfig` both use `type: 'http'`.
|
|
1547
|
+
* They are discriminated by property presence: `functionName` → function-based, `http` → declarative.
|
|
1548
|
+
* Use `isFunctionHttpValidator()` for type-safe narrowing when `type` alone is insufficient.
|
|
1670
1549
|
*/
|
|
1671
|
-
type
|
|
1550
|
+
type ValidatorConfig = BuiltInValidatorConfig | CustomValidatorConfig | AsyncValidatorConfig | FunctionHttpValidatorConfig | DeclarativeHttpValidatorConfig;
|
|
1551
|
+
|
|
1672
1552
|
/**
|
|
1673
|
-
*
|
|
1553
|
+
* Special form-state conditions for button disabled logic.
|
|
1674
1554
|
*
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
1555
|
+
* These conditions evaluate form or page-level state rather than field values,
|
|
1556
|
+
* and are primarily used for controlling button disabled states.
|
|
1677
1557
|
*
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
1680
|
-
*
|
|
1681
|
-
*
|
|
1682
|
-
*
|
|
1558
|
+
* @example
|
|
1559
|
+
* ```typescript
|
|
1560
|
+
* // Disable submit button when form is invalid or submitting
|
|
1561
|
+
* {
|
|
1562
|
+
* key: 'submit',
|
|
1563
|
+
* type: 'submit',
|
|
1564
|
+
* label: 'Submit',
|
|
1565
|
+
* logic: [
|
|
1566
|
+
* { type: 'disabled', condition: 'formInvalid' },
|
|
1567
|
+
* { type: 'disabled', condition: 'formSubmitting' },
|
|
1568
|
+
* ]
|
|
1569
|
+
* }
|
|
1683
1570
|
*
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1571
|
+
* // Disable next button when current page is invalid
|
|
1572
|
+
* nextButton({
|
|
1573
|
+
* key: 'next',
|
|
1574
|
+
* label: 'Next',
|
|
1575
|
+
* logic: [
|
|
1576
|
+
* { type: 'disabled', condition: 'pageInvalid' },
|
|
1577
|
+
* ]
|
|
1578
|
+
* })
|
|
1579
|
+
* ```
|
|
1688
1580
|
*
|
|
1689
|
-
*
|
|
1690
|
-
|
|
1581
|
+
* @public
|
|
1582
|
+
*/
|
|
1583
|
+
type FormStateCondition =
|
|
1584
|
+
/** True when form.valid() === false */
|
|
1585
|
+
'formInvalid'
|
|
1586
|
+
/** True when form.submitting() === true */
|
|
1587
|
+
| 'formSubmitting'
|
|
1588
|
+
/** True when fields on the current page are invalid (for paged forms) */
|
|
1589
|
+
| 'pageInvalid';
|
|
1590
|
+
/**
|
|
1591
|
+
* Logic type for controlling field state (hidden, readonly, disabled, required).
|
|
1691
1592
|
*
|
|
1692
|
-
* @
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
*
|
|
1697
|
-
* const injector = inject(Injector);
|
|
1698
|
-
* return resource({
|
|
1699
|
-
* request: () => params(),
|
|
1700
|
-
* loader: ({ request }) => {
|
|
1701
|
-
* if (!request?.username) return null;
|
|
1702
|
-
* const service = injector.get(UserService);
|
|
1703
|
-
* return firstValueFrom(service.checkAvailability(request.username));
|
|
1704
|
-
* }
|
|
1705
|
-
* });
|
|
1706
|
-
* },
|
|
1707
|
-
* onSuccess: (result, ctx) => {
|
|
1708
|
-
* if (!result) return null;
|
|
1709
|
-
* return result.available ? null : { kind: 'usernameTaken' };
|
|
1710
|
-
* },
|
|
1711
|
-
* onError: (error, ctx) => {
|
|
1712
|
-
* console.error('Availability check failed:', error);
|
|
1713
|
-
* return null; // Don't block form on network errors
|
|
1714
|
-
* }
|
|
1715
|
-
* };
|
|
1716
|
-
* ```
|
|
1593
|
+
* @public
|
|
1594
|
+
*/
|
|
1595
|
+
type StateLogicType = 'hidden' | 'readonly' | 'disabled' | 'required';
|
|
1596
|
+
/**
|
|
1597
|
+
* Base configuration for conditional field state logic.
|
|
1717
1598
|
*
|
|
1718
|
-
* @
|
|
1719
|
-
* @template TParams The type of params passed to the resource
|
|
1720
|
-
* @template TResult The type of result returned by the resource
|
|
1599
|
+
* @internal
|
|
1721
1600
|
*/
|
|
1722
|
-
interface
|
|
1723
|
-
/**
|
|
1724
|
-
* Function that receives field context and returns resource params.
|
|
1725
|
-
* Params will be tracked as a signal and trigger resource reload when changed.
|
|
1726
|
-
*/
|
|
1727
|
-
readonly params: (ctx: FieldContext<TValue>, config?: Record<string, unknown>) => TParams;
|
|
1601
|
+
interface BaseStateLogicConfig {
|
|
1728
1602
|
/**
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1603
|
+
* Logic type identifier for field state.
|
|
1604
|
+
*
|
|
1605
|
+
* - `hidden`: Hide the field from view (still participates in form state)
|
|
1606
|
+
* - `readonly`: Make the field read-only
|
|
1607
|
+
* - `disabled`: Disable user interaction
|
|
1608
|
+
* - `required`: Make the field required
|
|
1731
1609
|
*/
|
|
1732
|
-
|
|
1610
|
+
type: StateLogicType;
|
|
1733
1611
|
/**
|
|
1734
|
-
*
|
|
1735
|
-
*
|
|
1612
|
+
* Condition that determines when this logic applies.
|
|
1613
|
+
*
|
|
1614
|
+
* Can be:
|
|
1615
|
+
* - `boolean`: Static value (always applies or never applies)
|
|
1616
|
+
* - `ConditionalExpression`: Expression evaluated against field/form values
|
|
1617
|
+
* - `FormStateCondition`: Special form/page state check (for buttons)
|
|
1618
|
+
*
|
|
1619
|
+
* @example
|
|
1620
|
+
* ```typescript
|
|
1621
|
+
* // Static condition
|
|
1622
|
+
* condition: true
|
|
1623
|
+
*
|
|
1624
|
+
* // Field value condition
|
|
1625
|
+
* condition: {
|
|
1626
|
+
* type: 'fieldValue',
|
|
1627
|
+
* fieldPath: 'status',
|
|
1628
|
+
* operator: 'equals',
|
|
1629
|
+
* value: 'locked'
|
|
1630
|
+
* }
|
|
1631
|
+
*
|
|
1632
|
+
* // Form state condition (for buttons)
|
|
1633
|
+
* condition: 'formSubmitting'
|
|
1634
|
+
* ```
|
|
1736
1635
|
*/
|
|
1737
|
-
|
|
1636
|
+
condition: ConditionalExpression | boolean | FormStateCondition;
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* State logic that evaluates immediately on change (default).
|
|
1640
|
+
*
|
|
1641
|
+
* @internal
|
|
1642
|
+
*/
|
|
1643
|
+
interface ImmediateStateLogicConfig extends BaseStateLogicConfig {
|
|
1738
1644
|
/**
|
|
1739
|
-
*
|
|
1740
|
-
*
|
|
1645
|
+
* Trigger for immediate evaluation.
|
|
1646
|
+
* @default 'onChange'
|
|
1741
1647
|
*/
|
|
1742
|
-
|
|
1648
|
+
trigger?: 'onChange';
|
|
1649
|
+
/** Not allowed for onChange trigger */
|
|
1650
|
+
debounceMs?: never;
|
|
1743
1651
|
}
|
|
1744
1652
|
/**
|
|
1745
|
-
*
|
|
1653
|
+
* State logic that evaluates after a debounce period.
|
|
1654
|
+
*
|
|
1655
|
+
* @internal
|
|
1746
1656
|
*/
|
|
1747
|
-
interface
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1657
|
+
interface DebouncedStateLogicConfig extends BaseStateLogicConfig {
|
|
1658
|
+
/**
|
|
1659
|
+
* Trigger for debounced evaluation.
|
|
1660
|
+
* Evaluates after the value has stabilized for the debounce duration.
|
|
1661
|
+
*/
|
|
1662
|
+
trigger: 'debounced';
|
|
1663
|
+
/**
|
|
1664
|
+
* Debounce duration in milliseconds.
|
|
1665
|
+
* @default 500
|
|
1666
|
+
*/
|
|
1667
|
+
debounceMs?: number;
|
|
1752
1668
|
}
|
|
1753
1669
|
/**
|
|
1754
|
-
*
|
|
1755
|
-
*
|
|
1756
|
-
* Angular's validateHttp provides optimized HTTP validation with automatic
|
|
1757
|
-
* request cancellation and integration with the resource API.
|
|
1758
|
-
*
|
|
1759
|
-
* **Benefits:**
|
|
1760
|
-
* - Automatic request cancellation when field value changes
|
|
1761
|
-
* - Built-in integration with Angular's resource management
|
|
1762
|
-
* - Simpler than AsyncCustomValidator for HTTP use cases
|
|
1763
|
-
*
|
|
1764
|
-
* **Structure:**
|
|
1765
|
-
* - `request`: Function that returns URL string or HttpResourceRequest
|
|
1766
|
-
* - `onSuccess`: REQUIRED - Maps HTTP response to validation errors (inverted logic!)
|
|
1767
|
-
* - `onError`: Optional handler for HTTP errors (network failures, 4xx/5xx)
|
|
1670
|
+
* Configuration for conditional field state logic.
|
|
1768
1671
|
*
|
|
1769
|
-
*
|
|
1770
|
-
*
|
|
1771
|
-
*
|
|
1672
|
+
* Defines how field behavior changes based on conditions.
|
|
1673
|
+
* Supports hiding, disabling, making readonly, or requiring fields
|
|
1674
|
+
* based on form state or field values.
|
|
1772
1675
|
*
|
|
1773
|
-
* @example
|
|
1676
|
+
* @example
|
|
1774
1677
|
* ```typescript
|
|
1775
|
-
*
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
1778
|
-
*
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1783
|
-
* return response.available ? null : { kind: 'usernameTaken' };
|
|
1784
|
-
* },
|
|
1785
|
-
* onError: (error, ctx) => {
|
|
1786
|
-
* console.error('Availability check failed:', error);
|
|
1787
|
-
* return null; // Don't block form on network errors
|
|
1678
|
+
* // Hide email field when contact method is not email
|
|
1679
|
+
* {
|
|
1680
|
+
* type: 'hidden',
|
|
1681
|
+
* condition: {
|
|
1682
|
+
* type: 'fieldValue',
|
|
1683
|
+
* fieldPath: 'contactMethod',
|
|
1684
|
+
* operator: 'notEquals',
|
|
1685
|
+
* value: 'email'
|
|
1788
1686
|
* }
|
|
1789
|
-
* }
|
|
1790
|
-
* ```
|
|
1687
|
+
* }
|
|
1791
1688
|
*
|
|
1792
|
-
*
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
1795
|
-
*
|
|
1796
|
-
*
|
|
1797
|
-
* if (!zipCode) return undefined;
|
|
1689
|
+
* // Disable button when form is submitting
|
|
1690
|
+
* {
|
|
1691
|
+
* type: 'disabled',
|
|
1692
|
+
* condition: 'formSubmitting'
|
|
1693
|
+
* }
|
|
1798
1694
|
*
|
|
1799
|
-
*
|
|
1800
|
-
*
|
|
1801
|
-
*
|
|
1802
|
-
*
|
|
1803
|
-
*
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
1808
|
-
* };
|
|
1809
|
-
* },
|
|
1810
|
-
* onSuccess: (response) => {
|
|
1811
|
-
* return response.valid ? null : { kind: 'invalidAddress' };
|
|
1695
|
+
* // Debounced visibility (avoids flicker during rapid typing)
|
|
1696
|
+
* {
|
|
1697
|
+
* type: 'hidden',
|
|
1698
|
+
* trigger: 'debounced',
|
|
1699
|
+
* debounceMs: 300,
|
|
1700
|
+
* condition: {
|
|
1701
|
+
* type: 'fieldValue',
|
|
1702
|
+
* fieldPath: 'search',
|
|
1703
|
+
* operator: 'isEmpty'
|
|
1812
1704
|
* }
|
|
1813
|
-
* }
|
|
1705
|
+
* }
|
|
1814
1706
|
* ```
|
|
1815
1707
|
*
|
|
1816
|
-
* @
|
|
1817
|
-
|
|
1708
|
+
* @public
|
|
1709
|
+
*/
|
|
1710
|
+
type StateLogicConfig = ImmediateStateLogicConfig | DebouncedStateLogicConfig;
|
|
1711
|
+
/**
|
|
1712
|
+
* Shared fields that appear on all derivation logic config variants.
|
|
1713
|
+
*
|
|
1714
|
+
* @internal
|
|
1818
1715
|
*/
|
|
1819
|
-
interface
|
|
1716
|
+
interface SharedDerivationFields {
|
|
1820
1717
|
/**
|
|
1821
|
-
*
|
|
1822
|
-
* Can return:
|
|
1823
|
-
* - `undefined` to skip validation (e.g., if field is empty)
|
|
1824
|
-
* - `string` for simple GET requests (just the URL)
|
|
1825
|
-
* - `HttpResourceRequest` for full control (method, body, headers)
|
|
1718
|
+
* Logic type identifier for value derivation.
|
|
1826
1719
|
*/
|
|
1827
|
-
|
|
1720
|
+
type: 'derivation';
|
|
1828
1721
|
/**
|
|
1829
|
-
*
|
|
1722
|
+
* Target property name for property derivation.
|
|
1830
1723
|
*
|
|
1831
|
-
*
|
|
1832
|
-
*
|
|
1724
|
+
* When set, this derivation targets a field property (like `minDate`, `options`,
|
|
1725
|
+
* `label`, `placeholder`) instead of the field's value. The derivation is routed
|
|
1726
|
+
* to the property derivation pipeline.
|
|
1833
1727
|
*
|
|
1834
|
-
*
|
|
1835
|
-
*/
|
|
1836
|
-
readonly onSuccess: (result: TResult, ctx: FieldContext<TValue>) => TreeValidationResult;
|
|
1837
|
-
/**
|
|
1838
|
-
* Optional error handler for HTTP errors (network failures, 4xx/5xx status codes).
|
|
1839
|
-
* Return null to ignore the error, or ValidationError to display it to the user.
|
|
1728
|
+
* When absent, the derivation targets the field's value (default behavior).
|
|
1840
1729
|
*
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1730
|
+
* **Depth limit (max 2 levels):** Only simple and single-dot-nested paths are
|
|
1731
|
+
* supported. Paths with 2+ dots (e.g., `'a.b.c'`) will throw at runtime.
|
|
1732
|
+
*
|
|
1733
|
+
* @example
|
|
1734
|
+
* ```typescript
|
|
1735
|
+
* // Property derivation (new unified API)
|
|
1736
|
+
* {
|
|
1737
|
+
* key: 'endDate',
|
|
1738
|
+
* type: 'datepicker',
|
|
1739
|
+
* logic: [{
|
|
1740
|
+
* type: 'derivation',
|
|
1741
|
+
* targetProperty: 'minDate',
|
|
1742
|
+
* expression: 'formValue.startDate'
|
|
1743
|
+
* }]
|
|
1744
|
+
* }
|
|
1745
|
+
*
|
|
1746
|
+
* // Value derivation (no targetProperty — existing behavior)
|
|
1747
|
+
* {
|
|
1748
|
+
* key: 'total',
|
|
1749
|
+
* type: 'input',
|
|
1750
|
+
* logic: [{
|
|
1751
|
+
* type: 'derivation',
|
|
1752
|
+
* expression: 'formValue.quantity * formValue.unitPrice'
|
|
1753
|
+
* }]
|
|
1754
|
+
* }
|
|
1755
|
+
* ```
|
|
1843
1756
|
*/
|
|
1844
|
-
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
/**
|
|
1848
|
-
* Interface for dynamic forms logger implementations.
|
|
1849
|
-
* Allows custom logging integrations (Sentry, DataDog, etc.)
|
|
1850
|
-
*/
|
|
1851
|
-
interface Logger {
|
|
1757
|
+
targetProperty?: string;
|
|
1852
1758
|
/**
|
|
1853
|
-
*
|
|
1854
|
-
*
|
|
1759
|
+
* Optional name for this derivation for debugging purposes.
|
|
1760
|
+
*
|
|
1761
|
+
* When provided, this name appears in derivation debug logs,
|
|
1762
|
+
* making it easier to identify specific derivations in complex forms.
|
|
1763
|
+
*
|
|
1764
|
+
* @example
|
|
1765
|
+
* ```typescript
|
|
1766
|
+
* {
|
|
1767
|
+
* key: 'lineTotal',
|
|
1768
|
+
* type: 'input',
|
|
1769
|
+
* logic: [{
|
|
1770
|
+
* type: 'derivation',
|
|
1771
|
+
* debugName: 'Calculate line total',
|
|
1772
|
+
* expression: 'formValue.quantity * formValue.unitPrice'
|
|
1773
|
+
* }]
|
|
1774
|
+
* }
|
|
1775
|
+
* ```
|
|
1855
1776
|
*/
|
|
1856
|
-
|
|
1777
|
+
debugName?: string;
|
|
1857
1778
|
/**
|
|
1858
|
-
*
|
|
1859
|
-
*
|
|
1779
|
+
* Condition that determines when this derivation applies.
|
|
1780
|
+
*
|
|
1781
|
+
* Can be:
|
|
1782
|
+
* - `boolean`: Static value (always applies or never applies)
|
|
1783
|
+
* - `ConditionalExpression`: Expression evaluated against field/form values
|
|
1784
|
+
*
|
|
1785
|
+
* Defaults to `true` (always apply).
|
|
1786
|
+
*
|
|
1787
|
+
* Note: FormStateCondition is not supported for derivations.
|
|
1788
|
+
*
|
|
1789
|
+
* @example
|
|
1790
|
+
* ```typescript
|
|
1791
|
+
* // Always compute (default)
|
|
1792
|
+
* condition: true
|
|
1793
|
+
*
|
|
1794
|
+
* // Conditional derivation
|
|
1795
|
+
* condition: {
|
|
1796
|
+
* type: 'fieldValue',
|
|
1797
|
+
* fieldPath: 'country',
|
|
1798
|
+
* operator: 'equals',
|
|
1799
|
+
* value: 'USA'
|
|
1800
|
+
* }
|
|
1801
|
+
* ```
|
|
1860
1802
|
*/
|
|
1861
|
-
|
|
1803
|
+
condition?: ConditionalExpression | boolean;
|
|
1862
1804
|
/**
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
1805
|
+
* When true, the derivation stops running after the user manually
|
|
1806
|
+
* edits the target field.
|
|
1807
|
+
*
|
|
1808
|
+
* This is useful for "smart defaults" — values that should be
|
|
1809
|
+
* auto-filled initially but respected once the user explicitly changes them.
|
|
1810
|
+
*
|
|
1811
|
+
* Uses the field's `dirty()` signal to detect user modification.
|
|
1812
|
+
* Derivations write directly to `value.set()` which does not trigger
|
|
1813
|
+
* `markAsDirty()`, so `dirty === true` reliably indicates a user edit.
|
|
1814
|
+
*
|
|
1815
|
+
* @example
|
|
1816
|
+
* ```typescript
|
|
1817
|
+
* // Auto-fill display name from first + last name, but stop if user edits it
|
|
1818
|
+
* {
|
|
1819
|
+
* key: 'displayName',
|
|
1820
|
+
* logic: [{
|
|
1821
|
+
* type: 'derivation',
|
|
1822
|
+
* expression: 'formValue.firstName + " " + formValue.lastName',
|
|
1823
|
+
* stopOnUserOverride: true
|
|
1824
|
+
* }]
|
|
1825
|
+
* }
|
|
1826
|
+
* ```
|
|
1865
1827
|
*/
|
|
1866
|
-
|
|
1828
|
+
stopOnUserOverride?: boolean;
|
|
1867
1829
|
/**
|
|
1868
|
-
*
|
|
1869
|
-
*
|
|
1830
|
+
* When true (and `stopOnUserOverride` is also true), clears the
|
|
1831
|
+
* user-override flag when any dependency of this derivation changes,
|
|
1832
|
+
* allowing the derivation to run again.
|
|
1833
|
+
*
|
|
1834
|
+
* This is useful when a user override should only persist until the
|
|
1835
|
+
* underlying data changes — e.g., when switching countries, the
|
|
1836
|
+
* phone prefix should re-derive even if the user previously edited it.
|
|
1837
|
+
*
|
|
1838
|
+
* @example
|
|
1839
|
+
* ```typescript
|
|
1840
|
+
* {
|
|
1841
|
+
* key: 'phonePrefix',
|
|
1842
|
+
* logic: [{
|
|
1843
|
+
* type: 'derivation',
|
|
1844
|
+
* value: '+1',
|
|
1845
|
+
* condition: { type: 'fieldValue', fieldPath: 'country', operator: 'equals', value: 'USA' },
|
|
1846
|
+
* stopOnUserOverride: true,
|
|
1847
|
+
* reEngageOnDependencyChange: true,
|
|
1848
|
+
* dependsOn: ['country']
|
|
1849
|
+
* }]
|
|
1850
|
+
* }
|
|
1851
|
+
* ```
|
|
1870
1852
|
*/
|
|
1871
|
-
|
|
1853
|
+
reEngageOnDependencyChange?: boolean;
|
|
1872
1854
|
}
|
|
1873
|
-
|
|
1874
1855
|
/**
|
|
1875
|
-
*
|
|
1876
|
-
*
|
|
1877
|
-
* Used by both the derivation-warning tracker (keys are field paths) and the
|
|
1878
|
-
* deprecation-warning tracker (keys are deprecation IDs). The property is
|
|
1879
|
-
* generic so the same shape works for any string-keyed warning domain.
|
|
1880
|
-
*
|
|
1856
|
+
* Trigger variants for derivation timing.
|
|
1881
1857
|
* @internal
|
|
1882
1858
|
*/
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1859
|
+
type ImmediateDerivationTrigger = {
|
|
1860
|
+
trigger?: 'onChange';
|
|
1861
|
+
debounceMs?: never;
|
|
1862
|
+
};
|
|
1863
|
+
type DebouncedDerivationTrigger = {
|
|
1864
|
+
trigger: 'debounced';
|
|
1865
|
+
debounceMs?: number;
|
|
1866
|
+
};
|
|
1887
1867
|
/**
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1890
|
-
* Used in evaluation contexts to allow expressions and conditions
|
|
1891
|
-
* to reference field state (e.g., `fieldState.touched`, `formFieldState.email.dirty`).
|
|
1868
|
+
* Base for HTTP derivations. `source: 'http'` is required.
|
|
1869
|
+
* TypeScript enforces `dependsOn` and `responseExpression` at the type level.
|
|
1892
1870
|
*
|
|
1893
|
-
* @
|
|
1871
|
+
* @internal
|
|
1894
1872
|
*/
|
|
1895
|
-
interface
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
/**
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
readonly pending: boolean;
|
|
1873
|
+
interface HttpDerivationBase extends SharedDerivationFields {
|
|
1874
|
+
/** Identifies this derivation as HTTP-driven. */
|
|
1875
|
+
source: 'http';
|
|
1876
|
+
/** Forbidden on HTTP source */
|
|
1877
|
+
fn?: never;
|
|
1878
|
+
/** Forbidden on HTTP source */
|
|
1879
|
+
asyncFn?: never;
|
|
1903
1880
|
/**
|
|
1904
|
-
*
|
|
1881
|
+
* HTTP request configuration for server-driven derivations.
|
|
1905
1882
|
*
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
1883
|
+
* The request is sent when dependencies change, with automatic
|
|
1884
|
+
* debouncing and cancellation of in-flight requests.
|
|
1885
|
+
*
|
|
1886
|
+
* Configure debounce via the `trigger: 'debounced'` + `debounceMs` mechanism.
|
|
1887
|
+
*
|
|
1888
|
+
* @example
|
|
1889
|
+
* ```typescript
|
|
1890
|
+
* {
|
|
1891
|
+
* key: 'exchangeRate',
|
|
1892
|
+
* logic: [{
|
|
1893
|
+
* type: 'derivation',
|
|
1894
|
+
* source: 'http',
|
|
1895
|
+
* http: {
|
|
1896
|
+
* url: '/api/exchange-rate',
|
|
1897
|
+
* method: 'GET',
|
|
1898
|
+
* queryParams: {
|
|
1899
|
+
* from: 'formValue.sourceCurrency',
|
|
1900
|
+
* to: 'formValue.targetCurrency',
|
|
1901
|
+
* },
|
|
1902
|
+
* },
|
|
1903
|
+
* responseExpression: 'response.rate',
|
|
1904
|
+
* dependsOn: ['sourceCurrency', 'targetCurrency'],
|
|
1905
|
+
* }]
|
|
1906
|
+
* }
|
|
1907
|
+
* ```
|
|
1908
1908
|
*/
|
|
1909
|
-
|
|
1909
|
+
http: HttpRequestConfig;
|
|
1910
1910
|
/**
|
|
1911
|
-
*
|
|
1911
|
+
* Explicit field dependencies. Required for HTTP derivations to prevent
|
|
1912
|
+
* wildcard triggering on every keystroke.
|
|
1912
1913
|
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
1914
|
+
* The wildcard token `'*'` is rejected. The structural token `'$group'`
|
|
1915
|
+
* (and `'$group.X'`) is allowed and resolves to the field's parent container
|
|
1916
|
+
* path — be aware this fires whenever any sibling under that group changes,
|
|
1917
|
+
* so the caller owns the request frequency (consider `trigger: 'debounced'`).
|
|
1915
1918
|
*/
|
|
1916
|
-
|
|
1919
|
+
dependsOn: string[];
|
|
1917
1920
|
/**
|
|
1918
|
-
*
|
|
1921
|
+
* Expression to extract the derived value from the HTTP response.
|
|
1919
1922
|
*
|
|
1920
|
-
*
|
|
1921
|
-
*
|
|
1923
|
+
* Evaluated via `ExpressionParser` with `{ response }` as the evaluation scope.
|
|
1924
|
+
*
|
|
1925
|
+
* @example
|
|
1926
|
+
* ```typescript
|
|
1927
|
+
* responseExpression: 'response.rate'
|
|
1928
|
+
* responseExpression: 'response.data.suggestedPrice'
|
|
1929
|
+
* ```
|
|
1922
1930
|
*/
|
|
1923
|
-
|
|
1931
|
+
responseExpression: string;
|
|
1932
|
+
value?: never;
|
|
1933
|
+
expression?: never;
|
|
1934
|
+
functionName?: never;
|
|
1935
|
+
asyncFunctionName?: never;
|
|
1924
1936
|
}
|
|
1925
1937
|
/**
|
|
1926
|
-
*
|
|
1938
|
+
* Shared fields for both branches of {@link AsyncFunctionDerivationBase}.
|
|
1927
1939
|
*
|
|
1928
|
-
*
|
|
1929
|
-
* state of any field in the form by key.
|
|
1930
|
-
*
|
|
1931
|
-
* @public
|
|
1940
|
+
* @internal
|
|
1932
1941
|
*/
|
|
1933
|
-
|
|
1942
|
+
interface AsyncFunctionDerivationShared extends SharedDerivationFields {
|
|
1943
|
+
/** Identifies this derivation as async-function-driven. */
|
|
1944
|
+
source: 'asyncFunction';
|
|
1945
|
+
/** Forbidden on asyncFunction source */
|
|
1946
|
+
fn?: never;
|
|
1947
|
+
/**
|
|
1948
|
+
* Explicit field dependencies. Required for async derivations to prevent
|
|
1949
|
+
* wildcard triggering on every form change.
|
|
1950
|
+
*
|
|
1951
|
+
* The wildcard token `'*'` is rejected. The structural token `'$group'`
|
|
1952
|
+
* (and `'$group.X'`) is allowed and resolves to the field's parent container
|
|
1953
|
+
* path — be aware this fires whenever any sibling under that group changes,
|
|
1954
|
+
* so the caller owns the invocation frequency (handle this via
|
|
1955
|
+
* `trigger: 'debounced'` or via debouncing inside the async function).
|
|
1956
|
+
*/
|
|
1957
|
+
dependsOn: string[];
|
|
1958
|
+
value?: never;
|
|
1959
|
+
expression?: never;
|
|
1960
|
+
functionName?: never;
|
|
1961
|
+
http?: never;
|
|
1962
|
+
responseExpression?: never;
|
|
1963
|
+
}
|
|
1934
1964
|
/**
|
|
1935
|
-
*
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1965
|
+
* Base for async function derivations. `source: 'asyncFunction'` is required.
|
|
1966
|
+
* TypeScript enforces `dependsOn` at the type level, and `asyncFunctionName` /
|
|
1967
|
+
* `asyncFn` are mutually exclusive (XOR).
|
|
1938
1968
|
*
|
|
1939
|
-
* @
|
|
1969
|
+
* @internal
|
|
1940
1970
|
*/
|
|
1941
|
-
type
|
|
1942
|
-
|
|
1943
|
-
interface EvaluationContext<TValue = unknown, TFormValue extends Record<string, unknown> = any> {
|
|
1944
|
-
/** Current field value */
|
|
1945
|
-
fieldValue: TValue;
|
|
1971
|
+
type AsyncFunctionDerivationBase = (AsyncFunctionDerivationShared & {
|
|
1946
1972
|
/**
|
|
1947
|
-
*
|
|
1973
|
+
* Name of a registered async derivation function.
|
|
1948
1974
|
*
|
|
1949
|
-
*
|
|
1950
|
-
*
|
|
1951
|
-
*
|
|
1975
|
+
* The function receives the evaluation context and returns a Promise or Observable
|
|
1976
|
+
* of the derived value. Register functions in `customFnConfig.asyncDerivations`.
|
|
1977
|
+
*
|
|
1978
|
+
* @example
|
|
1979
|
+
* ```typescript
|
|
1980
|
+
* {
|
|
1981
|
+
* key: 'suggestedPrice',
|
|
1982
|
+
* logic: [{
|
|
1983
|
+
* type: 'derivation',
|
|
1984
|
+
* source: 'asyncFunction',
|
|
1985
|
+
* asyncFunctionName: 'fetchSuggestedPrice',
|
|
1986
|
+
* dependsOn: ['productId', 'quantity'],
|
|
1987
|
+
* }]
|
|
1988
|
+
* }
|
|
1989
|
+
* ```
|
|
1952
1990
|
*/
|
|
1953
|
-
|
|
1954
|
-
/**
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
customFunctions?: Record<string, (context: EvaluationContext) => unknown>;
|
|
1958
|
-
/** Logger for diagnostic output */
|
|
1959
|
-
logger: Logger;
|
|
1991
|
+
asyncFunctionName: string;
|
|
1992
|
+
/** Inline form is forbidden when `asyncFunctionName` is set */
|
|
1993
|
+
asyncFn?: never;
|
|
1994
|
+
}) | (AsyncFunctionDerivationShared & {
|
|
1960
1995
|
/**
|
|
1961
|
-
*
|
|
1962
|
-
* field has no inner group above the array boundary).
|
|
1996
|
+
* Inline async derivation function. Mutually exclusive with `asyncFunctionName`.
|
|
1963
1997
|
*
|
|
1964
|
-
*
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
1967
|
-
|
|
1998
|
+
* NOT JSON-serializable — for code-only configs. For configs loaded
|
|
1999
|
+
* from JSON / OpenAPI / databases, use `asyncFunctionName` to reference
|
|
2000
|
+
* a function registered in `customFnConfig.asyncDerivations`.
|
|
2001
|
+
*/
|
|
2002
|
+
asyncFn: AsyncDerivationFunction;
|
|
2003
|
+
/** Registered form is forbidden when `asyncFn` is set */
|
|
2004
|
+
asyncFunctionName?: never;
|
|
2005
|
+
});
|
|
2006
|
+
/**
|
|
2007
|
+
* Base for expression derivations.
|
|
2008
|
+
*
|
|
2009
|
+
* @internal
|
|
2010
|
+
*/
|
|
2011
|
+
interface ExpressionDerivationBase extends SharedDerivationFields {
|
|
2012
|
+
source?: never;
|
|
2013
|
+
/** Forbidden on expression mode */
|
|
2014
|
+
fn?: never;
|
|
2015
|
+
/** Forbidden on expression mode */
|
|
2016
|
+
asyncFn?: never;
|
|
2017
|
+
/**
|
|
2018
|
+
* JavaScript expression to evaluate for the derived value.
|
|
1968
2019
|
*
|
|
1969
|
-
*
|
|
1970
|
-
*
|
|
1971
|
-
*
|
|
1972
|
-
* - Inside nested groups: the innermost parent group's value object.
|
|
1973
|
-
* - Directly inside an array item (no inner group): the array item itself
|
|
1974
|
-
* (same shape as `formValue` in array context).
|
|
1975
|
-
* - Inside a group inside an array item: the inner group's value object
|
|
1976
|
-
* within the current item.
|
|
1977
|
-
* - Field at form root with no parent container: `undefined`.
|
|
2020
|
+
* Has access to `formValue` object containing all form values.
|
|
2021
|
+
* For array fields, `formValue` is scoped to the current array item.
|
|
2022
|
+
* Uses the same secure AST-based parser as other expressions.
|
|
1978
2023
|
*
|
|
1979
2024
|
* @example
|
|
1980
2025
|
* ```typescript
|
|
1981
|
-
*
|
|
1982
|
-
*
|
|
1983
|
-
*
|
|
1984
|
-
* return country === 'usa' ? 'NY' : '';
|
|
1985
|
-
* }
|
|
2026
|
+
* expression: 'formValue.quantity * formValue.unitPrice'
|
|
2027
|
+
* expression: 'formValue.firstName + " " + formValue.lastName'
|
|
2028
|
+
* expression: 'formValue.price * (1 - formValue.discount / 100)'
|
|
1986
2029
|
* ```
|
|
1987
2030
|
*/
|
|
1988
|
-
|
|
2031
|
+
expression: string;
|
|
1989
2032
|
/**
|
|
1990
|
-
*
|
|
2033
|
+
* Explicit field dependencies for expressions.
|
|
1991
2034
|
*
|
|
1992
|
-
*
|
|
1993
|
-
*
|
|
1994
|
-
* `formValue` is scoped to the current array item, while `rootFormValue`
|
|
1995
|
-
* provides access to the entire form value including fields outside the array.
|
|
2035
|
+
* For `expression`, dependencies are automatically extracted from the expression.
|
|
2036
|
+
* Provide `dependsOn` to override automatic detection for complex expressions.
|
|
1996
2037
|
*
|
|
1997
2038
|
* @example
|
|
1998
2039
|
* ```typescript
|
|
1999
|
-
* //
|
|
2040
|
+
* // Override automatic detection for complex expressions
|
|
2000
2041
|
* {
|
|
2001
|
-
* key: '
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2042
|
+
* key: 'total',
|
|
2043
|
+
* logic: [{
|
|
2044
|
+
* type: 'derivation',
|
|
2045
|
+
* expression: 'calculateTotal(formValue)',
|
|
2046
|
+
* dependsOn: ['quantity', 'unitPrice', 'discount']
|
|
2047
|
+
* }]
|
|
2006
2048
|
* }
|
|
2007
2049
|
* ```
|
|
2008
|
-
*
|
|
2009
|
-
* For non-array derivations, `rootFormValue` is not set and `formValue`
|
|
2010
|
-
* contains the entire form value.
|
|
2011
|
-
*/
|
|
2012
|
-
rootFormValue?: Record<string, unknown>;
|
|
2013
|
-
/**
|
|
2014
|
-
* Current array index when inside an array derivation.
|
|
2015
|
-
*/
|
|
2016
|
-
arrayIndex?: number;
|
|
2017
|
-
/**
|
|
2018
|
-
* Path to the array field when inside an array derivation.
|
|
2019
2050
|
*/
|
|
2020
|
-
|
|
2051
|
+
dependsOn?: string[];
|
|
2052
|
+
value?: never;
|
|
2053
|
+
functionName?: never;
|
|
2054
|
+
http?: never;
|
|
2055
|
+
asyncFunctionName?: never;
|
|
2056
|
+
responseExpression?: never;
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Base for value derivations.
|
|
2060
|
+
*
|
|
2061
|
+
* @internal
|
|
2062
|
+
*/
|
|
2063
|
+
interface ValueDerivationBase extends SharedDerivationFields {
|
|
2064
|
+
source?: never;
|
|
2065
|
+
/** Forbidden on value mode */
|
|
2066
|
+
fn?: never;
|
|
2067
|
+
/** Forbidden on value mode */
|
|
2068
|
+
asyncFn?: never;
|
|
2021
2069
|
/**
|
|
2022
|
-
*
|
|
2070
|
+
* Static value to set on this field.
|
|
2023
2071
|
*
|
|
2024
|
-
*
|
|
2025
|
-
* in conditional logic, derivations, and other expressions.
|
|
2072
|
+
* Use when the derived value is a constant.
|
|
2026
2073
|
*
|
|
2027
2074
|
* @example
|
|
2028
2075
|
* ```typescript
|
|
2029
|
-
* //
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
2033
|
-
* }
|
|
2034
|
-
*
|
|
2035
|
-
* // In JavaScript expression:
|
|
2036
|
-
* condition: {
|
|
2037
|
-
* type: 'javascript',
|
|
2038
|
-
* expression: "externalData.userRole === 'admin'"
|
|
2039
|
-
* }
|
|
2076
|
+
* value: '+1' // String
|
|
2077
|
+
* value: 100 // Number
|
|
2078
|
+
* value: true // Boolean
|
|
2079
|
+
* value: { code: 'US' } // Object
|
|
2040
2080
|
* ```
|
|
2041
2081
|
*/
|
|
2042
|
-
|
|
2082
|
+
value: unknown;
|
|
2043
2083
|
/**
|
|
2044
|
-
*
|
|
2045
|
-
*
|
|
2084
|
+
* Explicit field dependencies for value derivations.
|
|
2085
|
+
*
|
|
2086
|
+
* For `value` (static), no dependencies are needed.
|
|
2087
|
+
* Provide `dependsOn` to conditionally re-evaluate when specific fields change.
|
|
2046
2088
|
*/
|
|
2047
|
-
|
|
2089
|
+
dependsOn?: string[];
|
|
2090
|
+
expression?: never;
|
|
2091
|
+
functionName?: never;
|
|
2092
|
+
http?: never;
|
|
2093
|
+
asyncFunctionName?: never;
|
|
2094
|
+
responseExpression?: never;
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Shared fields for both branches of {@link FunctionDerivationBase}.
|
|
2098
|
+
*
|
|
2099
|
+
* @internal
|
|
2100
|
+
*/
|
|
2101
|
+
interface FunctionDerivationShared extends SharedDerivationFields {
|
|
2102
|
+
source?: never;
|
|
2103
|
+
/** Forbidden on function-derivation mode */
|
|
2104
|
+
asyncFn?: never;
|
|
2048
2105
|
/**
|
|
2049
|
-
*
|
|
2106
|
+
* Explicit field dependencies for function derivations.
|
|
2050
2107
|
*
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
2108
|
+
* If not provided, defaults to all fields ('*').
|
|
2109
|
+
* Specify `dependsOn` for better performance when the function only
|
|
2110
|
+
* depends on a subset of fields.
|
|
2053
2111
|
*
|
|
2054
2112
|
* @example
|
|
2055
2113
|
* ```typescript
|
|
2056
|
-
* //
|
|
2057
|
-
*
|
|
2058
|
-
*
|
|
2059
|
-
*
|
|
2114
|
+
* // Only re-evaluate when country changes
|
|
2115
|
+
* {
|
|
2116
|
+
* key: 'currency',
|
|
2117
|
+
* logic: [{
|
|
2118
|
+
* type: 'derivation',
|
|
2119
|
+
* functionName: 'getCurrencyForCountry',
|
|
2120
|
+
* dependsOn: ['country']
|
|
2121
|
+
* }]
|
|
2060
2122
|
* }
|
|
2061
2123
|
* ```
|
|
2062
2124
|
*/
|
|
2063
|
-
|
|
2125
|
+
dependsOn?: string[];
|
|
2126
|
+
value?: never;
|
|
2127
|
+
expression?: never;
|
|
2128
|
+
http?: never;
|
|
2129
|
+
asyncFunctionName?: never;
|
|
2130
|
+
responseExpression?: never;
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Base for custom sync function derivations. `functionName` and `fn` are
|
|
2134
|
+
* mutually exclusive (XOR).
|
|
2135
|
+
*
|
|
2136
|
+
* @internal
|
|
2137
|
+
*/
|
|
2138
|
+
type FunctionDerivationBase = (FunctionDerivationShared & {
|
|
2064
2139
|
/**
|
|
2065
|
-
*
|
|
2140
|
+
* Name of a registered custom derivation function.
|
|
2066
2141
|
*
|
|
2067
|
-
*
|
|
2068
|
-
*
|
|
2142
|
+
* The function receives the evaluation context and returns the derived value.
|
|
2143
|
+
* Register functions in `customFnConfig.derivations`.
|
|
2069
2144
|
*
|
|
2070
2145
|
* @example
|
|
2071
2146
|
* ```typescript
|
|
2072
|
-
*
|
|
2073
|
-
*
|
|
2074
|
-
*
|
|
2075
|
-
* expression: "formFieldState.email.dirty && formFieldState.email.valid"
|
|
2076
|
-
* }
|
|
2147
|
+
* functionName: 'getCurrencyForCountry'
|
|
2148
|
+
* functionName: 'calculateTax'
|
|
2149
|
+
* functionName: 'formatPhoneNumber'
|
|
2077
2150
|
* ```
|
|
2078
2151
|
*/
|
|
2079
|
-
|
|
2080
|
-
/**
|
|
2081
|
-
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2152
|
+
functionName: string;
|
|
2153
|
+
/** Inline form is forbidden when `functionName` is set */
|
|
2154
|
+
fn?: never;
|
|
2155
|
+
}) | (FunctionDerivationShared & {
|
|
2156
|
+
/**
|
|
2157
|
+
* Inline custom derivation function. Mutually exclusive with `functionName`.
|
|
2158
|
+
*
|
|
2159
|
+
* NOT JSON-serializable — for code-only configs. For configs loaded
|
|
2160
|
+
* from JSON / OpenAPI / databases, use `functionName` to reference
|
|
2161
|
+
* a function registered in `customFnConfig.derivations`.
|
|
2162
|
+
*/
|
|
2163
|
+
fn: CustomFunction;
|
|
2164
|
+
/** Registered form is forbidden when `fn` is set */
|
|
2165
|
+
functionName?: never;
|
|
2166
|
+
});
|
|
2167
|
+
/**
|
|
2168
|
+
* HTTP derivation that evaluates immediately on change (default).
|
|
2169
|
+
* @internal
|
|
2170
|
+
*/
|
|
2171
|
+
type OnChangeHttpDerivationLogicConfig = HttpDerivationBase & ImmediateDerivationTrigger;
|
|
2084
2172
|
/**
|
|
2085
|
-
*
|
|
2086
|
-
*
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2173
|
+
* HTTP derivation that evaluates after a debounce period.
|
|
2174
|
+
* @internal
|
|
2175
|
+
*/
|
|
2176
|
+
type DebouncedHttpDerivationLogicConfig = HttpDerivationBase & DebouncedDerivationTrigger;
|
|
2177
|
+
/**
|
|
2178
|
+
* Async function derivation that evaluates immediately on change (default).
|
|
2179
|
+
* @internal
|
|
2180
|
+
*/
|
|
2181
|
+
type OnChangeAsyncFunctionDerivationLogicConfig = AsyncFunctionDerivationBase & ImmediateDerivationTrigger;
|
|
2182
|
+
/**
|
|
2183
|
+
* Async function derivation that evaluates after a debounce period.
|
|
2184
|
+
* @internal
|
|
2185
|
+
*/
|
|
2186
|
+
type DebouncedAsyncFunctionDerivationLogicConfig = AsyncFunctionDerivationBase & DebouncedDerivationTrigger;
|
|
2187
|
+
/**
|
|
2188
|
+
* Expression derivation that evaluates immediately on change (default).
|
|
2189
|
+
* @internal
|
|
2190
|
+
*/
|
|
2191
|
+
type OnChangeExpressionDerivationLogicConfig = ExpressionDerivationBase & ImmediateDerivationTrigger;
|
|
2192
|
+
/**
|
|
2193
|
+
* Expression derivation that evaluates after a debounce period.
|
|
2194
|
+
* @internal
|
|
2195
|
+
*/
|
|
2196
|
+
type DebouncedExpressionDerivationLogicConfig = ExpressionDerivationBase & DebouncedDerivationTrigger;
|
|
2197
|
+
/**
|
|
2198
|
+
* Value derivation that evaluates immediately on change (default).
|
|
2199
|
+
* @internal
|
|
2200
|
+
*/
|
|
2201
|
+
type OnChangeValueDerivationLogicConfig = ValueDerivationBase & ImmediateDerivationTrigger;
|
|
2202
|
+
/**
|
|
2203
|
+
* Value derivation that evaluates after a debounce period.
|
|
2204
|
+
* @internal
|
|
2205
|
+
*/
|
|
2206
|
+
type DebouncedValueDerivationLogicConfig = ValueDerivationBase & DebouncedDerivationTrigger;
|
|
2207
|
+
/**
|
|
2208
|
+
* Function derivation that evaluates immediately on change (default).
|
|
2209
|
+
* @internal
|
|
2210
|
+
*/
|
|
2211
|
+
type OnChangeFunctionDerivationLogicConfig = FunctionDerivationBase & ImmediateDerivationTrigger;
|
|
2212
|
+
/**
|
|
2213
|
+
* Function derivation that evaluates after a debounce period.
|
|
2214
|
+
* @internal
|
|
2215
|
+
*/
|
|
2216
|
+
type DebouncedFunctionDerivationLogicConfig = FunctionDerivationBase & DebouncedDerivationTrigger;
|
|
2217
|
+
/**
|
|
2218
|
+
* Configuration for value derivation logic.
|
|
2092
2219
|
*
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2095
|
-
*
|
|
2096
|
-
* - Receive EvaluationContext (field values) not FieldContext (field state)
|
|
2220
|
+
* Enables programmatic value derivation based on conditions.
|
|
2221
|
+
* Derivations are self-targeting: the logic is placed on the field
|
|
2222
|
+
* that should receive the computed value.
|
|
2097
2223
|
*
|
|
2098
2224
|
* @example
|
|
2099
2225
|
* ```typescript
|
|
2100
|
-
* //
|
|
2101
|
-
*
|
|
2102
|
-
*
|
|
2103
|
-
*
|
|
2226
|
+
* // Set phone prefix based on country selection
|
|
2227
|
+
* {
|
|
2228
|
+
* key: 'phonePrefix',
|
|
2229
|
+
* logic: [{
|
|
2230
|
+
* type: 'derivation',
|
|
2231
|
+
* value: '+1',
|
|
2232
|
+
* condition: {
|
|
2233
|
+
* type: 'fieldValue',
|
|
2234
|
+
* fieldPath: 'country',
|
|
2235
|
+
* operator: 'equals',
|
|
2236
|
+
* value: 'USA'
|
|
2237
|
+
* }
|
|
2238
|
+
* }]
|
|
2239
|
+
* }
|
|
2104
2240
|
*
|
|
2105
|
-
* //
|
|
2241
|
+
* // Compute total from quantity and price
|
|
2106
2242
|
* {
|
|
2107
|
-
* key: '
|
|
2108
|
-
*
|
|
2109
|
-
* when: { function: 'isAdult' } // Only show if isAdult returns true
|
|
2243
|
+
* key: 'total',
|
|
2244
|
+
* derivation: 'formValue.quantity * formValue.unitPrice'
|
|
2110
2245
|
* }
|
|
2111
|
-
* ```
|
|
2112
2246
|
*
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2116
|
-
*
|
|
2117
|
-
*
|
|
2118
|
-
*
|
|
2119
|
-
*
|
|
2120
|
-
* }
|
|
2247
|
+
* // Use custom function for complex logic
|
|
2248
|
+
* {
|
|
2249
|
+
* key: 'currency',
|
|
2250
|
+
* logic: [{
|
|
2251
|
+
* type: 'derivation',
|
|
2252
|
+
* functionName: 'getCurrencyForCountry'
|
|
2253
|
+
* }]
|
|
2254
|
+
* }
|
|
2255
|
+
*
|
|
2256
|
+
* // Self-transform with debounced trigger
|
|
2257
|
+
* // (applies after user stops typing)
|
|
2258
|
+
* {
|
|
2259
|
+
* key: 'email',
|
|
2260
|
+
* logic: [{
|
|
2261
|
+
* type: 'derivation',
|
|
2262
|
+
* expression: 'formValue.email.toLowerCase()',
|
|
2263
|
+
* trigger: 'debounced',
|
|
2264
|
+
* debounceMs: 500
|
|
2265
|
+
* }]
|
|
2266
|
+
* }
|
|
2267
|
+
*
|
|
2268
|
+
* // Array item derivation (formValue is scoped to current item)
|
|
2269
|
+
* {
|
|
2270
|
+
* key: 'lineTotal', // Inside array field
|
|
2271
|
+
* derivation: 'formValue.quantity * formValue.unitPrice'
|
|
2272
|
+
* }
|
|
2121
2273
|
* ```
|
|
2274
|
+
*
|
|
2275
|
+
* @public
|
|
2122
2276
|
*/
|
|
2123
|
-
type
|
|
2124
|
-
|
|
2277
|
+
type DerivationLogicConfig = OnChangeHttpDerivationLogicConfig | DebouncedHttpDerivationLogicConfig | OnChangeAsyncFunctionDerivationLogicConfig | DebouncedAsyncFunctionDerivationLogicConfig | OnChangeExpressionDerivationLogicConfig | DebouncedExpressionDerivationLogicConfig | OnChangeValueDerivationLogicConfig | DebouncedValueDerivationLogicConfig | OnChangeFunctionDerivationLogicConfig | DebouncedFunctionDerivationLogicConfig;
|
|
2125
2278
|
/**
|
|
2126
|
-
*
|
|
2127
|
-
*
|
|
2128
|
-
* The function receives the evaluation context and may return a Promise or Observable.
|
|
2129
|
-
* Register functions in `customFnConfig.asyncDerivations`.
|
|
2279
|
+
* Union type for all logic configurations.
|
|
2130
2280
|
*
|
|
2131
|
-
*
|
|
2132
|
-
*
|
|
2133
|
-
* const fetchPrice: AsyncDerivationFunction = async (context) => {
|
|
2134
|
-
* const response = await fetch(`/api/price?product=${context.formValue.productId}`);
|
|
2135
|
-
* const data = await response.json();
|
|
2136
|
-
* return data.price;
|
|
2137
|
-
* };
|
|
2138
|
-
* ```
|
|
2281
|
+
* - `StateLogicConfig`: For field state changes (hidden, readonly, disabled, required)
|
|
2282
|
+
* - `DerivationLogicConfig`: For value derivation (including property derivation via `targetProperty`)
|
|
2139
2283
|
*
|
|
2140
2284
|
* @public
|
|
2141
2285
|
*/
|
|
2142
|
-
type
|
|
2286
|
+
type LogicConfig = StateLogicConfig | DerivationLogicConfig;
|
|
2143
2287
|
/**
|
|
2144
|
-
*
|
|
2288
|
+
* Log level for derivation debug output.
|
|
2145
2289
|
*
|
|
2146
|
-
*
|
|
2147
|
-
*
|
|
2290
|
+
* - 'none': No debug logging
|
|
2291
|
+
* - 'summary': Log cycle completion with counts (default in dev mode)
|
|
2292
|
+
* - 'verbose': Log individual derivation evaluations with details
|
|
2148
2293
|
*
|
|
2149
|
-
* @
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
*
|
|
2154
|
-
*
|
|
2155
|
-
*
|
|
2156
|
-
*
|
|
2294
|
+
* @public
|
|
2295
|
+
*/
|
|
2296
|
+
type DerivationLogLevel = 'none' | 'summary' | 'verbose';
|
|
2297
|
+
/**
|
|
2298
|
+
* Type guard to check if a condition is a FormStateCondition.
|
|
2299
|
+
*
|
|
2300
|
+
* @param condition - The condition to check
|
|
2301
|
+
* @returns true if the condition is a FormStateCondition
|
|
2157
2302
|
*
|
|
2158
2303
|
* @public
|
|
2159
2304
|
*/
|
|
2160
|
-
|
|
2305
|
+
declare function isFormStateCondition(condition: StateLogicConfig['condition'] | DerivationLogicConfig['condition']): condition is FormStateCondition;
|
|
2306
|
+
|
|
2307
|
+
/**
|
|
2308
|
+
* Configuration for applying predefined schemas
|
|
2309
|
+
*/
|
|
2310
|
+
interface SchemaApplicationConfig {
|
|
2311
|
+
/** Schema application type */
|
|
2312
|
+
type: 'apply' | 'applyWhen' | 'applyWhenValue' | 'applyEach';
|
|
2313
|
+
/** Schema identifier or inline schema definition */
|
|
2314
|
+
schema: string | SchemaDefinition;
|
|
2315
|
+
/** Condition for conditional application */
|
|
2316
|
+
condition?: ConditionalExpression;
|
|
2317
|
+
/** Type predicate for applyWhenValue */
|
|
2318
|
+
typePredicate?: string;
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Reusable schema definition that can be referenced by name
|
|
2322
|
+
*/
|
|
2323
|
+
interface SchemaDefinition {
|
|
2324
|
+
/** Unique schema identifier */
|
|
2325
|
+
name: string;
|
|
2326
|
+
/** Schema description */
|
|
2327
|
+
description?: string;
|
|
2328
|
+
/** Field path pattern this schema applies to */
|
|
2329
|
+
pathPattern?: string;
|
|
2330
|
+
/** Validators to apply */
|
|
2331
|
+
validators?: ValidatorConfig[];
|
|
2332
|
+
/** Logic rules to apply */
|
|
2333
|
+
logic?: LogicConfig[];
|
|
2334
|
+
/** Nested schema applications */
|
|
2335
|
+
subSchemas?: SchemaApplicationConfig[];
|
|
2336
|
+
}
|
|
2161
2337
|
|
|
2162
2338
|
/**
|
|
2163
2339
|
* Re-export Angular's ValidationError for consistency
|
|
@@ -2973,6 +3149,19 @@ interface FormOptions {
|
|
|
2973
3149
|
* @default undefined (uses global setting)
|
|
2974
3150
|
*/
|
|
2975
3151
|
excludeValueIfReadonly?: boolean;
|
|
3152
|
+
/**
|
|
3153
|
+
* Whether to run validation when a field is hidden.
|
|
3154
|
+
*
|
|
3155
|
+
* Acts as the root inherited value for the entire form — per-field
|
|
3156
|
+
* `FieldDef.validateWhenHidden` overrides it for a subtree, and once
|
|
3157
|
+
* overridden the new value is itself inherited by descendants unless
|
|
3158
|
+
* they override in turn.
|
|
3159
|
+
*
|
|
3160
|
+
* Overrides the global `withValidationExecutionDefaults()` setting for this form.
|
|
3161
|
+
*
|
|
3162
|
+
* @default undefined (uses global setting, which defaults to `false`)
|
|
3163
|
+
*/
|
|
3164
|
+
validateWhenHidden?: boolean;
|
|
2976
3165
|
/**
|
|
2977
3166
|
* Whether to attach the current form value to all events dispatched through the EventBus.
|
|
2978
3167
|
*
|
|
@@ -3666,14 +3855,14 @@ declare function isWrapperTypeDefinition(value: unknown): value is WrapperTypeDe
|
|
|
3666
3855
|
* </dbx-section>
|
|
3667
3856
|
* `,
|
|
3668
3857
|
* })
|
|
3669
|
-
* export class SectionWrapperComponent implements
|
|
3858
|
+
* export class SectionWrapperComponent implements FieldWrapper {
|
|
3670
3859
|
* readonly fieldComponent = viewChild.required('fieldComponent', { read: ViewContainerRef });
|
|
3671
3860
|
* readonly header = input<string>();
|
|
3672
3861
|
* readonly fieldInputs = input<WrapperFieldInputs>();
|
|
3673
3862
|
* }
|
|
3674
3863
|
* ```
|
|
3675
3864
|
*/
|
|
3676
|
-
interface
|
|
3865
|
+
interface FieldWrapper {
|
|
3677
3866
|
/** ViewContainerRef slot where inner content is rendered */
|
|
3678
3867
|
readonly fieldComponent: Signal<ViewContainerRef>;
|
|
3679
3868
|
}
|
|
@@ -3899,8 +4088,10 @@ interface FieldDef<TProps, TMeta extends FieldMeta = FieldMeta> {
|
|
|
3899
4088
|
/**
|
|
3900
4089
|
* Whether the field is hidden from view.
|
|
3901
4090
|
*
|
|
3902
|
-
* When true, the field is not rendered in the UI
|
|
3903
|
-
*
|
|
4091
|
+
* When true, the field is not rendered in the UI. By default, validators on
|
|
4092
|
+
* a hidden field are skipped — set {@link validateWhenHidden} to `true` (on
|
|
4093
|
+
* the field, an ancestor, the form, or globally) to keep validation running
|
|
4094
|
+
* while hidden. State logic and value derivations continue to apply.
|
|
3904
4095
|
*
|
|
3905
4096
|
* @value false
|
|
3906
4097
|
*/
|
|
@@ -3943,6 +4134,23 @@ interface FieldDef<TProps, TMeta extends FieldMeta = FieldMeta> {
|
|
|
3943
4134
|
* @default undefined (uses form-level or global setting)
|
|
3944
4135
|
*/
|
|
3945
4136
|
excludeValueIfReadonly?: boolean;
|
|
4137
|
+
/**
|
|
4138
|
+
* Whether to run validation when this field is hidden — statically (`hidden: true`),
|
|
4139
|
+
* via a `hidden` logic rule, or via any hidden ancestor.
|
|
4140
|
+
*
|
|
4141
|
+
* When `false`, all validators on this field (built-in like `required`/`email`/`min`,
|
|
4142
|
+
* custom `validate()`/expression validators, and `required` state logic) are skipped
|
|
4143
|
+
* while the field is hidden. The hidden state, disabled state, readonly state, and
|
|
4144
|
+
* value derivations continue to apply.
|
|
4145
|
+
*
|
|
4146
|
+
* Inherited from the parent field — once a field overrides this value, all of its
|
|
4147
|
+
* descendants inherit the new value unless they override in turn. Form-level
|
|
4148
|
+
* `FormOptions.validateWhenHidden` is the root inherited value, falling back to the
|
|
4149
|
+
* global default.
|
|
4150
|
+
*
|
|
4151
|
+
* @default undefined — inherits from parent / form / global default (which is `false`)
|
|
4152
|
+
*/
|
|
4153
|
+
validateWhenHidden?: boolean;
|
|
3946
4154
|
/**
|
|
3947
4155
|
* Column sizing configuration for responsive grid layout.
|
|
3948
4156
|
*
|
|
@@ -4051,6 +4259,21 @@ interface ArrayContext {
|
|
|
4051
4259
|
/** The array field definition. */
|
|
4052
4260
|
field: FieldDef<unknown>;
|
|
4053
4261
|
}
|
|
4262
|
+
/**
|
|
4263
|
+
* Group context for fields rendered inside `group` containers.
|
|
4264
|
+
*
|
|
4265
|
+
* Carries a reactive dot-separated chain of group ancestors (including the
|
|
4266
|
+
* current group). Used by `mapFieldToInputs` to scope DOM IDs of group
|
|
4267
|
+
* children, so the same leaf key can appear inside different groups without
|
|
4268
|
+
* producing duplicate `id` attributes (issue #401).
|
|
4269
|
+
*
|
|
4270
|
+
* Re-provided by every group's child injector — a nested group reads its
|
|
4271
|
+
* parent's context signal and composes its own path through `computed()`.
|
|
4272
|
+
*/
|
|
4273
|
+
interface GroupContext {
|
|
4274
|
+
/** Dot-separated path of group ancestors, including the current group's key (e.g. `'address'` or `'user.address'`). */
|
|
4275
|
+
groupPath: Signal<string>;
|
|
4276
|
+
}
|
|
4054
4277
|
/**
|
|
4055
4278
|
* Mapper function that converts a field definition to component inputs.
|
|
4056
4279
|
*
|
|
@@ -4414,6 +4637,7 @@ declare class DynamicForm<TFields extends RegisteredFieldTypes[] = RegisteredFie
|
|
|
4414
4637
|
excludeValueIfHidden?: boolean;
|
|
4415
4638
|
excludeValueIfDisabled?: boolean;
|
|
4416
4639
|
excludeValueIfReadonly?: boolean;
|
|
4640
|
+
validateWhenHidden?: boolean;
|
|
4417
4641
|
emitFormValueOnEvents?: boolean;
|
|
4418
4642
|
}>;
|
|
4419
4643
|
/** Field signal context for injection into child components */
|
|
@@ -4749,6 +4973,26 @@ declare const FIELD_SIGNAL_CONTEXT: InjectionToken<FieldSignalContext<Record<str
|
|
|
4749
4973
|
* ```
|
|
4750
4974
|
*/
|
|
4751
4975
|
declare const ARRAY_CONTEXT: InjectionToken<ArrayContext>;
|
|
4976
|
+
/**
|
|
4977
|
+
* Injection token for providing group ancestry to mappers and components.
|
|
4978
|
+
*
|
|
4979
|
+
* Re-provided in two places:
|
|
4980
|
+
* 1. `GroupFieldComponent` provides a context whose `groupPath()` is the
|
|
4981
|
+
* parent path extended by its own key (e.g. `'address'` or `'user.address'`).
|
|
4982
|
+
* 2. `createArrayItemInjector` provides a sentinel with an empty `groupPath`,
|
|
4983
|
+
* resetting the chain at array boundaries — descendants of an array item
|
|
4984
|
+
* that re-enter a group compose paths INSIDE the array item, matching the
|
|
4985
|
+
* property-derivation collector's keying.
|
|
4986
|
+
*
|
|
4987
|
+
* `mapFieldToInputs` reads this to scope DOM IDs and override-store keys so
|
|
4988
|
+
* the same leaf key can appear in different groups without colliding (#401).
|
|
4989
|
+
*
|
|
4990
|
+
* Inject with `{ optional: true }` because top-level fields with no group
|
|
4991
|
+
* ancestor (and no enclosing array) won't have the token provided. The token
|
|
4992
|
+
* IS present for fields inside an array even when no inner group exists — the
|
|
4993
|
+
* array-boundary sentinel makes injection succeed with an empty `groupPath`.
|
|
4994
|
+
*/
|
|
4995
|
+
declare const GROUP_CONTEXT: InjectionToken<GroupContext>;
|
|
4752
4996
|
/**
|
|
4753
4997
|
* Injection token for form-level default props.
|
|
4754
4998
|
*
|
|
@@ -5288,6 +5532,71 @@ type ResolvedValueExclusionConfig = Required<ValueExclusionConfig>;
|
|
|
5288
5532
|
*/
|
|
5289
5533
|
declare function withValueExclusionDefaults(config?: Partial<ValueExclusionConfig>): DynamicFormFeature<'value-exclusion'>;
|
|
5290
5534
|
|
|
5535
|
+
/**
|
|
5536
|
+
* Configuration for when validation runs based on a field's reactive state.
|
|
5537
|
+
*
|
|
5538
|
+
* Currently controls whether hidden fields are validated. Resolved through a
|
|
5539
|
+
* tree-aware hierarchy: per-field overrides inherit down to descendants, with
|
|
5540
|
+
* per-form `FormOptions` and global `withValidationExecutionDefaults()` providing
|
|
5541
|
+
* the root inherited value.
|
|
5542
|
+
*
|
|
5543
|
+
* @public
|
|
5544
|
+
*/
|
|
5545
|
+
interface ValidationExecutionConfig {
|
|
5546
|
+
/**
|
|
5547
|
+
* Whether validators run while a field is hidden (statically, via `hidden`
|
|
5548
|
+
* logic, or because any ancestor is hidden).
|
|
5549
|
+
*
|
|
5550
|
+
* When `false`, all validators on a hidden field are skipped — the field is
|
|
5551
|
+
* effectively inert until shown. State logic (hidden/disabled/readonly) and
|
|
5552
|
+
* value derivations continue to apply.
|
|
5553
|
+
*/
|
|
5554
|
+
readonly validateWhenHidden?: boolean;
|
|
5555
|
+
}
|
|
5556
|
+
/**
|
|
5557
|
+
* Fully resolved validation execution config — every property defined.
|
|
5558
|
+
*
|
|
5559
|
+
* @public
|
|
5560
|
+
*/
|
|
5561
|
+
type ResolvedValidationExecutionConfig = Required<ValidationExecutionConfig>;
|
|
5562
|
+
|
|
5563
|
+
/**
|
|
5564
|
+
* Configures global validation execution defaults — when validators should run
|
|
5565
|
+
* relative to a field's reactive state.
|
|
5566
|
+
*
|
|
5567
|
+
* By default, validators are **skipped** for hidden fields (`validateWhenHidden: false`),
|
|
5568
|
+
* matching what most users expect: a hidden field is inert. Use this feature to
|
|
5569
|
+
* restore the legacy behavior where hidden fields still validate.
|
|
5570
|
+
*
|
|
5571
|
+
* @example Default behavior — no feature needed
|
|
5572
|
+
* ```typescript
|
|
5573
|
+
* provideDynamicForm(...withMaterialFields())
|
|
5574
|
+
* // hidden fields skip validation
|
|
5575
|
+
* ```
|
|
5576
|
+
*
|
|
5577
|
+
* @example Restore legacy validation-on-hidden behavior
|
|
5578
|
+
* ```typescript
|
|
5579
|
+
* provideDynamicForm(
|
|
5580
|
+
* ...withMaterialFields(),
|
|
5581
|
+
* withValidationExecutionDefaults({ validateWhenHidden: true })
|
|
5582
|
+
* )
|
|
5583
|
+
* ```
|
|
5584
|
+
*
|
|
5585
|
+
* @remarks
|
|
5586
|
+
* **Precedence rules:**
|
|
5587
|
+
* 1. Per-field `FieldDef.validateWhenHidden` — wins for that field; inherits down to descendants
|
|
5588
|
+
* that don't override.
|
|
5589
|
+
* 2. Per-form `FormOptions.validateWhenHidden` — root inherited value for the form.
|
|
5590
|
+
* 3. Global `withValidationExecutionDefaults()` — fallback when no form/field setting.
|
|
5591
|
+
* 4. No feature configured — uses token default (`validateWhenHidden: false`).
|
|
5592
|
+
*
|
|
5593
|
+
* @param config - Partial override. Unspecified properties keep their defaults.
|
|
5594
|
+
* @returns A `DynamicFormFeature` that configures validation execution defaults.
|
|
5595
|
+
*
|
|
5596
|
+
* @public
|
|
5597
|
+
*/
|
|
5598
|
+
declare function withValidationExecutionDefaults(config?: Partial<ValidationExecutionConfig>): DynamicFormFeature<'validation-execution'>;
|
|
5599
|
+
|
|
5291
5600
|
/**
|
|
5292
5601
|
* Zero-cost type carrier used in wrapper registrations.
|
|
5293
5602
|
*
|
|
@@ -5397,7 +5706,7 @@ interface BaseValueField<TProps, TValue, TMeta extends FieldMeta = FieldMeta, TN
|
|
|
5397
5706
|
nullable?: TNullable;
|
|
5398
5707
|
}
|
|
5399
5708
|
declare function isValueField<TProps, TMeta extends FieldMeta = FieldMeta>(field: FieldDef<TProps, TMeta>): field is BaseValueField<TProps, ValueType, TMeta, boolean>;
|
|
5400
|
-
type ExcludedKeys$1 = 'type' | 'conditionals' | 'value' | 'valueType' | 'nullable' | 'disabled' | 'readonly' | 'hidden' | 'col' | 'minValue' | 'maxValue' | 'step' | 'required' | 'email' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'validators' | 'logic' | 'derivation' | 'schemas' | 'excludeValueIfHidden' | 'excludeValueIfDisabled' | 'excludeValueIfReadonly' | 'wrappers' | 'skipAutoWrappers' | 'skipDefaultWrappers';
|
|
5709
|
+
type ExcludedKeys$1 = 'type' | 'conditionals' | 'value' | 'valueType' | 'nullable' | 'disabled' | 'readonly' | 'hidden' | 'col' | 'minValue' | 'maxValue' | 'step' | 'required' | 'email' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'validators' | 'logic' | 'derivation' | 'schemas' | 'excludeValueIfHidden' | 'excludeValueIfDisabled' | 'excludeValueIfReadonly' | 'validateWhenHidden' | 'wrappers' | 'skipAutoWrappers' | 'skipDefaultWrappers';
|
|
5401
5710
|
type ValueFieldComponent<T extends BaseValueField<Record<string, unknown> | unknown, unknown, FieldMeta, boolean>> = Prettify<WithInputSignals<Omit<T, ExcludedKeys$1>>>;
|
|
5402
5711
|
|
|
5403
5712
|
interface BaseCheckedField<TProps, TMeta extends FieldMeta = FieldMeta> extends FieldDef<TProps, TMeta>, FieldWithValidation {
|
|
@@ -5410,7 +5719,7 @@ interface BaseCheckedField<TProps, TMeta extends FieldMeta = FieldMeta> extends
|
|
|
5410
5719
|
required?: boolean;
|
|
5411
5720
|
}
|
|
5412
5721
|
declare function isCheckedField<TProps, TMeta extends FieldMeta = FieldMeta>(field: FieldDef<TProps, TMeta>): field is BaseCheckedField<TProps, TMeta>;
|
|
5413
|
-
type ExcludedKeys = 'type' | 'conditionals' | 'value' | 'disabled' | 'readonly' | 'hidden' | 'col' | keyof FieldWithValidation | 'excludeValueIfHidden' | 'excludeValueIfDisabled' | 'excludeValueIfReadonly' | 'wrappers' | 'skipAutoWrappers' | 'skipDefaultWrappers';
|
|
5722
|
+
type ExcludedKeys = 'type' | 'conditionals' | 'value' | 'disabled' | 'readonly' | 'hidden' | 'col' | keyof FieldWithValidation | 'excludeValueIfHidden' | 'excludeValueIfDisabled' | 'excludeValueIfReadonly' | 'validateWhenHidden' | 'wrappers' | 'skipAutoWrappers' | 'skipDefaultWrappers';
|
|
5414
5723
|
type CheckedFieldComponent<T extends BaseCheckedField<Record<string, unknown> | unknown>> = Prettify<WithInputSignals<Omit<T, ExcludedKeys>>>;
|
|
5415
5724
|
|
|
5416
5725
|
declare function applyValidator(config: ValidatorConfig, fieldPath: SchemaPath<any> | SchemaPathTree<any>): void;
|
|
@@ -6238,6 +6547,7 @@ declare class GroupFieldComponent<TModel extends Record<string, unknown> = Recor
|
|
|
6238
6547
|
private readonly destroyRef;
|
|
6239
6548
|
private readonly fieldRegistry;
|
|
6240
6549
|
private readonly parentFieldSignalContext;
|
|
6550
|
+
private readonly parentGroupContext;
|
|
6241
6551
|
private readonly injector;
|
|
6242
6552
|
protected readonly environmentInjector: EnvironmentInjector;
|
|
6243
6553
|
private readonly eventBus;
|
|
@@ -6267,6 +6577,13 @@ declare class GroupFieldComponent<TModel extends Record<string, unknown> = Recor
|
|
|
6267
6577
|
readonly disabled: _angular_core.Signal<boolean>;
|
|
6268
6578
|
private readonly nestedFieldTree;
|
|
6269
6579
|
private readonly groupFieldSignalContext;
|
|
6580
|
+
/**
|
|
6581
|
+
* Group context propagated to descendants — carries the dot-separated path of
|
|
6582
|
+
* group ancestors so child fields can scope their DOM IDs (issue #401).
|
|
6583
|
+
* Reactive via `computed()`: a nested group composes its path from the parent
|
|
6584
|
+
* context signal + its own key, so the chain stays consistent if either changes.
|
|
6585
|
+
*/
|
|
6586
|
+
private readonly groupContext;
|
|
6270
6587
|
private readonly groupInjector;
|
|
6271
6588
|
readonly validityChange: _angular_core.OutputRef<boolean>;
|
|
6272
6589
|
readonly dirtyChange: _angular_core.OutputRef<boolean>;
|
|
@@ -6658,5 +6975,5 @@ declare class RootFormRegistryService {
|
|
|
6658
6975
|
*/
|
|
6659
6976
|
declare const INITIALIZATION_TIMEOUT_MS: InjectionToken<number>;
|
|
6660
6977
|
|
|
6661
|
-
export { ARRAY_CONTEXT, AppendArrayItemEvent, ArrayFieldComponent, BUILT_IN_FIELDS, ConsoleLogger, ContainerFieldComponent, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, DEFAULT_WRAPPERS, DynamicForm, DynamicFormError, DynamicFormLogger, DynamicTextPipe, EventBus, EventDispatcher, FIELD_REGISTRY, FIELD_SIGNAL_CONTEXT, FORM_OPTIONS, FormClearEvent, FormResetEvent, GroupFieldComponent, INITIALIZATION_TIMEOUT_MS, InsertArrayItemEvent, MoveArrayItemEvent, NextPageEvent, NoopLogger, PageChangeEvent, PopArrayItemEvent, PrependArrayItemEvent, PreviousPageEvent, RemoveAtIndexEvent, RootFormRegistryService, ShiftArrayItemEvent, SubmitEvent, WRAPPER_REGISTRY, applyMetaToElement, applyValidator, applyValidators, arrayEvent, arrayFieldMapper, baseFieldMapper, buildBaseInputs, containerFieldMapper, createField, createWrappers, dynamicTextToObservable, evaluateNonFieldDisabled, evaluateNonFieldHidden, field, formConfig, getArrayLength, groupFieldMapper, hasFormValue, interpolateParams, isArrayField, isCheckedField, isContainerField, isContainerTypedField, isDisplayOnlyField, isEqual, isFormStateCondition, isGroupField, isLeafField, isPageField, isRowField, isSimplifiedArrayField, isValueBearingField, isValueField, isWrapperTypeDefinition, isWrappersBundle, omit, pageFieldMapper, provideDynamicForm, resolveNextButtonDisabled, resolveNonFieldDisabled, resolveNonFieldHidden, resolveSubmitButtonDisabled, resolveTokens, rowFieldMapper, textFieldMapper, toReadonlyFieldTree, withEventFormValue, withLoggerConfig, withPreviousValue, withValueExclusionDefaults, wrapperProps };
|
|
6662
|
-
export type { ArrayAllowedChildren, ArrayButtonConfig, ArrayContext, ArrayField, ArrayFieldTree, ArrayItemContext, ArrayItemDefinitionTemplate, ArrayItemTemplate, AsyncCondition, AsyncConditionFunction, AsyncCustomValidator, AsyncDerivationFunction, AsyncValidatorConfig, AvailableFieldTypes, BaseCheckedField, BaseValidatorConfig, BaseValueField, BuiltInValidatorConfig, ButtonLogicContext, CheckedFieldComponent, ConditionalExpression, ContainerAllowedChildren, ContainerField, ContainerFieldTypes, CssWrapper, CustomFnConfig, CustomValidator, CustomValidatorConfig, DeclarativeHttpValidatorConfig, DynamicFormFieldRegistry, DynamicText, EvaluationContext, ExtractField, ExtractFieldDefs, ExtractFormValue, FieldComponent, FieldDef, FieldMeta, FieldOption, FieldPathAccess, FieldRegistryContainers, FieldRegistryLeaves, FieldRegistryWrappers, FieldScope, FieldSignalContext, FieldStateContext, FieldStateInfo, FieldTypeDefinition, FieldWithValidation,
|
|
6978
|
+
export { ARRAY_CONTEXT, AppendArrayItemEvent, ArrayFieldComponent, BUILT_IN_FIELDS, ConsoleLogger, ContainerFieldComponent, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, DEFAULT_WRAPPERS, DynamicForm, DynamicFormError, DynamicFormLogger, DynamicTextPipe, EventBus, EventDispatcher, FIELD_REGISTRY, FIELD_SIGNAL_CONTEXT, FORM_OPTIONS, FormClearEvent, FormResetEvent, GROUP_CONTEXT, GroupFieldComponent, INITIALIZATION_TIMEOUT_MS, InsertArrayItemEvent, MoveArrayItemEvent, NextPageEvent, NoopLogger, PageChangeEvent, PopArrayItemEvent, PrependArrayItemEvent, PreviousPageEvent, RemoveAtIndexEvent, RootFormRegistryService, ShiftArrayItemEvent, SubmitEvent, WRAPPER_REGISTRY, applyMetaToElement, applyValidator, applyValidators, arrayEvent, arrayFieldMapper, baseFieldMapper, buildBaseInputs, containerFieldMapper, createField, createWrappers, dynamicTextToObservable, evaluateNonFieldDisabled, evaluateNonFieldHidden, field, formConfig, getArrayLength, groupFieldMapper, hasFormValue, interpolateParams, isArrayField, isCheckedField, isContainerField, isContainerTypedField, isDisplayOnlyField, isEqual, isFormStateCondition, isGroupField, isLeafField, isPageField, isRowField, isSimplifiedArrayField, isValueBearingField, isValueField, isWrapperTypeDefinition, isWrappersBundle, omit, pageFieldMapper, provideDynamicForm, resolveNextButtonDisabled, resolveNonFieldDisabled, resolveNonFieldHidden, resolveSubmitButtonDisabled, resolveTokens, rowFieldMapper, textFieldMapper, toReadonlyFieldTree, withEventFormValue, withLoggerConfig, withPreviousValue, withValidationExecutionDefaults, withValueExclusionDefaults, wrapperProps };
|
|
6979
|
+
export type { ArrayAllowedChildren, ArrayButtonConfig, ArrayContext, ArrayField, ArrayFieldTree, ArrayItemContext, ArrayItemDefinitionTemplate, ArrayItemTemplate, AsyncCondition, AsyncConditionFunction, AsyncCustomValidator, AsyncDerivationFunction, AsyncValidatorConfig, AvailableFieldTypes, BaseCheckedField, BaseValidatorConfig, BaseValueField, BuiltInValidatorConfig, ButtonLogicContext, CheckedFieldComponent, ConditionalExpression, ContainerAllowedChildren, ContainerField, ContainerFieldTypes, CssWrapper, CustomFnConfig, CustomFunction, CustomValidator, CustomValidatorConfig, DeclarativeHttpValidatorConfig, DynamicFormFieldRegistry, DynamicText, EvaluationContext, ExtractField, ExtractFieldDefs, ExtractFormValue, FieldComponent, FieldDef, FieldMeta, FieldOption, FieldPathAccess, FieldRegistryContainers, FieldRegistryLeaves, FieldRegistryWrappers, FieldScope, FieldSignalContext, FieldStateContext, FieldStateInfo, FieldTypeDefinition, FieldWithValidation, FieldWrapper, FormConfig, FormEvent, FormEventConstructor, FormFieldStateMap, FormMode, FormModeDetectionResult, FormOptions, FormStateCondition, GroupAllowedChildren, GroupField, HttpCondition, HttpCustomValidator, HttpRequestConfig, HttpResourceRequest, HttpValidationResponseMapping, InferFormValue$1 as InferFormValue, InferWrapperRegistry, LeafFieldTypes, Logger, LogicConfig, MapperFn, NarrowField, NarrowFields, NextButtonOptions, NonFieldLogicConfig, NonFieldLogicContext, NonFieldLogicType, PageAllowedChildren, PageField, Prettify, ReadonlyFieldTree, RegisteredFieldTypes, ResolvedValidationExecutionConfig, ResolvedValueExclusionConfig, RowAllowedChildren, RowField, SchemaApplicationConfig, SchemaDefinition, SimplifiedArrayField, StateLogicConfig, SubmissionActionResult, SubmissionConfig, SubmitButtonOptions, TextElementType, TextField, TextProps, TokenContext, ValidationError, ValidationExecutionConfig, ValidationMessages, ValidatorConfig, ValueExclusionConfig, ValueFieldComponent, ValueHandlingMode, ValueType, WithInputSignals, WrapperConfig, WrapperFieldInputs, WrapperRegistration, WrapperTypeDefinition, WrappersBundle };
|