@ng-forge/dynamic-forms 0.9.0-next.10 → 0.9.0-next.12

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.
Files changed (26) hide show
  1. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs → ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs} +167 -69
  2. package/fesm2022/ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs.map +1 -0
  3. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs → ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs} +9 -5
  4. package/fesm2022/ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs.map +1 -0
  5. package/fesm2022/{ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs → ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs} +2 -2
  6. package/fesm2022/{ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs.map → ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs.map} +1 -1
  7. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs → ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs} +2 -2
  8. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs.map} +1 -1
  9. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs → ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs} +2 -2
  10. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs.map → ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs.map} +1 -1
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs → ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs} +30 -6
  12. package/fesm2022/ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs.map +1 -0
  13. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs} +713 -318
  14. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs.map +1 -0
  15. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs → ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs} +9 -5
  16. package/fesm2022/ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs.map +1 -0
  17. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs → ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs} +2 -2
  18. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs.map → ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs.map} +1 -1
  19. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  20. package/package.json +1 -1
  21. package/types/ng-forge-dynamic-forms.d.ts +2042 -1803
  22. package/fesm2022/ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs.map +0 -1
  23. package/fesm2022/ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs.map +0 -1
  24. package/fesm2022/ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs.map +0 -1
  25. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs.map +0 -1
  26. package/fesm2022/ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs.map +0 -1
@@ -16,502 +16,631 @@ type Prettify<T> = {
16
16
  [K in keyof T]: T[K];
17
17
  } & {};
18
18
 
19
- type DynamicText = string | Observable<string> | Signal<string>;
20
-
21
19
  /**
22
- * Configuration for an HTTP request used by declarative HTTP validators, derivations, and conditions.
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).
20
+ * Interface for dynamic forms logger implementations.
21
+ * Allows custom logging integrations (Sentry, DataDog, etc.)
26
22
  */
27
- interface HttpRequestConfig {
28
- /** URL to send the request to */
29
- url: string;
30
- /** HTTP method. Defaults to 'GET' */
31
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
23
+ interface Logger {
32
24
  /**
33
- * URL path parameters. Keys correspond to `:key` placeholders in the URL.
34
- * Values are expressions evaluated by ExpressionParser against the current EvaluationContext.
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
- * ```
25
+ * Log a debug message.
26
+ * Use for detailed diagnostic information during development.
44
27
  */
45
- params?: Record<string, string>;
28
+ debug(message: string, ...args: unknown[]): void;
46
29
  /**
47
- * Query parameters appended to the URL.
48
- * Values are expressions evaluated by ExpressionParser against the current EvaluationContext.
30
+ * Log an info message.
31
+ * Use for general operational information.
49
32
  */
50
- queryParams?: Record<string, string>;
51
- /** Request body (for POST/PUT/PATCH) */
52
- body?: Record<string, unknown>;
33
+ info(message: string, ...args: unknown[]): void;
53
34
  /**
54
- * When true, top-level string values in `body` are treated as expressions
55
- * and evaluated by ExpressionParser (shallow only — nested objects are passed through as-is).
35
+ * Log a warning message.
36
+ * Use for potentially problematic situations that don't prevent operation.
56
37
  */
57
- evaluateBodyExpressions?: boolean;
58
- /** HTTP headers to include in the request */
59
- headers?: Record<string, string>;
38
+ warn(message: string, ...args: unknown[]): void;
39
+ /**
40
+ * Log an error message.
41
+ * Use for error conditions that may affect functionality.
42
+ */
43
+ error(message: string, ...args: unknown[]): void;
60
44
  }
61
45
 
62
46
  /**
63
- * Comparison operators for field value and form value conditions.
47
+ * Shared shape for DI-scoped warning trackers that dedupe log output.
64
48
  *
65
- * @public
66
- */
67
- type ComparisonOperator = 'equals' | 'notEquals' | 'greater' | 'less' | 'greaterOrEqual' | 'lessOrEqual' | 'contains' | 'startsWith' | 'endsWith' | 'matches';
68
- /**
69
- * Condition that compares a specific field's value against an expected value.
49
+ * Used by both the derivation-warning tracker (keys are field paths) and the
50
+ * deprecation-warning tracker (keys are deprecation IDs). The property is
51
+ * generic so the same shape works for any string-keyed warning domain.
70
52
  *
71
- * @public
53
+ * @internal
72
54
  */
73
- interface FieldValueCondition {
74
- type: 'fieldValue';
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;
55
+ interface WarningTracker {
56
+ warnedKeys: Set<string>;
81
57
  }
58
+
82
59
  /**
83
- * Condition that invokes a registered custom function by name.
60
+ * Represents the state information of a single form field.
84
61
  *
85
- * Register functions via `customFnConfig.customFunctions`.
62
+ * Used in evaluation contexts to allow expressions and conditions
63
+ * to reference field state (e.g., `fieldState.touched`, `formFieldState.email.dirty`).
86
64
  *
87
65
  * @public
88
66
  */
89
- interface CustomCondition {
90
- type: 'custom';
91
- /** Name of the registered custom function to invoke */
92
- functionName: string;
67
+ interface FieldStateInfo {
68
+ readonly touched: boolean;
69
+ readonly dirty: boolean;
70
+ /** Convenience property: equivalent to `!dirty` */
71
+ readonly pristine: boolean;
72
+ readonly valid: boolean;
73
+ readonly invalid: boolean;
74
+ readonly pending: boolean;
75
+ /**
76
+ * Whether the field is currently hidden.
77
+ *
78
+ * Note: This is a library-managed property, not a native Angular Signal Forms
79
+ * property. Returns `false` if the field instance does not expose a `hidden` signal.
80
+ */
81
+ readonly hidden: boolean;
82
+ /**
83
+ * Whether the field is currently readonly.
84
+ *
85
+ * Note: This is a library-managed property, not a native Angular Signal Forms
86
+ * property. Returns `false` if the field instance does not expose a `readonly` signal.
87
+ */
88
+ readonly readonly: boolean;
89
+ /**
90
+ * Whether the field is currently disabled.
91
+ *
92
+ * Note: This is a library-managed property, not a native Angular Signal Forms
93
+ * property. Returns `false` if the field instance does not expose a `disabled` signal.
94
+ */
95
+ readonly disabled: boolean;
93
96
  }
94
97
  /**
95
- * Condition that evaluates a JavaScript expression using the secure AST-based parser.
98
+ * Map of field keys to their state information.
96
99
  *
97
- * The expression has access to `formValue`, `fieldValue`, `externalData`, etc.
100
+ * Used as `formFieldState` in evaluation contexts to access
101
+ * state of any field in the form by key.
98
102
  *
99
103
  * @public
100
104
  */
101
- interface JavascriptCondition {
102
- type: 'javascript';
103
- /** JavaScript expression string to evaluate */
104
- expression: string;
105
- }
105
+ type FormFieldStateMap = Record<string, FieldStateInfo | undefined>;
106
106
  /**
107
- * Condition that evaluates based on an HTTP response from a remote server.
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.
107
+ * Field state context for the current field being evaluated.
112
108
  *
113
- * Since `LogicFn` must return `boolean` synchronously, this condition uses
114
- * a signal-based async resolution pattern internally.
109
+ * Used as `fieldState` in evaluation contexts.
115
110
  *
116
111
  * @public
117
112
  */
118
- interface HttpCondition {
119
- type: 'http';
120
- /** HTTP request configuration */
121
- http: HttpRequestConfig;
113
+ type FieldStateContext = FieldStateInfo;
114
+
115
+ interface EvaluationContext<TValue = unknown, TFormValue extends Record<string, unknown> = any> {
116
+ /** Current field value */
117
+ fieldValue: TValue;
122
118
  /**
123
- * Expression to extract boolean from response (scope: `{ response }`).
124
- * e.g. `'response.allowed'`, `'response.permissions.canEdit'`
125
- * When omitted, the entire response is coerced to boolean: `!!response`
119
+ * Form value for the current evaluation scope.
120
+ *
121
+ * For regular (non-array) derivations, this contains the complete form value.
122
+ * For array item derivations, this is scoped to the current array item.
123
+ * Use `rootFormValue` to access the complete form when inside an array context.
126
124
  */
127
- responseExpression?: string;
125
+ formValue: TFormValue;
126
+ /** Field path for relative references */
127
+ fieldPath: string;
128
+ /** Custom evaluation functions */
129
+ customFunctions?: Record<string, (context: EvaluationContext) => unknown>;
130
+ /** Logger for diagnostic output */
131
+ logger: Logger;
128
132
  /**
129
- * Value to return while the HTTP request is in-flight.
133
+ * Value of the field's nearest parent **group** (or array item, when the
134
+ * field has no inner group above the array boundary).
130
135
  *
131
- * Choose based on the logic type and desired UX:
132
- * - For `hidden`: `false` = visible while loading, `true` = hidden while loading
133
- * - For `disabled`: `false` = enabled while loading, `true` = disabled while loading
134
- * - For `required`: `false` = optional while loading, `true` = required while loading
136
+ * Complements `formValue` and `fieldValue` for derivations on fields nested
137
+ * inside groups built by factory helpers, where the parent group's key
138
+ * isn't known at config-authoring time. Mirrors the runtime semantics of
139
+ * the `'$group'` token used in `dependsOn`.
135
140
  *
136
- * @default false
137
- */
138
- pendingValue?: boolean;
139
- /** Cache duration in ms for HTTP responses. @default 30000 */
140
- cacheDurationMs?: number;
141
- /**
142
- * Debounce time in milliseconds for re-evaluation when dependent form values change.
143
- * @default 300
141
+ * Resolution at evaluation time:
142
+ * - Inside a group at form root: the parent group's value object (e.g.,
143
+ * for `address.state`, `groupValue === formValue.address`).
144
+ * - Inside nested groups: the innermost parent group's value object.
145
+ * - Directly inside an array item (no inner group): the array item itself
146
+ * (same shape as `formValue` in array context).
147
+ * - Inside a group inside an array item: the inner group's value object
148
+ * within the current item.
149
+ * - Field at form root with no parent container: `undefined`.
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * // Field 'state' inside group 'address':
154
+ * deriveStateFromCountry: (ctx) => {
155
+ * const country = ctx.groupValue?.country; // no need to know parent key
156
+ * return country === 'usa' ? 'NY' : '';
157
+ * }
158
+ * ```
144
159
  */
145
- debounceMs?: number;
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;
160
+ groupValue?: unknown;
163
161
  /**
164
- * Value to return while async resolution is pending.
162
+ * Root form value when inside an array context.
165
163
  *
166
- * Choose based on the logic type and desired UX:
167
- * - For `hidden`: `false` = visible while loading, `true` = hidden while loading
168
- * - For `disabled`: `false` = enabled while loading, `true` = disabled while loading
169
- * - For `required`: `false` = optional while loading, `true` = required while loading
164
+ * This provides access to values outside the current array item.
165
+ * When a derivation targets an array item field (e.g., `items.$.lineTotal`),
166
+ * `formValue` is scoped to the current array item, while `rootFormValue`
167
+ * provides access to the entire form value including fields outside the array.
170
168
  *
171
- * @default false
169
+ * @example
170
+ * ```typescript
171
+ * // In an array item derivation (on the lineTotal field inside lineItems array):
172
+ * {
173
+ * key: 'lineTotal',
174
+ * type: 'input',
175
+ * // formValue = current array item { quantity: 2, unitPrice: 50 }
176
+ * // rootFormValue = entire form { globalDiscount: 0.1, lineItems: [...] }
177
+ * derivation: 'formValue.quantity * formValue.unitPrice * (1 - rootFormValue.globalDiscount)'
178
+ * }
179
+ * ```
180
+ *
181
+ * For non-array derivations, `rootFormValue` is not set and `formValue`
182
+ * contains the entire form value.
172
183
  */
173
- pendingValue?: boolean;
184
+ rootFormValue?: Record<string, unknown>;
174
185
  /**
175
- * Debounce ms for re-evaluation.
176
- * @default 300
186
+ * Current array index when inside an array derivation.
177
187
  */
178
- debounceMs?: number;
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';
188
+ arrayIndex?: number;
218
189
  /**
219
- * Condition that determines when this container is hidden.
220
- * Can be a boolean or a conditional expression evaluated against form values.
190
+ * Path to the array field when inside an array derivation.
221
191
  */
222
- condition: ConditionalExpression | boolean;
192
+ arrayPath?: string;
193
+ /**
194
+ * External data signals resolved to their current values.
195
+ *
196
+ * This allows forms to reference data from outside the form context
197
+ * in conditional logic, derivations, and other expressions.
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * // In form config:
202
+ * externalData: {
203
+ * userRole: computed(() => this.userService.currentRole()),
204
+ * permissions: computed(() => this.authService.permissions()),
205
+ * }
206
+ *
207
+ * // In JavaScript expression:
208
+ * condition: {
209
+ * type: 'javascript',
210
+ * expression: "externalData.userRole === 'admin'"
211
+ * }
212
+ * ```
213
+ */
214
+ externalData?: Record<string, unknown>;
215
+ /**
216
+ * DI-scoped tracker for deprecation warnings.
217
+ * Used by the condition evaluator to deduplicate deprecation warnings.
218
+ */
219
+ deprecationTracker?: WarningTracker;
220
+ /**
221
+ * State of the current field being evaluated.
222
+ *
223
+ * Provides access to field state properties like `touched`, `dirty`, `valid`, etc.
224
+ * Uses a Proxy for lazy evaluation — properties are only read when accessed.
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * // In a JavaScript expression:
229
+ * condition: {
230
+ * type: 'javascript',
231
+ * expression: "fieldState.touched && fieldState.dirty"
232
+ * }
233
+ * ```
234
+ */
235
+ fieldState?: FieldStateContext;
236
+ /**
237
+ * State of all fields in the form, keyed by field key.
238
+ *
239
+ * Provides access to any field's state properties by key.
240
+ * Uses a Proxy for lazy evaluation — field state is only read when accessed.
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * // In a JavaScript expression:
245
+ * condition: {
246
+ * type: 'javascript',
247
+ * expression: "formFieldState.email.dirty && formFieldState.email.valid"
248
+ * }
249
+ * ```
250
+ */
251
+ formFieldState?: FormFieldStateMap;
252
+ /** Allow additional properties for flexible expression evaluation */
253
+ [key: string]: unknown;
223
254
  }
224
255
 
225
256
  /**
226
- * An array item template is an array of allowed children that defines one OBJECT array item's fields.
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
257
+ * Custom function signature for conditional expressions
234
258
  *
235
- * This allows support for:
236
- * - Primitive arrays: `['tag1', 'tag2']`
237
- * - Object arrays: `[{ name: 'Alice', email: '...' }]`
238
- * - Heterogeneous arrays: `[{ value: 'x' }, 'y']` (mixed primitives and objects)
239
- */
240
- type ArrayItemDefinition = ArrayAllowedChildren | ArrayItemTemplate;
241
- /**
242
- * Array field interface for creating dynamic field collections that map to array values.
259
+ * Custom functions are used for conditional logic in:
260
+ * - `when` conditions (field visibility, conditional validation)
261
+ * - `readonly` logic (dynamic readonly state)
262
+ * - `disabled` logic (dynamic disabled state)
263
+ * - Dynamic value calculations
243
264
  *
244
- * Key concepts:
245
- * - The outer `fields` array defines INITIAL ITEMS (each element is one array item to render)
246
- * - Each element can be either:
247
- * - A single FieldDef (primitive item) - the field's value is extracted directly
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
265
+ * Unlike validators, custom functions:
266
+ * - Return any value (typically boolean for conditions, but could be strings, numbers, etc.)
267
+ * - Do NOT return ValidationError objects
268
+ * - Receive EvaluationContext (field values) not FieldContext (field state)
252
269
  *
253
270
  * @example
254
- * // Primitive array: ['angular', 'typescript']
271
+ * ```typescript
272
+ * // Boolean condition function
273
+ * const isAdult: CustomFunction = (ctx) => {
274
+ * return ctx.age >= 18;
275
+ * };
276
+ *
277
+ * // Used in field configuration:
255
278
  * {
256
- * key: 'tags',
257
- * type: 'array',
258
- * fields: [
259
- * { key: 'tag', type: 'input', value: 'angular' }, // Single field = primitive
260
- * { key: 'tag', type: 'input', value: 'typescript' },
261
- * ]
279
+ * key: 'alcoholPreference',
280
+ * type: 'select',
281
+ * when: { function: 'isAdult' } // Only show if isAdult returns true
262
282
  * }
283
+ * ```
263
284
  *
264
285
  * @example
265
- * // Object array: [{ name: 'Alice', email: '...' }]
266
- * {
267
- * key: 'contacts',
268
- * type: 'array',
269
- * fields: [
270
- * [ // Array = object
271
- * { key: 'name', type: 'input', value: 'Alice' },
272
- * { key: 'email', type: 'input', value: 'alice@example.com' }
273
- * ],
274
- * ]
275
- * }
286
+ * ```typescript
287
+ * // Calculation function
288
+ * const calculateDiscount: CustomFunction = (ctx) => {
289
+ * const price = ctx.price || 0;
290
+ * const isVip = ctx.isVip || false;
291
+ * return isVip ? price * 0.8 : price * 0.9;
292
+ * };
293
+ * ```
294
+ */
295
+ type CustomFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => unknown;
296
+
297
+ /**
298
+ * Async function for derivations — returns any value.
299
+ *
300
+ * The function receives the evaluation context and may return a Promise or Observable.
301
+ * Register functions in `customFnConfig.asyncDerivations`.
276
302
  *
277
303
  * @example
278
- * // Heterogeneous array: [{ label: 'Structured' }, 'Simple']
279
- * {
280
- * key: 'items',
281
- * type: 'array',
282
- * fields: [
283
- * [{ key: 'label', type: 'input', value: 'Structured' }], // Object item
284
- * { key: 'value', type: 'input', value: 'Simple' }, // Primitive item
285
- * ]
286
- * }
304
+ * ```typescript
305
+ * const fetchPrice: AsyncDerivationFunction = async (context) => {
306
+ * const response = await fetch(`/api/price?product=${context.formValue.productId}`);
307
+ * const data = await response.json();
308
+ * return data.price;
309
+ * };
310
+ * ```
311
+ *
312
+ * @public
313
+ */
314
+ type AsyncDerivationFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => Promise<unknown> | Observable<unknown>;
315
+ /**
316
+ * Async function for conditions — returns boolean.
317
+ *
318
+ * The function receives the evaluation context and may return a Promise or Observable.
319
+ * Register functions in `customFnConfig.asyncConditions`.
287
320
  *
288
321
  * @example
289
- * // Empty array (add items via buttons)
290
- * {
291
- * key: 'tags',
292
- * type: 'array',
293
- * fields: [] // No initial items - user adds via button with template
294
- * }
322
+ * ```typescript
323
+ * const checkPermission: AsyncConditionFunction = async (context) => {
324
+ * const response = await fetch(`/api/permissions?user=${context.formValue.userId}`);
325
+ * const data = await response.json();
326
+ * return data.canEdit;
327
+ * };
328
+ * ```
295
329
  *
296
- * Nesting constraints: Arrays can contain rows, leaf fields, or groups (for object arrays),
297
- * but NOT pages or other arrays. Runtime validation enforces these rules.
330
+ * @public
331
+ */
332
+ type AsyncConditionFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => Promise<boolean> | Observable<boolean>;
333
+
334
+ /**
335
+ * Configuration for an HTTP request used by declarative HTTP validators, derivations, and conditions.
298
336
  *
299
- * Note: Arrays are container fields and do not support `meta` since they have no native form element.
337
+ * All string values in `queryParams` are treated as expressions evaluated by ExpressionParser.
338
+ * When `evaluateBodyExpressions` is true, top-level string values in `body` are also evaluated as expressions (shallow only).
300
339
  */
301
- interface ArrayField<TFields extends readonly ArrayItemDefinition[] = readonly ArrayItemDefinition[]> extends FieldDef<never> {
302
- type: 'array';
340
+ interface HttpRequestConfig {
341
+ /** URL to send the request to */
342
+ url: string;
343
+ /** HTTP method. Defaults to 'GET' */
344
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
303
345
  /**
304
- * Array of item definitions. Each element defines one array item:
305
- * - Single FieldDef = primitive item (field's value is extracted directly)
306
- * - Array of FieldDefs = object item (fields are merged into an object)
307
- * - Empty array `[]` = no initial items (add via buttons with explicit templates)
308
- * - Items can have different configurations (heterogeneous arrays supported)
309
- * - Initial values are set via `value` property on each field definition
346
+ * URL path parameters. Keys correspond to `:key` placeholders in the URL.
347
+ * Values are expressions evaluated by ExpressionParser against the current EvaluationContext.
348
+ *
349
+ * @example
350
+ * ```typescript
351
+ * {
352
+ * url: '/api/users/:userId/orders/:orderId',
353
+ * params: { userId: 'formValue.userId', orderId: 'fieldValue' }
354
+ * }
355
+ * // With userId=42 and fieldValue='abc' → '/api/users/42/orders/abc'
356
+ * ```
310
357
  */
311
- readonly fields: TFields;
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;
358
+ params?: Record<string, string>;
316
359
  /**
317
- * Logic configurations for conditional array visibility.
318
- * Only 'hidden' type logic is supported for arrays.
360
+ * Query parameters appended to the URL.
361
+ * Values are expressions evaluated by ExpressionParser against the current EvaluationContext.
319
362
  */
320
- readonly logic?: ContainerLogicConfig[];
363
+ queryParams?: Record<string, string>;
364
+ /** Request body (for POST/PUT/PATCH) */
365
+ body?: Record<string, unknown>;
321
366
  /**
322
- * Minimum number of items required in the array.
323
- * Validation fails if the array has fewer items than this value.
324
- */
325
- readonly minLength?: number;
326
- /**
327
- * Maximum number of items allowed in the array.
328
- * Validation fails if the array has more items than this value.
367
+ * When true, top-level string values in `body` are treated as expressions
368
+ * and evaluated by ExpressionParser (shallow only — nested objects are passed through as-is).
329
369
  */
330
- readonly maxLength?: number;
370
+ evaluateBodyExpressions?: boolean;
371
+ /** HTTP headers to include in the request */
372
+ headers?: Record<string, string>;
331
373
  }
374
+
332
375
  /**
333
- * Type guard for ArrayField with proper type narrowing.
334
- * Validates that the field is an array type with a fields property that is an array.
335
- * Fields can contain either single FieldDefs (primitive items) or arrays of FieldDefs (object items).
376
+ * Comparison operators for field value and form value conditions.
377
+ *
378
+ * @public
336
379
  */
337
- declare function isArrayField(field: FieldDef<any>): field is ArrayField;
380
+ type ComparisonOperator = 'equals' | 'notEquals' | 'greater' | 'less' | 'greaterOrEqual' | 'lessOrEqual' | 'contains' | 'startsWith' | 'endsWith' | 'matches';
338
381
  /**
339
- * Configuration for auto-generated add/remove buttons in simplified array fields.
382
+ * Condition that compares a specific field's value against an expected value.
383
+ *
384
+ * @public
340
385
  */
341
- interface ArrayButtonConfig {
342
- /** Custom label for the button */
343
- readonly label?: string;
344
- /** Additional properties passed to the button component */
345
- readonly props?: Record<string, unknown>;
386
+ interface FieldValueCondition {
387
+ type: 'fieldValue';
388
+ /** Path to the field whose value to compare */
389
+ fieldPath: string;
390
+ /** Comparison operator */
391
+ operator: ComparisonOperator;
392
+ /** Value to compare against */
393
+ value?: unknown;
346
394
  }
347
395
  /**
348
- * Simplified array field interface for common use cases.
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.
396
+ * Condition that invokes a custom function.
353
397
  *
354
- * Discriminant: `template` presence → simplified API; `Array.isArray(template)` → object vs primitive.
398
+ * Two mutually exclusive forms:
399
+ * - `functionName`: name of a function registered in `customFnConfig.customFunctions`.
400
+ * JSON-serializable; suitable for configs loaded from APIs, databases, or OpenAPI.
401
+ * - `fn`: inline function. NOT JSON-serializable; for code-only configs.
355
402
  *
356
- * @example
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
- * }
403
+ * Exactly one of `functionName` or `fn` must be set.
364
404
  *
365
- * @example
366
- * // Object array: [{ name: 'Jane', phone: '555' }]
367
- * {
368
- * key: 'contacts',
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
- * }
405
+ * Encoded as a strict discriminated union (XOR via `?: never`). `CustomValidatorConfig`
406
+ * intentionally uses a permissive interface for the same `fn`/`functionName` split
407
+ * because validators have a third source (`expression`) and the historical interface
408
+ * was already runtime-checked — the asymmetry is by design, not an oversight.
376
409
  *
377
- * @example
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
- * }
410
+ * @public
386
411
  */
387
- interface SimplifiedArrayField extends FieldDef<never> {
388
- type: 'array';
412
+ type CustomCondition = {
413
+ type: 'custom';
414
+ /** Name of the registered custom function to invoke */
415
+ functionName: string;
416
+ /** Inline form is forbidden when `functionName` is set */
417
+ fn?: never;
418
+ } | {
419
+ type: 'custom';
389
420
  /**
390
- * Template defining the structure of a single array item.
391
- * - Single field (ArrayAllowedChildren) → primitive array (each item is a single value)
392
- * - Array of fields (readonly ArrayAllowedChildren[]) → object array (each item is an object)
421
+ * Inline custom function. Mutually exclusive with `functionName`.
422
+ *
423
+ * NOT JSON-serializable — for code-only configs. For configs loaded
424
+ * from JSON / OpenAPI / databases, use `functionName` to reference
425
+ * a function registered in `customFnConfig.customFunctions`.
393
426
  */
394
- readonly template: ArrayAllowedChildren | readonly ArrayAllowedChildren[];
395
- /** Initial values for the array. Each element creates one array item. */
396
- readonly value?: readonly unknown[];
427
+ fn: CustomFunction;
428
+ /** Registered form is forbidden when `fn` is set */
429
+ functionName?: never;
430
+ };
431
+ /**
432
+ * Condition that evaluates a JavaScript expression using the secure AST-based parser.
433
+ *
434
+ * The expression has access to `formValue`, `fieldValue`, `externalData`, etc.
435
+ *
436
+ * @public
437
+ */
438
+ interface JavascriptCondition {
439
+ type: 'javascript';
440
+ /** JavaScript expression string to evaluate */
441
+ expression: string;
442
+ }
443
+ /**
444
+ * Condition that evaluates based on an HTTP response from a remote server.
445
+ *
446
+ * The HTTP request is resolved reactively — when dependent form values change,
447
+ * the request is re-evaluated (with debouncing). The result is cached per
448
+ * resolved request to avoid redundant network calls.
449
+ *
450
+ * Since `LogicFn` must return `boolean` synchronously, this condition uses
451
+ * a signal-based async resolution pattern internally.
452
+ *
453
+ * @public
454
+ */
455
+ interface HttpCondition {
456
+ type: 'http';
457
+ /** HTTP request configuration */
458
+ http: HttpRequestConfig;
397
459
  /**
398
- * Configuration for the auto-generated "Add" button, or `false` to disable it.
399
- * Defaults to a button with label "Add".
460
+ * Expression to extract boolean from response (scope: `{ response }`).
461
+ * e.g. `'response.allowed'`, `'response.permissions.canEdit'`
462
+ * When omitted, the entire response is coerced to boolean: `!!response`
400
463
  */
401
- readonly addButton?: ArrayButtonConfig | false;
464
+ responseExpression?: string;
402
465
  /**
403
- * Configuration for the auto-generated "Remove" button on each item, or `false` to disable it.
404
- * Defaults to a button with label "Remove".
466
+ * Value to return while the HTTP request is in-flight.
467
+ *
468
+ * Choose based on the logic type and desired UX:
469
+ * - For `hidden`: `false` = visible while loading, `true` = hidden while loading
470
+ * - For `disabled`: `false` = enabled while loading, `true` = disabled while loading
471
+ * - For `required`: `false` = optional while loading, `true` = required while loading
472
+ *
473
+ * @default false
405
474
  */
406
- readonly removeButton?: ArrayButtonConfig | false;
407
- /** Simplified arrays do not support the label property */
408
- readonly label?: never;
409
- /** Simplified arrays do not support meta */
410
- readonly meta?: never;
475
+ pendingValue?: boolean;
476
+ /** Cache duration in ms for HTTP responses. @default 30000 */
477
+ cacheDurationMs?: number;
411
478
  /**
412
- * Logic configurations for conditional array visibility.
413
- * Only 'hidden' type logic is supported for arrays.
479
+ * Debounce time in milliseconds for re-evaluation when dependent form values change.
480
+ * @default 300
414
481
  */
415
- readonly logic?: ContainerLogicConfig[];
482
+ debounceMs?: number;
483
+ }
484
+ /**
485
+ * Shared fields for both branches of {@link AsyncCondition}.
486
+ *
487
+ * @internal
488
+ */
489
+ interface AsyncConditionBase {
490
+ type: 'async';
416
491
  /**
417
- * Minimum number of items required in the array.
418
- * Validation fails if the array has fewer items than this value.
492
+ * Value to return while async resolution is pending.
493
+ *
494
+ * Choose based on the logic type and desired UX:
495
+ * - For `hidden`: `false` = visible while loading, `true` = hidden while loading
496
+ * - For `disabled`: `false` = enabled while loading, `true` = disabled while loading
497
+ * - For `required`: `false` = optional while loading, `true` = required while loading
498
+ *
499
+ * @default false
419
500
  */
420
- readonly minLength?: number;
501
+ pendingValue?: boolean;
421
502
  /**
422
- * Maximum number of items allowed in the array.
423
- * Validation fails if the array has more items than this value.
503
+ * Debounce ms for re-evaluation.
504
+ * @default 300
424
505
  */
425
- readonly maxLength?: number;
426
- /** Mutually exclusive with `template` — use `fields` for the full API instead */
427
- readonly fields?: never;
506
+ debounceMs?: number;
428
507
  }
429
508
  /**
430
- * Type guard for SimplifiedArrayField.
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
509
+ * Condition that resolves asynchronously via a custom function.
439
510
  *
440
- * TypeScript cannot enforce field nesting rules due to circular dependency limitations.
441
- * For documentation: Groups should contain rows and leaf fields, but NOT pages or other groups.
442
- * Runtime validation enforces these rules.
511
+ * Two mutually exclusive forms:
512
+ * - `asyncFunctionName`: name of a function registered in `customFnConfig.asyncConditions`.
513
+ * JSON-serializable; suitable for configs loaded from APIs, databases, or OpenAPI.
514
+ * - `asyncFn`: inline async function. NOT JSON-serializable; for code-only configs.
443
515
  *
444
- * Note: Groups are container fields and do not support `meta` since they have no native form element.
516
+ * Exactly one of `asyncFunctionName` or `asyncFn` must be set.
517
+ *
518
+ * The function is resolved reactively — when dependent form values change,
519
+ * the function is re-evaluated (with debouncing). The result is cached per
520
+ * evaluation to avoid redundant calls.
521
+ *
522
+ * Since `LogicFn` must return `boolean` synchronously, this condition uses
523
+ * a signal-based async resolution pattern internally.
524
+ *
525
+ * @public
445
526
  */
446
- interface GroupField<TFields extends readonly GroupAllowedChildren[] = readonly GroupAllowedChildren[]> extends FieldDef<never> {
447
- type: 'group';
448
- readonly fields: TFields;
449
- /** Groups do not have a label property - they are logical containers only **/
450
- readonly label?: never;
451
- /** Groups do not support meta - they have no native form element **/
452
- readonly meta?: never;
527
+ type AsyncCondition = (AsyncConditionBase & {
528
+ /** Name of the registered async condition function */
529
+ asyncFunctionName: string;
530
+ /** Inline form is forbidden when `asyncFunctionName` is set */
531
+ asyncFn?: never;
532
+ }) | (AsyncConditionBase & {
453
533
  /**
454
- * Logic configurations for conditional group visibility.
455
- * Only 'hidden' type logic is supported for groups.
534
+ * Inline async condition function. Mutually exclusive with `asyncFunctionName`.
535
+ *
536
+ * NOT JSON-serializable — for code-only configs. For configs loaded
537
+ * from JSON / OpenAPI / databases, use `asyncFunctionName` to reference
538
+ * a function registered in `customFnConfig.asyncConditions`.
456
539
  */
457
- readonly logic?: ContainerLogicConfig[];
540
+ asyncFn: AsyncConditionFunction;
541
+ /** Registered form is forbidden when `asyncFn` is set */
542
+ asyncFunctionName?: never;
543
+ });
544
+ /**
545
+ * Logical AND — all sub-conditions must be true.
546
+ *
547
+ * @public
548
+ */
549
+ interface AndCondition {
550
+ type: 'and';
551
+ /** Sub-conditions that must all evaluate to true */
552
+ conditions: ConditionalExpression[];
458
553
  }
459
554
  /**
460
- * Type guard for GroupField with proper type narrowing
555
+ * Logical OR — at least one sub-condition must be true.
556
+ *
557
+ * @public
461
558
  */
462
- declare function isGroupField(field: FieldDef<any>): field is GroupField;
463
-
559
+ interface OrCondition {
560
+ type: 'or';
561
+ /** Sub-conditions where at least one must evaluate to true */
562
+ conditions: ConditionalExpression[];
563
+ }
464
564
  /**
465
- * Scalar types supported by hidden fields.
466
- * These are the primitive value types that can be stored.
565
+ * Discriminated union of all conditional expression types.
566
+ *
567
+ * Each variant only allows the properties relevant to its type,
568
+ * providing compile-time safety against invalid property combinations.
569
+ *
570
+ * @public
467
571
  */
468
- type HiddenScalar = string | number | boolean;
572
+ type ConditionalExpression = FieldValueCondition | CustomCondition | JavascriptCondition | HttpCondition | AsyncCondition | AndCondition | OrCondition;
573
+
469
574
  /**
470
- * All value types supported by hidden fields.
471
- * Supports both scalar values and arrays of scalars for storing
472
- * multiple IDs or similar data.
575
+ * Logic configuration for container fields (page, group, row, array).
576
+ * Containers only support the 'hidden' logic type since they are layout containers,
577
+ * not form controls (they can't be readonly, disabled, or required).
473
578
  */
474
- type HiddenValue = HiddenScalar | HiddenScalar[];
579
+ interface ContainerLogicConfig {
580
+ /** Only 'hidden' is supported for container fields */
581
+ type: 'hidden';
582
+ /**
583
+ * Condition that determines when this container is hidden.
584
+ * Can be a boolean or a conditional expression evaluated against form values.
585
+ */
586
+ condition: ConditionalExpression | boolean;
587
+ }
588
+
475
589
  /**
476
- * Hidden field definition for storing values without rendering UI.
477
- *
478
- * Hidden fields participate in the form schema and contribute to form values,
479
- * but do not render any visible component. They are useful for:
480
- * - Storing IDs when updating existing records
481
- * - Persisting metadata that shouldn't be user-editable
482
- * - Tracking computed values that need to be submitted
483
- *
484
- * Unlike the `hidden` property on other fields (which hides a rendered field),
485
- * this field type renders nothing at all - it's a componentless field.
590
+ * Row field interface for creating horizontal layouts.
591
+ * A row is a synthetic field type that resolves to a Container at runtime,
592
+ * with a synthesized `{ type: 'row' }` wrapper applied for layout. The row
593
+ * itself does not hold a value — its children flatten into the parent form.
486
594
  *
487
- * @example
488
- * ```typescript
489
- * // Store a record ID for updates
490
- * const idField: HiddenField<string> = {
491
- * type: 'hidden',
492
- * key: 'id',
493
- * value: '550e8400-e29b-41d4-a716-446655440000',
494
- * };
595
+ * Note: Rows do not support `meta` since they have no native form element.
596
+ */
597
+ interface RowField<TFields extends readonly RowAllowedChildren[] = readonly RowAllowedChildren[]> extends FieldDef<never> {
598
+ type: 'row';
599
+ /** Child definitions to render within this row */
600
+ readonly fields: TFields;
601
+ /** Row fields do not have a label property **/
602
+ readonly label?: never;
603
+ /** Rows do not support meta - they have no native form element **/
604
+ readonly meta?: never;
605
+ /**
606
+ * Logic configurations for conditional row visibility.
607
+ * Only 'hidden' type logic is supported for rows.
608
+ */
609
+ readonly logic?: ContainerLogicConfig[];
610
+ }
611
+ /**
612
+ * Type guard for RowField with proper type narrowing
613
+ */
614
+ declare function isRowField(field: FieldDef<any>): field is RowField;
615
+
616
+ /**
617
+ * Group field interface for creating logical field groupings that map to object values
618
+ * Groups create nested form structures where child field values are collected into an object
619
+ * This is a programmatic grouping only - users cannot customize this field type
495
620
  *
496
- * // Store multiple tag IDs
497
- * const tagIdsField: HiddenField<number[]> = {
498
- * type: 'hidden',
499
- * key: 'tagIds',
500
- * value: [1, 2, 3],
501
- * };
502
- * ```
621
+ * TypeScript cannot enforce field nesting rules due to circular dependency limitations.
622
+ * For documentation: Groups should contain rows and leaf fields, but NOT pages or other groups.
623
+ * Runtime validation enforces these rules.
503
624
  *
504
- * @typeParam TValue - The type of value stored (must be HiddenValue compatible)
625
+ * Note: Groups are container fields and do not support `meta` since they have no native form element.
505
626
  */
506
- interface HiddenField<TValue extends HiddenValue = HiddenValue> extends FieldDef<never> {
507
- /** Discriminant for hidden field type */
508
- type: 'hidden';
627
+ interface GroupField<TFields extends readonly GroupAllowedChildren[] = readonly GroupAllowedChildren[]> extends FieldDef<never> {
628
+ type: 'group';
629
+ readonly fields: TFields;
630
+ /** Groups do not have a label property - they are logical containers only **/
631
+ readonly label?: never;
632
+ /** Groups do not support meta - they have no native form element **/
633
+ readonly meta?: never;
509
634
  /**
510
- * The value to store in the form.
511
- * This is required - a hidden field without a value serves no purpose.
635
+ * Logic configurations for conditional group visibility.
636
+ * Only 'hidden' type logic is supported for groups.
512
637
  */
513
- value: TValue;
638
+ readonly logic?: ContainerLogicConfig[];
514
639
  }
640
+ /**
641
+ * Type guard for GroupField with proper type narrowing
642
+ */
643
+ declare function isGroupField(field: FieldDef<any>): field is GroupField;
515
644
 
516
645
  /**
517
646
  * Page field interface for creating top-level page layouts
@@ -722,1442 +851,1254 @@ type ExtractFields<T> = T extends {
722
851
  type InferFormValue$1<T> = ExtractFields<T> extends readonly RegisteredFieldTypes[] ? InferFormValueWithDepth<ExtractFields<T> extends RegisteredFieldTypes[] ? ExtractFields<T> : [...ExtractFields<T>], 5> : never;
723
852
 
724
853
  /**
725
- * Mapping configuration for interpreting HTTP responses as validation results.
726
- *
727
- * Expressions in `validWhen` and `errorParams` are evaluated with scope `{ response }` only —
728
- * they do NOT have access to `formValue`, `fieldValue`, or the full `EvaluationContext`.
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
854
+ * Custom validator function signature using Angular's public FieldContext API
770
855
  *
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>.
856
+ * Takes FieldContext (full Angular context) and optional params, returns validation error(s) or null.
857
+ * Provides access to field state, form hierarchy, and other fields for validation logic.
802
858
  *
803
- */
804
- interface AsyncValidatorConfig extends BaseValidatorConfig {
805
- /** Validator type identifier. */
806
- type: 'async';
807
- /** Name of registered async validator function */
808
- functionName: string;
809
- /** Optional parameters to pass to validator function */
810
- params?: Record<string, unknown>;
811
- }
812
- /**
813
- * Function-based HTTP validator configuration — requires a registered function.
859
+ * **Use FieldContext public APIs to access:**
860
+ * - Current field value: `ctx.value()`
861
+ * - Field state: `ctx.state` (errors, touched, dirty, etc.)
862
+ * - Other field values: `ctx.valueOf(path)` where path is a FieldPath
863
+ * - Other field states: `ctx.stateOf(path)`
864
+ * - Other fields: `ctx.fieldTreeOf(path)`
865
+ * - Current field tree: `ctx.fieldTree`
814
866
  *
815
- * Uses Angular's `validateHttp` API. The function is registered via
816
- * `customFnConfig.httpValidators`.
867
+ * **Return Types:**
868
+ * - Single error: `{ kind: 'errorKind' }` for field-level validation
869
+ * - Multiple errors: `[{ kind: 'error1' }, { kind: 'error2' }]` for cross-field validation
870
+ * - No error: `null` when validation passes
817
871
  *
818
- * Discriminated from `DeclarativeHttpValidatorConfig` by the presence of `functionName`.
872
+ * **Best Practice - Return Only Error Kind:**
873
+ * Validators should focus on validation logic, not presentation.
874
+ * Return just the error `kind` and configure messages at field level for i18n support.
819
875
  *
820
- */
821
- interface FunctionHttpValidatorConfig extends BaseValidatorConfig {
822
- /** Validator type identifier. */
823
- type: 'http';
824
- /** Name of registered HTTP validator configuration */
825
- functionName: string;
826
- /** Optional parameters to pass to HTTP validator */
827
- params?: Record<string, unknown>;
828
- }
829
- /**
830
- * Declarative HTTP validator configuration — fully JSON-serializable, no function registration required.
876
+ * @example Single Field Validation
877
+ * ```typescript
878
+ * const noSpaces: CustomValidator<string> = (ctx) => {
879
+ * const value = ctx.value();
880
+ * if (typeof value === 'string' && value.includes(' ')) {
881
+ * return { kind: 'noSpaces' };
882
+ * }
883
+ * return null;
884
+ * };
831
885
  *
832
- * Uses `HttpRequestConfig` to define the HTTP request and `HttpValidationResponseMapping`
833
- * to interpret the response as a validation result. Powered by Angular's `validateHttp` API.
886
+ * // Field configuration
887
+ * {
888
+ * key: 'username',
889
+ * validators: [{ type: 'custom', functionName: 'noSpaces' }],
890
+ * validationMessages: {
891
+ * noSpaces: 'Spaces are not allowed'
892
+ * }
893
+ * }
894
+ * ```
834
895
  *
835
- * Discriminated from `FunctionHttpValidatorConfig` by the presence of `http` + `responseMapping`
836
- * (and absence of `functionName`).
837
- */
838
- interface DeclarativeHttpValidatorConfig extends BaseValidatorConfig {
839
- /** Validator type identifier */
840
- type: 'http';
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.
896
+ * @example Cross-Field Validation with valueOf()
897
+ * ```typescript
898
+ * // Compare two fields using public API
899
+ * const lessThan: CustomValidator<number> = (ctx, params) => {
900
+ * const value = ctx.value();
901
+ * const compareToPath = params?.field as string;
849
902
  *
850
- * Note: `FunctionHttpValidatorConfig` and `DeclarativeHttpValidatorConfig` both use `type: 'http'`.
851
- * They are discriminated by property presence: `functionName` → function-based, `http` → declarative.
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.
903
+ * // Use valueOf() to access other field - public API!
904
+ * const otherValue = ctx.valueOf(compareToPath as any);
858
905
  *
859
- * These conditions evaluate form or page-level state rather than field values,
860
- * and are primarily used for controlling button disabled states.
906
+ * if (otherValue !== undefined && value >= otherValue) {
907
+ * return { kind: 'notLessThan' };
908
+ * }
909
+ * return null;
910
+ * };
911
+ * ```
861
912
  *
862
- * @example
913
+ * @example Multiple Errors (Cross-Field Validation)
863
914
  * ```typescript
864
- * // Disable submit button when form is invalid or submitting
865
- * {
866
- * key: 'submit',
867
- * type: 'submit',
868
- * label: 'Submit',
869
- * logic: [
870
- * { type: 'disabled', condition: 'formInvalid' },
871
- * { type: 'disabled', condition: 'formSubmitting' },
872
- * ]
873
- * }
874
- *
875
- * // Disable next button when current page is invalid
876
- * nextButton({
877
- * key: 'next',
878
- * label: 'Next',
879
- * logic: [
880
- * { type: 'disabled', condition: 'pageInvalid' },
881
- * ]
882
- * })
915
+ * const validateDateRange: CustomValidator = (ctx) => {
916
+ * const errors: ValidationError[] = [];
917
+ *
918
+ * const startDate = ctx.valueOf('startDate' as any);
919
+ * const endDate = ctx.valueOf('endDate' as any);
920
+ *
921
+ * if (!startDate) errors.push({ kind: 'startDateRequired' });
922
+ * if (!endDate) errors.push({ kind: 'endDateRequired' });
923
+ * if (startDate && endDate && startDate > endDate) {
924
+ * errors.push({ kind: 'invalidDateRange' });
925
+ * }
926
+ *
927
+ * return errors.length > 0 ? errors : null;
928
+ * };
883
929
  * ```
884
930
  *
885
- * @public
886
- */
887
- type FormStateCondition =
888
- /** True when form.valid() === false */
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).
931
+ * **Message Resolution Priority:**
932
+ * 1. Field-level `validationMessages[kind]` (highest priority - per-field customization)
933
+ * 2. Form-level `defaultValidationMessages[kind]` (fallback for common messages)
934
+ * 3. **No message configured = Warning logged + error NOT displayed**
896
935
  *
897
- * @public
936
+ * **Important:** Validator-returned messages are NOT used. All messages MUST be explicitly configured.
937
+ *
938
+ * @template TValue The type of value stored in the field being validated
898
939
  */
899
- type StateLogicType = 'hidden' | 'readonly' | 'disabled' | 'required';
940
+ type CustomValidator<TValue = unknown> = (ctx: FieldContext<TValue>, params?: Record<string, unknown>) => ValidationError$1 | ValidationError$1[] | null;
900
941
  /**
901
- * Base configuration for conditional field state logic.
942
+ * Async custom validator configuration using Angular's resource-based API
902
943
  *
903
- * @internal
944
+ * Angular's validateAsync uses the resource API for async validation,
945
+ * which provides better integration with Signal Forms lifecycle management.
946
+ *
947
+ * **Structure:**
948
+ * - `params`: Function that computes params from field context
949
+ * - `factory`: Function that creates a ResourceRef from params signal
950
+ * - `onSuccess`: Maps successful resource result to validation errors
951
+ * - `onError`: Optional handler for resource errors (network failures, etc.)
952
+ *
953
+ * **Use Cases:**
954
+ * - Database lookups via services with resource API
955
+ * - Complex async business logic with Angular resources
956
+ * - Any async operation using Angular's resource pattern
957
+ *
958
+ * **Note:** For HTTP validation, prefer `HttpCustomValidator` which provides
959
+ * a simpler API specifically designed for HTTP requests.
960
+ *
961
+ * @example Database Lookup with Resource
962
+ * ```typescript
963
+ * const checkUsernameAvailable: AsyncCustomValidator<string> = {
964
+ * params: (ctx) => ({ username: ctx.value() }),
965
+ * factory: (params) => {
966
+ * const injector = inject(Injector);
967
+ * return resource({
968
+ * request: () => params(),
969
+ * loader: ({ request }) => {
970
+ * if (!request?.username) return null;
971
+ * const service = injector.get(UserService);
972
+ * return firstValueFrom(service.checkAvailability(request.username));
973
+ * }
974
+ * });
975
+ * },
976
+ * onSuccess: (result, ctx) => {
977
+ * if (!result) return null;
978
+ * return result.available ? null : { kind: 'usernameTaken' };
979
+ * },
980
+ * onError: (error, ctx) => {
981
+ * console.error('Availability check failed:', error);
982
+ * return null; // Don't block form on network errors
983
+ * }
984
+ * };
985
+ * ```
986
+ *
987
+ * @template TValue The type of value stored in the field being validated
988
+ * @template TParams The type of params passed to the resource
989
+ * @template TResult The type of result returned by the resource
904
990
  */
905
- interface BaseStateLogicConfig {
991
+ interface AsyncCustomValidator<TValue = unknown, TParams = unknown, TResult = unknown> {
906
992
  /**
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
993
+ * Function that receives field context and returns resource params.
994
+ * Params will be tracked as a signal and trigger resource reload when changed.
913
995
  */
914
- type: StateLogicType;
996
+ readonly params: (ctx: FieldContext<TValue>, config?: Record<string, unknown>) => TParams;
915
997
  /**
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
- * ```
998
+ * Function that creates a ResourceRef from the params signal.
999
+ * The resource will be automatically managed by Angular's lifecycle.
939
1000
  */
940
- condition: ConditionalExpression | boolean | FormStateCondition;
941
- }
942
- /**
943
- * State logic that evaluates immediately on change (default).
944
- *
945
- * @internal
946
- */
947
- interface ImmediateStateLogicConfig extends BaseStateLogicConfig {
1001
+ readonly factory: (params: Signal<TParams | undefined>) => ResourceRef<TResult | undefined>;
948
1002
  /**
949
- * Trigger for immediate evaluation.
950
- * @default 'onChange'
1003
+ * Optional function to map successful resource result to validation errors.
1004
+ * Return null if validation passes, or ValidationError/ValidationError[] if validation fails.
951
1005
  */
952
- trigger?: 'onChange';
953
- /** Not allowed for onChange trigger */
954
- debounceMs?: never;
1006
+ readonly onSuccess?: (result: TResult, ctx: FieldContext<TValue>) => TreeValidationResult;
1007
+ /**
1008
+ * Optional error handler for resource errors (network failures, etc.).
1009
+ * Return null to ignore the error, or ValidationError to display it to the user.
1010
+ */
1011
+ readonly onError?: (error: unknown, ctx: FieldContext<TValue>) => ValidationError$1 | ValidationError$1[] | null;
955
1012
  }
956
1013
  /**
957
- * State logic that evaluates after a debounce period.
958
- *
959
- * @internal
1014
+ * HTTP request configuration for validateHttp API
960
1015
  */
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;
1016
+ interface HttpResourceRequest {
1017
+ url: string;
1018
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
1019
+ body?: unknown;
1020
+ headers?: Record<string, string | string[]>;
972
1021
  }
973
1022
  /**
974
- * Configuration for conditional field state logic.
1023
+ * HTTP-based validator configuration for Angular's validateHttp API
975
1024
  *
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.
1025
+ * Angular's validateHttp provides optimized HTTP validation with automatic
1026
+ * request cancellation and integration with the resource API.
979
1027
  *
980
- * @example
1028
+ * **Benefits:**
1029
+ * - Automatic request cancellation when field value changes
1030
+ * - Built-in integration with Angular's resource management
1031
+ * - Simpler than AsyncCustomValidator for HTTP use cases
1032
+ *
1033
+ * **Structure:**
1034
+ * - `request`: Function that returns URL string or HttpResourceRequest
1035
+ * - `onSuccess`: REQUIRED - Maps HTTP response to validation errors (inverted logic!)
1036
+ * - `onError`: Optional handler for HTTP errors (network failures, 4xx/5xx)
1037
+ *
1038
+ * **Important:** `onSuccess` uses inverted logic - it maps SUCCESSFUL HTTP responses
1039
+ * to validation errors. For example, if the API returns `{ available: false }`,
1040
+ * your `onSuccess` should return `{ kind: 'usernameTaken' }`.
1041
+ *
1042
+ * @example Username Availability Check (GET)
981
1043
  * ```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'
1044
+ * const checkUsername: HttpCustomValidator<string> = {
1045
+ * request: (ctx) => {
1046
+ * const username = ctx.value();
1047
+ * if (!username) return undefined; // Skip validation if empty
1048
+ * return `/api/users/check-username?username=${encodeURIComponent(username)}`;
1049
+ * },
1050
+ * onSuccess: (response, ctx) => {
1051
+ * // Inverted logic: successful response may indicate validation failure
1052
+ * return response.available ? null : { kind: 'usernameTaken' };
1053
+ * },
1054
+ * onError: (error, ctx) => {
1055
+ * console.error('Availability check failed:', error);
1056
+ * return null; // Don't block form on network errors
990
1057
  * }
991
- * }
1058
+ * };
1059
+ * ```
992
1060
  *
993
- * // Disable button when form is submitting
994
- * {
995
- * type: 'disabled',
996
- * condition: 'formSubmitting'
997
- * }
1061
+ * @example Address Validation (POST with Body)
1062
+ * ```typescript
1063
+ * const validateAddress: HttpCustomValidator = {
1064
+ * request: (ctx) => {
1065
+ * const zipCode = ctx.value();
1066
+ * if (!zipCode) return undefined;
998
1067
  *
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'
1068
+ * return {
1069
+ * url: '/api/validate-address',
1070
+ * method: 'POST',
1071
+ * body: {
1072
+ * street: ctx.valueOf('street' as any),
1073
+ * city: ctx.valueOf('city' as any),
1074
+ * zipCode: zipCode
1075
+ * },
1076
+ * headers: { 'Content-Type': 'application/json' }
1077
+ * };
1078
+ * },
1079
+ * onSuccess: (response) => {
1080
+ * return response.valid ? null : { kind: 'invalidAddress' };
1008
1081
  * }
1009
- * }
1082
+ * };
1010
1083
  * ```
1011
1084
  *
1012
- * @public
1085
+ * @template TValue The type of value stored in the field being validated
1086
+ * @template TResult The type of HTTP response
1013
1087
  */
1014
- type StateLogicConfig = ImmediateStateLogicConfig | DebouncedStateLogicConfig;
1015
- /**
1016
- * Shared fields that appear on all derivation logic config variants.
1017
- *
1018
- * @internal
1019
- */
1020
- interface SharedDerivationFields {
1088
+ interface HttpCustomValidator<TValue = unknown, TResult = unknown> {
1021
1089
  /**
1022
- * Logic type identifier for value derivation.
1090
+ * Function that returns the HTTP request configuration.
1091
+ * Can return:
1092
+ * - `undefined` to skip validation (e.g., if field is empty)
1093
+ * - `string` for simple GET requests (just the URL)
1094
+ * - `HttpResourceRequest` for full control (method, body, headers)
1023
1095
  */
1024
- type: 'derivation';
1096
+ readonly request: (ctx: FieldContext<TValue>, config?: Record<string, unknown>) => string | HttpResourceRequest | undefined;
1025
1097
  /**
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.
1098
+ * REQUIRED function to map successful HTTP response to validation errors.
1036
1099
  *
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
- * }
1100
+ * **Inverted Logic:** This is called on successful HTTP responses.
1101
+ * Return null if validation passes, or ValidationError/ValidationError[] if validation fails.
1049
1102
  *
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
- * ```
1103
+ * Example: API returns `{ available: false }` → return `{ kind: 'usernameTaken' }`
1060
1104
  */
1061
- targetProperty?: string;
1105
+ readonly onSuccess: (result: TResult, ctx: FieldContext<TValue>) => TreeValidationResult;
1062
1106
  /**
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.
1107
+ * Optional error handler for HTTP errors (network failures, 4xx/5xx status codes).
1108
+ * Return null to ignore the error, or ValidationError to display it to the user.
1067
1109
  *
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
- * ```
1110
+ * Common pattern: Log the error and return null to avoid blocking form submission
1111
+ * on network issues.
1080
1112
  */
1081
- debugName?: string;
1113
+ readonly onError?: (error: unknown, ctx: FieldContext<TValue>) => ValidationError$1 | ValidationError$1[] | null;
1114
+ }
1115
+
1116
+ /**
1117
+ * Mapping configuration for interpreting HTTP responses as validation results.
1118
+ *
1119
+ * Expressions in `validWhen` and `errorParams` are evaluated with scope `{ response }` only —
1120
+ * they do NOT have access to `formValue`, `fieldValue`, or the full `EvaluationContext`.
1121
+ * This is intentional for v1: response mapping is purely about interpreting the HTTP response.
1122
+ */
1123
+ interface HttpValidationResponseMapping {
1082
1124
  /**
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
1125
+ * Expression evaluated with scope `{ response }`. Must evaluate to `true` (strict boolean) to be valid.
1097
1126
  *
1098
- * // Conditional derivation
1099
- * condition: {
1100
- * type: 'fieldValue',
1101
- * fieldPath: 'country',
1102
- * operator: 'equals',
1103
- * value: 'USA'
1104
- * }
1105
- * ```
1127
+ * NOTE: This is the OPPOSITE of the function-based HttpCustomValidator.onSuccess
1128
+ * convention, which maps successful HTTP responses to validation errors.
1129
+ * Here, `=== true` means "validation passed". Non-boolean results are treated as invalid and trigger a warning.
1106
1130
  */
1107
- condition?: ConditionalExpression | boolean;
1131
+ validWhen: string;
1132
+ /** Error kind returned when validation fails — maps to field-level `validationMessages` */
1133
+ errorKind: string;
1108
1134
  /**
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
- * ```
1135
+ * Parameters to include in the validation error for message interpolation.
1136
+ * Map of parameter names to expressions evaluated against `{ response }`.
1131
1137
  */
1132
- stopOnUserOverride?: boolean;
1138
+ errorParams?: Record<string, string>;
1139
+ }
1140
+
1141
+ /**
1142
+ * Base configuration shared by all validators
1143
+ */
1144
+ interface BaseValidatorConfig {
1145
+ /** Conditional logic for when validator applies */
1146
+ when?: ConditionalExpression;
1147
+ }
1148
+ /**
1149
+ * Built-in validator configuration (required, email, min, max, etc.)
1150
+ */
1151
+ interface BuiltInValidatorConfig extends BaseValidatorConfig {
1152
+ /** Validator type identifier */
1153
+ type: 'required' | 'email' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern';
1154
+ /** Static value for the validator (e.g., min value, pattern) */
1155
+ value?: number | string | RegExp;
1156
+ /** Dynamic value expression that evaluates to validator parameter */
1157
+ expression?: string;
1158
+ }
1159
+ /**
1160
+ * Custom validator configuration using Angular's public FieldContext API.
1161
+ * Returns ValidationError | ValidationError[] | null synchronously.
1162
+ *
1163
+ * Three authoring forms (mutually exclusive at runtime, not enforced by TypeScript):
1164
+ * 1. Registered function: `{ type: 'custom', functionName: 'myValidator' }`
1165
+ * 2. Inline function (code-only): `{ type: 'custom', fn: (ctx) => ... }`
1166
+ * 3. Expression-based: `{ type: 'custom', expression: 'fieldValue === formValue.password', kind: 'passwordMismatch' }`
1167
+ *
1168
+ * Unlike `AsyncValidatorConfig` and `FunctionHttpValidatorConfig` (strict `fn` ↔
1169
+ * `functionName` XOR via discriminated unions), this surface keeps a permissive
1170
+ * interface — the historical expression-vs-functionName split was already
1171
+ * runtime-checked, so adding `fn` follows the same precedent. The runtime
1172
+ * resolver picks one source and warns if `fn` and `functionName` are both set;
1173
+ * inline `fn` wins.
1174
+ *
1175
+ * `fn` is NOT JSON-serializable — for code-only configs. For configs loaded from
1176
+ * JSON / OpenAPI / databases, prefer `functionName` to reference a function
1177
+ * registered in `customFnConfig.validators`.
1178
+ */
1179
+ interface CustomValidatorConfig extends BaseValidatorConfig {
1180
+ /** Validator type identifier */
1181
+ type: 'custom';
1182
+ /** Name of registered validator function (function-based pattern) */
1183
+ functionName?: string;
1133
1184
  /**
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.
1185
+ * Inline custom validator (code-only authoring).
1186
+ * Mutually exclusive with `functionName`. NOT JSON-serializable.
1187
+ */
1188
+ fn?: CustomValidator;
1189
+ /** Optional parameters to pass to validator function */
1190
+ params?: Record<string, unknown>;
1191
+ /** JavaScript expression to evaluate (expression-based pattern) */
1192
+ expression?: string;
1193
+ /** Error kind for expression-based validators - links to validationMessages */
1194
+ kind?: string;
1195
+ /**
1196
+ * Parameters to include in error object for message interpolation (expression-based pattern)
1197
+ * Map of parameter names to expressions that will be evaluated in the same context as the validation expression
1141
1198
  *
1142
1199
  * @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
- * }]
1200
+ * errorParams: {
1201
+ * minValue: 'formValue.minValue',
1202
+ * maxValue: 'formValue.maxValue'
1154
1203
  * }
1155
- * ```
1204
+ * // Allows message template: "Value must be between {{minValue}} and {{maxValue}}"
1156
1205
  */
1157
- reEngageOnDependencyChange?: boolean;
1206
+ errorParams?: Record<string, string>;
1158
1207
  }
1159
1208
  /**
1160
- * Trigger variants for derivation timing.
1209
+ * Shared fields for both branches of {@link AsyncValidatorConfig}.
1210
+ *
1161
1211
  * @internal
1162
1212
  */
1163
- type ImmediateDerivationTrigger = {
1164
- trigger?: 'onChange';
1165
- debounceMs?: never;
1166
- };
1167
- type DebouncedDerivationTrigger = {
1168
- trigger: 'debounced';
1169
- debounceMs?: number;
1170
- };
1213
+ interface AsyncValidatorConfigShared extends BaseValidatorConfig {
1214
+ /** Validator type identifier. */
1215
+ type: 'async';
1216
+ /** Optional parameters to pass to validator function */
1217
+ params?: Record<string, unknown>;
1218
+ }
1171
1219
  /**
1172
- * Base for HTTP derivations. `source: 'http'` is required.
1173
- * TypeScript enforces `dependsOn` and `responseExpression` at the type level.
1220
+ * Async custom validator configuration using Angular's `validateAsync` API.
1221
+ * Returns Observable<ValidationError | ValidationError[] | null>.
1174
1222
  *
1175
- * @internal
1223
+ * Two mutually exclusive authoring forms:
1224
+ * - `functionName`: name of a function registered in `customFnConfig.asyncValidators`.
1225
+ * JSON-serializable; suitable for configs loaded from APIs, databases, or OpenAPI.
1226
+ * - `fn`: inline async validator. NOT JSON-serializable; for code-only configs.
1227
+ *
1228
+ * Exactly one of `functionName` or `fn` must be set.
1176
1229
  */
1177
- interface HttpDerivationBase extends SharedDerivationFields {
1178
- /** Identifies this derivation as HTTP-driven. */
1179
- source: 'http';
1230
+ type AsyncValidatorConfig = (AsyncValidatorConfigShared & {
1231
+ /** Name of registered async validator function */
1232
+ functionName: string;
1233
+ /** Inline form is forbidden when `functionName` is set */
1234
+ fn?: never;
1235
+ }) | (AsyncValidatorConfigShared & {
1180
1236
  /**
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.
1237
+ * Inline async validator. Mutually exclusive with `functionName`.
1185
1238
  *
1186
- * Configure debounce via the `trigger: 'debounced'` + `debounceMs` mechanism.
1239
+ * NOT JSON-serializable — for code-only configs. For configs loaded
1240
+ * from JSON / OpenAPI / databases, use `functionName` to reference
1241
+ * a validator registered in `customFnConfig.asyncValidators`.
1242
+ */
1243
+ fn: AsyncCustomValidator;
1244
+ /** Registered form is forbidden when `fn` is set */
1245
+ functionName?: never;
1246
+ });
1247
+ /**
1248
+ * Shared fields for both branches of {@link FunctionHttpValidatorConfig}.
1249
+ *
1250
+ * @internal
1251
+ */
1252
+ interface FunctionHttpValidatorConfigShared extends BaseValidatorConfig {
1253
+ /** Validator type identifier. */
1254
+ type: 'http';
1255
+ /** Optional parameters to pass to HTTP validator */
1256
+ params?: Record<string, unknown>;
1257
+ }
1258
+ /**
1259
+ * Function-based HTTP validator configuration. Uses Angular's `validateHttp` API.
1260
+ *
1261
+ * Two mutually exclusive authoring forms:
1262
+ * - `functionName`: name of an HTTP validator registered in `customFnConfig.httpValidators`.
1263
+ * JSON-serializable.
1264
+ * - `fn`: inline HTTP validator. NOT JSON-serializable; for code-only configs.
1265
+ *
1266
+ * Exactly one of `functionName` or `fn` must be set.
1267
+ *
1268
+ * Discriminated from `DeclarativeHttpValidatorConfig` by the presence of `functionName`
1269
+ * or `fn` (and absence of `http` + `responseMapping`).
1270
+ */
1271
+ type FunctionHttpValidatorConfig = (FunctionHttpValidatorConfigShared & {
1272
+ /** Name of registered HTTP validator configuration */
1273
+ functionName: string;
1274
+ /** Inline form is forbidden when `functionName` is set */
1275
+ fn?: never;
1276
+ }) | (FunctionHttpValidatorConfigShared & {
1277
+ /**
1278
+ * Inline HTTP validator. Mutually exclusive with `functionName`.
1187
1279
  *
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
- * ```
1280
+ * NOT JSON-serializable — for code-only configs. For configs loaded
1281
+ * from JSON / OpenAPI / databases, use `functionName` to reference
1282
+ * an HTTP validator registered in `customFnConfig.httpValidators`.
1230
1283
  */
1231
- responseExpression: string;
1232
- value?: never;
1233
- expression?: never;
1284
+ fn: HttpCustomValidator;
1285
+ /** Registered form is forbidden when `fn` is set */
1234
1286
  functionName?: never;
1235
- asyncFunctionName?: never;
1236
- }
1287
+ });
1237
1288
  /**
1238
- * Base for async function derivations. `source: 'asyncFunction'` is required.
1239
- * TypeScript enforces `dependsOn` at the type level.
1289
+ * Declarative HTTP validator configuration — fully JSON-serializable, no function registration required.
1240
1290
  *
1241
- * @internal
1291
+ * Uses `HttpRequestConfig` to define the HTTP request and `HttpValidationResponseMapping`
1292
+ * to interpret the response as a validation result. Powered by Angular's `validateHttp` API.
1293
+ *
1294
+ * Discriminated from `FunctionHttpValidatorConfig` by the presence of `http` + `responseMapping`
1295
+ * (and absence of `functionName`).
1242
1296
  */
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;
1297
+ interface DeclarativeHttpValidatorConfig extends BaseValidatorConfig {
1298
+ /** Validator type identifier */
1299
+ type: 'http';
1300
+ /** HTTP request configuration with expression-based query params and body */
1301
+ http: HttpRequestConfig;
1302
+ /** Mapping that interprets the HTTP response as a validation result */
1303
+ responseMapping: HttpValidationResponseMapping;
1282
1304
  }
1283
1305
  /**
1284
- * Base for expression derivations.
1306
+ * Configuration for signal forms validator functions that can be serialized from API.
1307
+ * Discriminated union type for type-safe validator configuration.
1308
+ *
1309
+ * Note: `FunctionHttpValidatorConfig` and `DeclarativeHttpValidatorConfig` both use `type: 'http'`.
1310
+ * They are discriminated by property presence: `functionName` → function-based, `http` → declarative.
1311
+ * Use `isFunctionHttpValidator()` for type-safe narrowing when `type` alone is insufficient.
1312
+ */
1313
+ type ValidatorConfig = BuiltInValidatorConfig | CustomValidatorConfig | AsyncValidatorConfig | FunctionHttpValidatorConfig | DeclarativeHttpValidatorConfig;
1314
+
1315
+ /**
1316
+ * Special form-state conditions for button disabled logic.
1317
+ *
1318
+ * These conditions evaluate form or page-level state rather than field values,
1319
+ * and are primarily used for controlling button disabled states.
1320
+ *
1321
+ * @example
1322
+ * ```typescript
1323
+ * // Disable submit button when form is invalid or submitting
1324
+ * {
1325
+ * key: 'submit',
1326
+ * type: 'submit',
1327
+ * label: 'Submit',
1328
+ * logic: [
1329
+ * { type: 'disabled', condition: 'formInvalid' },
1330
+ * { type: 'disabled', condition: 'formSubmitting' },
1331
+ * ]
1332
+ * }
1333
+ *
1334
+ * // Disable next button when current page is invalid
1335
+ * nextButton({
1336
+ * key: 'next',
1337
+ * label: 'Next',
1338
+ * logic: [
1339
+ * { type: 'disabled', condition: 'pageInvalid' },
1340
+ * ]
1341
+ * })
1342
+ * ```
1343
+ *
1344
+ * @public
1345
+ */
1346
+ type FormStateCondition =
1347
+ /** True when form.valid() === false */
1348
+ 'formInvalid'
1349
+ /** True when form.submitting() === true */
1350
+ | 'formSubmitting'
1351
+ /** True when fields on the current page are invalid (for paged forms) */
1352
+ | 'pageInvalid';
1353
+ /**
1354
+ * Logic type for controlling field state (hidden, readonly, disabled, required).
1355
+ *
1356
+ * @public
1357
+ */
1358
+ type StateLogicType = 'hidden' | 'readonly' | 'disabled' | 'required';
1359
+ /**
1360
+ * Base configuration for conditional field state logic.
1285
1361
  *
1286
1362
  * @internal
1287
1363
  */
1288
- interface ExpressionDerivationBase extends SharedDerivationFields {
1289
- source?: never;
1364
+ interface BaseStateLogicConfig {
1290
1365
  /**
1291
- * JavaScript expression to evaluate for the derived value.
1292
- *
1293
- * Has access to `formValue` object containing all form values.
1294
- * For array fields, `formValue` is scoped to the current array item.
1295
- * Uses the same secure AST-based parser as other expressions.
1366
+ * Logic type identifier for field state.
1296
1367
  *
1297
- * @example
1298
- * ```typescript
1299
- * expression: 'formValue.quantity * formValue.unitPrice'
1300
- * expression: 'formValue.firstName + " " + formValue.lastName'
1301
- * expression: 'formValue.price * (1 - formValue.discount / 100)'
1302
- * ```
1368
+ * - `hidden`: Hide the field from view (still participates in form state)
1369
+ * - `readonly`: Make the field read-only
1370
+ * - `disabled`: Disable user interaction
1371
+ * - `required`: Make the field required
1303
1372
  */
1304
- expression: string;
1373
+ type: StateLogicType;
1305
1374
  /**
1306
- * Explicit field dependencies for expressions.
1375
+ * Condition that determines when this logic applies.
1307
1376
  *
1308
- * For `expression`, dependencies are automatically extracted from the expression.
1309
- * Provide `dependsOn` to override automatic detection for complex expressions.
1377
+ * Can be:
1378
+ * - `boolean`: Static value (always applies or never applies)
1379
+ * - `ConditionalExpression`: Expression evaluated against field/form values
1380
+ * - `FormStateCondition`: Special form/page state check (for buttons)
1310
1381
  *
1311
1382
  * @example
1312
1383
  * ```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
- * }]
1384
+ * // Static condition
1385
+ * condition: true
1386
+ *
1387
+ * // Field value condition
1388
+ * condition: {
1389
+ * type: 'fieldValue',
1390
+ * fieldPath: 'status',
1391
+ * operator: 'equals',
1392
+ * value: 'locked'
1321
1393
  * }
1394
+ *
1395
+ * // Form state condition (for buttons)
1396
+ * condition: 'formSubmitting'
1322
1397
  * ```
1323
1398
  */
1324
- dependsOn?: string[];
1325
- value?: never;
1326
- functionName?: never;
1327
- http?: never;
1328
- asyncFunctionName?: never;
1329
- responseExpression?: never;
1399
+ condition: ConditionalExpression | boolean | FormStateCondition;
1330
1400
  }
1331
1401
  /**
1332
- * Base for value derivations.
1402
+ * State logic that evaluates immediately on change (default).
1333
1403
  *
1334
1404
  * @internal
1335
1405
  */
1336
- interface ValueDerivationBase extends SharedDerivationFields {
1337
- source?: never;
1338
- /**
1339
- * Static value to set on this field.
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
- * ```
1350
- */
1351
- value: unknown;
1406
+ interface ImmediateStateLogicConfig extends BaseStateLogicConfig {
1352
1407
  /**
1353
- * Explicit field dependencies for value derivations.
1354
- *
1355
- * For `value` (static), no dependencies are needed.
1356
- * Provide `dependsOn` to conditionally re-evaluate when specific fields change.
1408
+ * Trigger for immediate evaluation.
1409
+ * @default 'onChange'
1357
1410
  */
1358
- dependsOn?: string[];
1359
- expression?: never;
1360
- functionName?: never;
1361
- http?: never;
1362
- asyncFunctionName?: never;
1363
- responseExpression?: never;
1411
+ trigger?: 'onChange';
1412
+ /** Not allowed for onChange trigger */
1413
+ debounceMs?: never;
1364
1414
  }
1365
1415
  /**
1366
- * Base for custom sync function derivations.
1416
+ * State logic that evaluates after a debounce period.
1367
1417
  *
1368
1418
  * @internal
1369
1419
  */
1370
- interface FunctionDerivationBase extends SharedDerivationFields {
1371
- source?: never;
1420
+ interface DebouncedStateLogicConfig extends BaseStateLogicConfig {
1372
1421
  /**
1373
- * Name of a registered custom derivation function.
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
- * ```
1422
+ * Trigger for debounced evaluation.
1423
+ * Evaluates after the value has stabilized for the debounce duration.
1384
1424
  */
1385
- functionName: string;
1425
+ trigger: 'debounced';
1386
1426
  /**
1387
- * Explicit field dependencies for function derivations.
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
- * ```
1427
+ * Debounce duration in milliseconds.
1428
+ * @default 500
1405
1429
  */
1406
- dependsOn?: string[];
1407
- value?: never;
1408
- expression?: never;
1409
- http?: never;
1410
- asyncFunctionName?: never;
1411
- responseExpression?: never;
1430
+ debounceMs?: number;
1412
1431
  }
1413
1432
  /**
1414
- * HTTP derivation that evaluates immediately on change (default).
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
1461
- */
1462
- type DebouncedFunctionDerivationLogicConfig = FunctionDerivationBase & DebouncedDerivationTrigger;
1463
- /**
1464
- * Configuration for value derivation logic.
1433
+ * Configuration for conditional field state logic.
1465
1434
  *
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.
1435
+ * Defines how field behavior changes based on conditions.
1436
+ * Supports hiding, disabling, making readonly, or requiring fields
1437
+ * based on form state or field values.
1469
1438
  *
1470
1439
  * @example
1471
1440
  * ```typescript
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
- * }
1486
- *
1487
- * // Compute total from quantity and price
1488
- * {
1489
- * key: 'total',
1490
- * derivation: 'formValue.quantity * formValue.unitPrice'
1491
- * }
1492
- *
1493
- * // Use custom function for complex logic
1441
+ * // Hide email field when contact method is not email
1494
1442
  * {
1495
- * key: 'currency',
1496
- * logic: [{
1497
- * type: 'derivation',
1498
- * functionName: 'getCurrencyForCountry'
1499
- * }]
1443
+ * type: 'hidden',
1444
+ * condition: {
1445
+ * type: 'fieldValue',
1446
+ * fieldPath: 'contactMethod',
1447
+ * operator: 'notEquals',
1448
+ * value: 'email'
1449
+ * }
1500
1450
  * }
1501
1451
  *
1502
- * // Self-transform with debounced trigger
1503
- * // (applies after user stops typing)
1452
+ * // Disable button when form is submitting
1504
1453
  * {
1505
- * key: 'email',
1506
- * logic: [{
1507
- * type: 'derivation',
1508
- * expression: 'formValue.email.toLowerCase()',
1509
- * trigger: 'debounced',
1510
- * debounceMs: 500
1511
- * }]
1454
+ * type: 'disabled',
1455
+ * condition: 'formSubmitting'
1512
1456
  * }
1513
1457
  *
1514
- * // Array item derivation (formValue is scoped to current item)
1458
+ * // Debounced visibility (avoids flicker during rapid typing)
1515
1459
  * {
1516
- * key: 'lineTotal', // Inside array field
1517
- * derivation: 'formValue.quantity * formValue.unitPrice'
1460
+ * type: 'hidden',
1461
+ * trigger: 'debounced',
1462
+ * debounceMs: 300,
1463
+ * condition: {
1464
+ * type: 'fieldValue',
1465
+ * fieldPath: 'search',
1466
+ * operator: 'isEmpty'
1467
+ * }
1518
1468
  * }
1519
1469
  * ```
1520
1470
  *
1521
1471
  * @public
1522
1472
  */
1523
- type DerivationLogicConfig = OnChangeHttpDerivationLogicConfig | DebouncedHttpDerivationLogicConfig | OnChangeAsyncFunctionDerivationLogicConfig | DebouncedAsyncFunctionDerivationLogicConfig | OnChangeExpressionDerivationLogicConfig | DebouncedExpressionDerivationLogicConfig | OnChangeValueDerivationLogicConfig | DebouncedValueDerivationLogicConfig | OnChangeFunctionDerivationLogicConfig | DebouncedFunctionDerivationLogicConfig;
1524
- /**
1525
- * Union type for all logic configurations.
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.
1535
- *
1536
- * - 'none': No debug logging
1537
- * - 'summary': Log cycle completion with counts (default in dev mode)
1538
- * - 'verbose': Log individual derivation evaluations with details
1539
- *
1540
- * @public
1541
- */
1542
- type DerivationLogLevel = 'none' | 'summary' | 'verbose';
1473
+ type StateLogicConfig = ImmediateStateLogicConfig | DebouncedStateLogicConfig;
1543
1474
  /**
1544
- * Type guard to check if a condition is a FormStateCondition.
1545
- *
1546
- * @param condition - The condition to check
1547
- * @returns true if the condition is a FormStateCondition
1475
+ * Shared fields that appear on all derivation logic config variants.
1548
1476
  *
1549
- * @public
1550
- */
1551
- declare function isFormStateCondition(condition: StateLogicConfig['condition'] | DerivationLogicConfig['condition']): condition is FormStateCondition;
1552
-
1553
- /**
1554
- * Configuration for applying predefined schemas
1555
- */
1556
- interface SchemaApplicationConfig {
1557
- /** Schema application type */
1558
- type: 'apply' | 'applyWhen' | 'applyWhenValue' | 'applyEach';
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;
1565
- }
1566
- /**
1567
- * Reusable schema definition that can be referenced by name
1477
+ * @internal
1568
1478
  */
1569
- interface SchemaDefinition {
1570
- /** Unique schema identifier */
1571
- name: string;
1572
- /** Schema description */
1573
- description?: string;
1574
- /** Field path pattern this schema applies to */
1575
- pathPattern?: string;
1576
- /** Validators to apply */
1577
- validators?: ValidatorConfig[];
1578
- /** Logic rules to apply */
1579
- logic?: LogicConfig[];
1580
- /** Nested schema applications */
1581
- subSchemas?: SchemaApplicationConfig[];
1582
- }
1583
-
1584
- /**
1585
- * Custom validator function signature using Angular's public FieldContext API
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
1602
- *
1603
- * **Best Practice - Return Only Error Kind:**
1604
- * Validators should focus on validation logic, not presentation.
1605
- * Return just the error `kind` and configure messages at field level for i18n support.
1606
- *
1607
- * @example Single Field Validation
1608
- * ```typescript
1609
- * const noSpaces: CustomValidator<string> = (ctx) => {
1610
- * const value = ctx.value();
1611
- * if (typeof value === 'string' && value.includes(' ')) {
1612
- * return { kind: 'noSpaces' };
1613
- * }
1614
- * return null;
1615
- * };
1616
- *
1617
- * // Field configuration
1618
- * {
1619
- * key: 'username',
1620
- * validators: [{ type: 'custom', functionName: 'noSpaces' }],
1621
- * validationMessages: {
1622
- * noSpaces: 'Spaces are not allowed'
1623
- * }
1624
- * }
1625
- * ```
1626
- *
1627
- * @example Cross-Field Validation with valueOf()
1628
- * ```typescript
1629
- * // Compare two fields using public API
1630
- * const lessThan: CustomValidator<number> = (ctx, params) => {
1631
- * const value = ctx.value();
1632
- * const compareToPath = params?.field as string;
1633
- *
1634
- * // Use valueOf() to access other field - public API!
1635
- * const otherValue = ctx.valueOf(compareToPath as any);
1636
- *
1637
- * if (otherValue !== undefined && value >= otherValue) {
1638
- * return { kind: 'notLessThan' };
1639
- * }
1640
- * return null;
1641
- * };
1642
- * ```
1643
- *
1644
- * @example Multiple Errors (Cross-Field Validation)
1645
- * ```typescript
1646
- * const validateDateRange: CustomValidator = (ctx) => {
1647
- * const errors: ValidationError[] = [];
1648
- *
1649
- * const startDate = ctx.valueOf('startDate' as any);
1650
- * const endDate = ctx.valueOf('endDate' as any);
1651
- *
1652
- * if (!startDate) errors.push({ kind: 'startDateRequired' });
1653
- * if (!endDate) errors.push({ kind: 'endDateRequired' });
1654
- * if (startDate && endDate && startDate > endDate) {
1655
- * errors.push({ kind: 'invalidDateRange' });
1656
- * }
1657
- *
1658
- * return errors.length > 0 ? errors : null;
1659
- * };
1660
- * ```
1661
- *
1662
- * **Message Resolution Priority:**
1663
- * 1. Field-level `validationMessages[kind]` (highest priority - per-field customization)
1664
- * 2. Form-level `defaultValidationMessages[kind]` (fallback for common messages)
1665
- * 3. **No message configured = Warning logged + error NOT displayed**
1666
- *
1667
- * **Important:** Validator-returned messages are NOT used. All messages MUST be explicitly configured.
1668
- *
1669
- * @template TValue The type of value stored in the field being validated
1670
- */
1671
- type CustomValidator<TValue = unknown> = (ctx: FieldContext<TValue>, params?: Record<string, unknown>) => ValidationError$1 | ValidationError$1[] | null;
1672
- /**
1673
- * Async custom validator configuration using Angular's resource-based API
1674
- *
1675
- * Angular's validateAsync uses the resource API for async validation,
1676
- * which provides better integration with Signal Forms lifecycle management.
1677
- *
1678
- * **Structure:**
1679
- * - `params`: Function that computes params from field context
1680
- * - `factory`: Function that creates a ResourceRef from params signal
1681
- * - `onSuccess`: Maps successful resource result to validation errors
1682
- * - `onError`: Optional handler for resource errors (network failures, etc.)
1683
- *
1684
- * **Use Cases:**
1685
- * - Database lookups via services with resource API
1686
- * - Complex async business logic with Angular resources
1687
- * - Any async operation using Angular's resource pattern
1688
- *
1689
- * **Note:** For HTTP validation, prefer `HttpCustomValidator` which provides
1690
- * a simpler API specifically designed for HTTP requests.
1691
- *
1692
- * @example Database Lookup with Resource
1693
- * ```typescript
1694
- * const checkUsernameAvailable: AsyncCustomValidator<string> = {
1695
- * params: (ctx) => ({ username: ctx.value() }),
1696
- * factory: (params) => {
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
- * ```
1717
- *
1718
- * @template TValue The type of value stored in the field being validated
1719
- * @template TParams The type of params passed to the resource
1720
- * @template TResult The type of result returned by the resource
1721
- */
1722
- interface AsyncCustomValidator<TValue = unknown, TParams = unknown, TResult = unknown> {
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;
1479
+ interface SharedDerivationFields {
1728
1480
  /**
1729
- * Function that creates a ResourceRef from the params signal.
1730
- * The resource will be automatically managed by Angular's lifecycle.
1481
+ * Logic type identifier for value derivation.
1731
1482
  */
1732
- readonly factory: (params: Signal<TParams | undefined>) => ResourceRef<TResult | undefined>;
1483
+ type: 'derivation';
1733
1484
  /**
1734
- * Optional function to map successful resource result to validation errors.
1735
- * Return null if validation passes, or ValidationError/ValidationError[] if validation fails.
1485
+ * Target property name for property derivation.
1486
+ *
1487
+ * When set, this derivation targets a field property (like `minDate`, `options`,
1488
+ * `label`, `placeholder`) instead of the field's value. The derivation is routed
1489
+ * to the property derivation pipeline.
1490
+ *
1491
+ * When absent, the derivation targets the field's value (default behavior).
1492
+ *
1493
+ * **Depth limit (max 2 levels):** Only simple and single-dot-nested paths are
1494
+ * supported. Paths with 2+ dots (e.g., `'a.b.c'`) will throw at runtime.
1495
+ *
1496
+ * @example
1497
+ * ```typescript
1498
+ * // Property derivation (new unified API)
1499
+ * {
1500
+ * key: 'endDate',
1501
+ * type: 'datepicker',
1502
+ * logic: [{
1503
+ * type: 'derivation',
1504
+ * targetProperty: 'minDate',
1505
+ * expression: 'formValue.startDate'
1506
+ * }]
1507
+ * }
1508
+ *
1509
+ * // Value derivation (no targetProperty — existing behavior)
1510
+ * {
1511
+ * key: 'total',
1512
+ * type: 'input',
1513
+ * logic: [{
1514
+ * type: 'derivation',
1515
+ * expression: 'formValue.quantity * formValue.unitPrice'
1516
+ * }]
1517
+ * }
1518
+ * ```
1736
1519
  */
1737
- readonly onSuccess?: (result: TResult, ctx: FieldContext<TValue>) => TreeValidationResult;
1520
+ targetProperty?: string;
1738
1521
  /**
1739
- * Optional error handler for resource errors (network failures, etc.).
1740
- * Return null to ignore the error, or ValidationError to display it to the user.
1522
+ * Optional name for this derivation for debugging purposes.
1523
+ *
1524
+ * When provided, this name appears in derivation debug logs,
1525
+ * making it easier to identify specific derivations in complex forms.
1526
+ *
1527
+ * @example
1528
+ * ```typescript
1529
+ * {
1530
+ * key: 'lineTotal',
1531
+ * type: 'input',
1532
+ * logic: [{
1533
+ * type: 'derivation',
1534
+ * debugName: 'Calculate line total',
1535
+ * expression: 'formValue.quantity * formValue.unitPrice'
1536
+ * }]
1537
+ * }
1538
+ * ```
1741
1539
  */
1742
- readonly onError?: (error: unknown, ctx: FieldContext<TValue>) => ValidationError$1 | ValidationError$1[] | null;
1743
- }
1744
- /**
1745
- * HTTP request configuration for validateHttp API
1746
- */
1747
- interface HttpResourceRequest {
1748
- url: string;
1749
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
1750
- body?: unknown;
1751
- headers?: Record<string, string | string[]>;
1752
- }
1753
- /**
1754
- * HTTP-based validator configuration for Angular's validateHttp API
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)
1768
- *
1769
- * **Important:** `onSuccess` uses inverted logic - it maps SUCCESSFUL HTTP responses
1770
- * to validation errors. For example, if the API returns `{ available: false }`,
1771
- * your `onSuccess` should return `{ kind: 'usernameTaken' }`.
1772
- *
1773
- * @example Username Availability Check (GET)
1774
- * ```typescript
1775
- * const checkUsername: HttpCustomValidator<string> = {
1776
- * request: (ctx) => {
1777
- * const username = ctx.value();
1778
- * if (!username) return undefined; // Skip validation if empty
1779
- * return `/api/users/check-username?username=${encodeURIComponent(username)}`;
1780
- * },
1781
- * onSuccess: (response, ctx) => {
1782
- * // Inverted logic: successful response may indicate validation failure
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
1788
- * }
1789
- * };
1790
- * ```
1791
- *
1792
- * @example Address Validation (POST with Body)
1793
- * ```typescript
1794
- * const validateAddress: HttpCustomValidator = {
1795
- * request: (ctx) => {
1796
- * const zipCode = ctx.value();
1797
- * if (!zipCode) return undefined;
1798
- *
1799
- * return {
1800
- * url: '/api/validate-address',
1801
- * method: 'POST',
1802
- * body: {
1803
- * street: ctx.valueOf('street' as any),
1804
- * city: ctx.valueOf('city' as any),
1805
- * zipCode: zipCode
1806
- * },
1807
- * headers: { 'Content-Type': 'application/json' }
1808
- * };
1809
- * },
1810
- * onSuccess: (response) => {
1811
- * return response.valid ? null : { kind: 'invalidAddress' };
1812
- * }
1813
- * };
1814
- * ```
1815
- *
1816
- * @template TValue The type of value stored in the field being validated
1817
- * @template TResult The type of HTTP response
1818
- */
1819
- interface HttpCustomValidator<TValue = unknown, TResult = unknown> {
1540
+ debugName?: string;
1820
1541
  /**
1821
- * Function that returns the HTTP request configuration.
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)
1542
+ * Condition that determines when this derivation applies.
1543
+ *
1544
+ * Can be:
1545
+ * - `boolean`: Static value (always applies or never applies)
1546
+ * - `ConditionalExpression`: Expression evaluated against field/form values
1547
+ *
1548
+ * Defaults to `true` (always apply).
1549
+ *
1550
+ * Note: FormStateCondition is not supported for derivations.
1551
+ *
1552
+ * @example
1553
+ * ```typescript
1554
+ * // Always compute (default)
1555
+ * condition: true
1556
+ *
1557
+ * // Conditional derivation
1558
+ * condition: {
1559
+ * type: 'fieldValue',
1560
+ * fieldPath: 'country',
1561
+ * operator: 'equals',
1562
+ * value: 'USA'
1563
+ * }
1564
+ * ```
1826
1565
  */
1827
- readonly request: (ctx: FieldContext<TValue>, config?: Record<string, unknown>) => string | HttpResourceRequest | undefined;
1566
+ condition?: ConditionalExpression | boolean;
1828
1567
  /**
1829
- * REQUIRED function to map successful HTTP response to validation errors.
1568
+ * When true, the derivation stops running after the user manually
1569
+ * edits the target field.
1830
1570
  *
1831
- * **Inverted Logic:** This is called on successful HTTP responses.
1832
- * Return null if validation passes, or ValidationError/ValidationError[] if validation fails.
1571
+ * This is useful for "smart defaults" — values that should be
1572
+ * auto-filled initially but respected once the user explicitly changes them.
1833
1573
  *
1834
- * Example: API returns `{ available: false }` → return `{ kind: 'usernameTaken' }`
1574
+ * Uses the field's `dirty()` signal to detect user modification.
1575
+ * Derivations write directly to `value.set()` which does not trigger
1576
+ * `markAsDirty()`, so `dirty === true` reliably indicates a user edit.
1577
+ *
1578
+ * @example
1579
+ * ```typescript
1580
+ * // Auto-fill display name from first + last name, but stop if user edits it
1581
+ * {
1582
+ * key: 'displayName',
1583
+ * logic: [{
1584
+ * type: 'derivation',
1585
+ * expression: 'formValue.firstName + " " + formValue.lastName',
1586
+ * stopOnUserOverride: true
1587
+ * }]
1588
+ * }
1589
+ * ```
1835
1590
  */
1836
- readonly onSuccess: (result: TResult, ctx: FieldContext<TValue>) => TreeValidationResult;
1591
+ stopOnUserOverride?: boolean;
1837
1592
  /**
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.
1593
+ * When true (and `stopOnUserOverride` is also true), clears the
1594
+ * user-override flag when any dependency of this derivation changes,
1595
+ * allowing the derivation to run again.
1840
1596
  *
1841
- * Common pattern: Log the error and return null to avoid blocking form submission
1842
- * on network issues.
1597
+ * This is useful when a user override should only persist until the
1598
+ * underlying data changes — e.g., when switching countries, the
1599
+ * phone prefix should re-derive even if the user previously edited it.
1600
+ *
1601
+ * @example
1602
+ * ```typescript
1603
+ * {
1604
+ * key: 'phonePrefix',
1605
+ * logic: [{
1606
+ * type: 'derivation',
1607
+ * value: '+1',
1608
+ * condition: { type: 'fieldValue', fieldPath: 'country', operator: 'equals', value: 'USA' },
1609
+ * stopOnUserOverride: true,
1610
+ * reEngageOnDependencyChange: true,
1611
+ * dependsOn: ['country']
1612
+ * }]
1613
+ * }
1614
+ * ```
1843
1615
  */
1844
- readonly onError?: (error: unknown, ctx: FieldContext<TValue>) => ValidationError$1 | ValidationError$1[] | null;
1616
+ reEngageOnDependencyChange?: boolean;
1845
1617
  }
1846
-
1847
1618
  /**
1848
- * Interface for dynamic forms logger implementations.
1849
- * Allows custom logging integrations (Sentry, DataDog, etc.)
1619
+ * Trigger variants for derivation timing.
1620
+ * @internal
1850
1621
  */
1851
- interface Logger {
1852
- /**
1853
- * Log a debug message.
1854
- * Use for detailed diagnostic information during development.
1855
- */
1856
- debug(message: string, ...args: unknown[]): void;
1622
+ type ImmediateDerivationTrigger = {
1623
+ trigger?: 'onChange';
1624
+ debounceMs?: never;
1625
+ };
1626
+ type DebouncedDerivationTrigger = {
1627
+ trigger: 'debounced';
1628
+ debounceMs?: number;
1629
+ };
1630
+ /**
1631
+ * Base for HTTP derivations. `source: 'http'` is required.
1632
+ * TypeScript enforces `dependsOn` and `responseExpression` at the type level.
1633
+ *
1634
+ * @internal
1635
+ */
1636
+ interface HttpDerivationBase extends SharedDerivationFields {
1637
+ /** Identifies this derivation as HTTP-driven. */
1638
+ source: 'http';
1639
+ /** Forbidden on HTTP source */
1640
+ fn?: never;
1641
+ /** Forbidden on HTTP source */
1642
+ asyncFn?: never;
1857
1643
  /**
1858
- * Log an info message.
1859
- * Use for general operational information.
1644
+ * HTTP request configuration for server-driven derivations.
1645
+ *
1646
+ * The request is sent when dependencies change, with automatic
1647
+ * debouncing and cancellation of in-flight requests.
1648
+ *
1649
+ * Configure debounce via the `trigger: 'debounced'` + `debounceMs` mechanism.
1650
+ *
1651
+ * @example
1652
+ * ```typescript
1653
+ * {
1654
+ * key: 'exchangeRate',
1655
+ * logic: [{
1656
+ * type: 'derivation',
1657
+ * source: 'http',
1658
+ * http: {
1659
+ * url: '/api/exchange-rate',
1660
+ * method: 'GET',
1661
+ * queryParams: {
1662
+ * from: 'formValue.sourceCurrency',
1663
+ * to: 'formValue.targetCurrency',
1664
+ * },
1665
+ * },
1666
+ * responseExpression: 'response.rate',
1667
+ * dependsOn: ['sourceCurrency', 'targetCurrency'],
1668
+ * }]
1669
+ * }
1670
+ * ```
1860
1671
  */
1861
- info(message: string, ...args: unknown[]): void;
1672
+ http: HttpRequestConfig;
1862
1673
  /**
1863
- * Log a warning message.
1864
- * Use for potentially problematic situations that don't prevent operation.
1674
+ * Explicit field dependencies. Required for HTTP derivations to prevent
1675
+ * wildcard triggering on every keystroke.
1676
+ *
1677
+ * The wildcard token `'*'` is rejected. The structural token `'$group'`
1678
+ * (and `'$group.X'`) is allowed and resolves to the field's parent container
1679
+ * path — be aware this fires whenever any sibling under that group changes,
1680
+ * so the caller owns the request frequency (consider `trigger: 'debounced'`).
1865
1681
  */
1866
- warn(message: string, ...args: unknown[]): void;
1682
+ dependsOn: string[];
1867
1683
  /**
1868
- * Log an error message.
1869
- * Use for error conditions that may affect functionality.
1684
+ * Expression to extract the derived value from the HTTP response.
1685
+ *
1686
+ * Evaluated via `ExpressionParser` with `{ response }` as the evaluation scope.
1687
+ *
1688
+ * @example
1689
+ * ```typescript
1690
+ * responseExpression: 'response.rate'
1691
+ * responseExpression: 'response.data.suggestedPrice'
1692
+ * ```
1870
1693
  */
1871
- error(message: string, ...args: unknown[]): void;
1694
+ responseExpression: string;
1695
+ value?: never;
1696
+ expression?: never;
1697
+ functionName?: never;
1698
+ asyncFunctionName?: never;
1872
1699
  }
1873
-
1874
1700
  /**
1875
- * Shared shape for DI-scoped warning trackers that dedupe log output.
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.
1701
+ * Shared fields for both branches of {@link AsyncFunctionDerivationBase}.
1880
1702
  *
1881
1703
  * @internal
1882
1704
  */
1883
- interface WarningTracker {
1884
- warnedKeys: Set<string>;
1705
+ interface AsyncFunctionDerivationShared extends SharedDerivationFields {
1706
+ /** Identifies this derivation as async-function-driven. */
1707
+ source: 'asyncFunction';
1708
+ /** Forbidden on asyncFunction source */
1709
+ fn?: never;
1710
+ /**
1711
+ * Explicit field dependencies. Required for async derivations to prevent
1712
+ * wildcard triggering on every form change.
1713
+ *
1714
+ * The wildcard token `'*'` is rejected. The structural token `'$group'`
1715
+ * (and `'$group.X'`) is allowed and resolves to the field's parent container
1716
+ * path — be aware this fires whenever any sibling under that group changes,
1717
+ * so the caller owns the invocation frequency (handle this via
1718
+ * `trigger: 'debounced'` or via debouncing inside the async function).
1719
+ */
1720
+ dependsOn: string[];
1721
+ value?: never;
1722
+ expression?: never;
1723
+ functionName?: never;
1724
+ http?: never;
1725
+ responseExpression?: never;
1885
1726
  }
1886
-
1887
1727
  /**
1888
- * Represents the state information of a single form field.
1889
- *
1890
- * Used in evaluation contexts to allow expressions and conditions
1891
- * to reference field state (e.g., `fieldState.touched`, `formFieldState.email.dirty`).
1728
+ * Base for async function derivations. `source: 'asyncFunction'` is required.
1729
+ * TypeScript enforces `dependsOn` at the type level, and `asyncFunctionName` /
1730
+ * `asyncFn` are mutually exclusive (XOR).
1892
1731
  *
1893
- * @public
1732
+ * @internal
1894
1733
  */
1895
- interface FieldStateInfo {
1896
- readonly touched: boolean;
1897
- readonly dirty: boolean;
1898
- /** Convenience property: equivalent to `!dirty` */
1899
- readonly pristine: boolean;
1900
- readonly valid: boolean;
1901
- readonly invalid: boolean;
1902
- readonly pending: boolean;
1734
+ type AsyncFunctionDerivationBase = (AsyncFunctionDerivationShared & {
1903
1735
  /**
1904
- * Whether the field is currently hidden.
1736
+ * Name of a registered async derivation function.
1905
1737
  *
1906
- * Note: This is a library-managed property, not a native Angular Signal Forms
1907
- * property. Returns `false` if the field instance does not expose a `hidden` signal.
1908
- */
1909
- readonly hidden: boolean;
1910
- /**
1911
- * Whether the field is currently readonly.
1738
+ * The function receives the evaluation context and returns a Promise or Observable
1739
+ * of the derived value. Register functions in `customFnConfig.asyncDerivations`.
1912
1740
  *
1913
- * Note: This is a library-managed property, not a native Angular Signal Forms
1914
- * property. Returns `false` if the field instance does not expose a `readonly` signal.
1741
+ * @example
1742
+ * ```typescript
1743
+ * {
1744
+ * key: 'suggestedPrice',
1745
+ * logic: [{
1746
+ * type: 'derivation',
1747
+ * source: 'asyncFunction',
1748
+ * asyncFunctionName: 'fetchSuggestedPrice',
1749
+ * dependsOn: ['productId', 'quantity'],
1750
+ * }]
1751
+ * }
1752
+ * ```
1915
1753
  */
1916
- readonly readonly: boolean;
1754
+ asyncFunctionName: string;
1755
+ /** Inline form is forbidden when `asyncFunctionName` is set */
1756
+ asyncFn?: never;
1757
+ }) | (AsyncFunctionDerivationShared & {
1917
1758
  /**
1918
- * Whether the field is currently disabled.
1759
+ * Inline async derivation function. Mutually exclusive with `asyncFunctionName`.
1919
1760
  *
1920
- * Note: This is a library-managed property, not a native Angular Signal Forms
1921
- * property. Returns `false` if the field instance does not expose a `disabled` signal.
1761
+ * NOT JSON-serializable — for code-only configs. For configs loaded
1762
+ * from JSON / OpenAPI / databases, use `asyncFunctionName` to reference
1763
+ * a function registered in `customFnConfig.asyncDerivations`.
1922
1764
  */
1923
- readonly disabled: boolean;
1924
- }
1925
- /**
1926
- * Map of field keys to their state information.
1927
- *
1928
- * Used as `formFieldState` in evaluation contexts to access
1929
- * state of any field in the form by key.
1930
- *
1931
- * @public
1932
- */
1933
- type FormFieldStateMap = Record<string, FieldStateInfo | undefined>;
1765
+ asyncFn: AsyncDerivationFunction;
1766
+ /** Registered form is forbidden when `asyncFn` is set */
1767
+ asyncFunctionName?: never;
1768
+ });
1934
1769
  /**
1935
- * Field state context for the current field being evaluated.
1936
- *
1937
- * Used as `fieldState` in evaluation contexts.
1770
+ * Base for expression derivations.
1938
1771
  *
1939
- * @public
1772
+ * @internal
1940
1773
  */
1941
- type FieldStateContext = FieldStateInfo;
1942
-
1943
- interface EvaluationContext<TValue = unknown, TFormValue extends Record<string, unknown> = any> {
1944
- /** Current field value */
1945
- fieldValue: TValue;
1946
- /**
1947
- * Form value for the current evaluation scope.
1948
- *
1949
- * For regular (non-array) derivations, this contains the complete form value.
1950
- * For array item derivations, this is scoped to the current array item.
1951
- * Use `rootFormValue` to access the complete form when inside an array context.
1952
- */
1953
- formValue: TFormValue;
1954
- /** Field path for relative references */
1955
- fieldPath: string;
1956
- /** Custom evaluation functions */
1957
- customFunctions?: Record<string, (context: EvaluationContext) => unknown>;
1958
- /** Logger for diagnostic output */
1959
- logger: Logger;
1774
+ interface ExpressionDerivationBase extends SharedDerivationFields {
1775
+ source?: never;
1776
+ /** Forbidden on expression mode */
1777
+ fn?: never;
1778
+ /** Forbidden on expression mode */
1779
+ asyncFn?: never;
1960
1780
  /**
1961
- * Value of the field's nearest parent **group** (or array item, when the
1962
- * field has no inner group above the array boundary).
1963
- *
1964
- * Complements `formValue` and `fieldValue` for derivations on fields nested
1965
- * inside groups built by factory helpers, where the parent group's key
1966
- * isn't known at config-authoring time. Mirrors the runtime semantics of
1967
- * the `'$group'` token used in `dependsOn`.
1781
+ * JavaScript expression to evaluate for the derived value.
1968
1782
  *
1969
- * Resolution at evaluation time:
1970
- * - Inside a group at form root: the parent group's value object (e.g.,
1971
- * for `address.state`, `groupValue === formValue.address`).
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`.
1783
+ * Has access to `formValue` object containing all form values.
1784
+ * For array fields, `formValue` is scoped to the current array item.
1785
+ * Uses the same secure AST-based parser as other expressions.
1978
1786
  *
1979
1787
  * @example
1980
1788
  * ```typescript
1981
- * // Field 'state' inside group 'address':
1982
- * deriveStateFromCountry: (ctx) => {
1983
- * const country = ctx.groupValue?.country; // no need to know parent key
1984
- * return country === 'usa' ? 'NY' : '';
1985
- * }
1789
+ * expression: 'formValue.quantity * formValue.unitPrice'
1790
+ * expression: 'formValue.firstName + " " + formValue.lastName'
1791
+ * expression: 'formValue.price * (1 - formValue.discount / 100)'
1986
1792
  * ```
1987
1793
  */
1988
- groupValue?: unknown;
1794
+ expression: string;
1989
1795
  /**
1990
- * Root form value when inside an array context.
1796
+ * Explicit field dependencies for expressions.
1991
1797
  *
1992
- * This provides access to values outside the current array item.
1993
- * When a derivation targets an array item field (e.g., `items.$.lineTotal`),
1994
- * `formValue` is scoped to the current array item, while `rootFormValue`
1995
- * provides access to the entire form value including fields outside the array.
1798
+ * For `expression`, dependencies are automatically extracted from the expression.
1799
+ * Provide `dependsOn` to override automatic detection for complex expressions.
1996
1800
  *
1997
1801
  * @example
1998
1802
  * ```typescript
1999
- * // In an array item derivation (on the lineTotal field inside lineItems array):
1803
+ * // Override automatic detection for complex expressions
2000
1804
  * {
2001
- * key: 'lineTotal',
2002
- * type: 'input',
2003
- * // formValue = current array item { quantity: 2, unitPrice: 50 }
2004
- * // rootFormValue = entire form { globalDiscount: 0.1, lineItems: [...] }
2005
- * derivation: 'formValue.quantity * formValue.unitPrice * (1 - rootFormValue.globalDiscount)'
1805
+ * key: 'total',
1806
+ * logic: [{
1807
+ * type: 'derivation',
1808
+ * expression: 'calculateTotal(formValue)',
1809
+ * dependsOn: ['quantity', 'unitPrice', 'discount']
1810
+ * }]
2006
1811
  * }
2007
1812
  * ```
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
1813
  */
2020
- arrayPath?: string;
1814
+ dependsOn?: string[];
1815
+ value?: never;
1816
+ functionName?: never;
1817
+ http?: never;
1818
+ asyncFunctionName?: never;
1819
+ responseExpression?: never;
1820
+ }
1821
+ /**
1822
+ * Base for value derivations.
1823
+ *
1824
+ * @internal
1825
+ */
1826
+ interface ValueDerivationBase extends SharedDerivationFields {
1827
+ source?: never;
1828
+ /** Forbidden on value mode */
1829
+ fn?: never;
1830
+ /** Forbidden on value mode */
1831
+ asyncFn?: never;
2021
1832
  /**
2022
- * External data signals resolved to their current values.
1833
+ * Static value to set on this field.
2023
1834
  *
2024
- * This allows forms to reference data from outside the form context
2025
- * in conditional logic, derivations, and other expressions.
1835
+ * Use when the derived value is a constant.
2026
1836
  *
2027
1837
  * @example
2028
1838
  * ```typescript
2029
- * // In form config:
2030
- * externalData: {
2031
- * userRole: computed(() => this.userService.currentRole()),
2032
- * permissions: computed(() => this.authService.permissions()),
2033
- * }
2034
- *
2035
- * // In JavaScript expression:
2036
- * condition: {
2037
- * type: 'javascript',
2038
- * expression: "externalData.userRole === 'admin'"
2039
- * }
1839
+ * value: '+1' // String
1840
+ * value: 100 // Number
1841
+ * value: true // Boolean
1842
+ * value: { code: 'US' } // Object
2040
1843
  * ```
2041
1844
  */
2042
- externalData?: Record<string, unknown>;
1845
+ value: unknown;
2043
1846
  /**
2044
- * DI-scoped tracker for deprecation warnings.
2045
- * Used by the condition evaluator to deduplicate deprecation warnings.
1847
+ * Explicit field dependencies for value derivations.
1848
+ *
1849
+ * For `value` (static), no dependencies are needed.
1850
+ * Provide `dependsOn` to conditionally re-evaluate when specific fields change.
2046
1851
  */
2047
- deprecationTracker?: WarningTracker;
1852
+ dependsOn?: string[];
1853
+ expression?: never;
1854
+ functionName?: never;
1855
+ http?: never;
1856
+ asyncFunctionName?: never;
1857
+ responseExpression?: never;
1858
+ }
1859
+ /**
1860
+ * Shared fields for both branches of {@link FunctionDerivationBase}.
1861
+ *
1862
+ * @internal
1863
+ */
1864
+ interface FunctionDerivationShared extends SharedDerivationFields {
1865
+ source?: never;
1866
+ /** Forbidden on function-derivation mode */
1867
+ asyncFn?: never;
2048
1868
  /**
2049
- * State of the current field being evaluated.
1869
+ * Explicit field dependencies for function derivations.
2050
1870
  *
2051
- * Provides access to field state properties like `touched`, `dirty`, `valid`, etc.
2052
- * Uses a Proxy for lazy evaluation — properties are only read when accessed.
1871
+ * If not provided, defaults to all fields ('*').
1872
+ * Specify `dependsOn` for better performance when the function only
1873
+ * depends on a subset of fields.
2053
1874
  *
2054
1875
  * @example
2055
1876
  * ```typescript
2056
- * // In a JavaScript expression:
2057
- * condition: {
2058
- * type: 'javascript',
2059
- * expression: "fieldState.touched && fieldState.dirty"
1877
+ * // Only re-evaluate when country changes
1878
+ * {
1879
+ * key: 'currency',
1880
+ * logic: [{
1881
+ * type: 'derivation',
1882
+ * functionName: 'getCurrencyForCountry',
1883
+ * dependsOn: ['country']
1884
+ * }]
2060
1885
  * }
2061
1886
  * ```
2062
1887
  */
2063
- fieldState?: FieldStateContext;
1888
+ dependsOn?: string[];
1889
+ value?: never;
1890
+ expression?: never;
1891
+ http?: never;
1892
+ asyncFunctionName?: never;
1893
+ responseExpression?: never;
1894
+ }
1895
+ /**
1896
+ * Base for custom sync function derivations. `functionName` and `fn` are
1897
+ * mutually exclusive (XOR).
1898
+ *
1899
+ * @internal
1900
+ */
1901
+ type FunctionDerivationBase = (FunctionDerivationShared & {
2064
1902
  /**
2065
- * State of all fields in the form, keyed by field key.
1903
+ * Name of a registered custom derivation function.
2066
1904
  *
2067
- * Provides access to any field's state properties by key.
2068
- * Uses a Proxy for lazy evaluation — field state is only read when accessed.
1905
+ * The function receives the evaluation context and returns the derived value.
1906
+ * Register functions in `customFnConfig.derivations`.
2069
1907
  *
2070
1908
  * @example
2071
1909
  * ```typescript
2072
- * // In a JavaScript expression:
2073
- * condition: {
2074
- * type: 'javascript',
2075
- * expression: "formFieldState.email.dirty && formFieldState.email.valid"
2076
- * }
1910
+ * functionName: 'getCurrencyForCountry'
1911
+ * functionName: 'calculateTax'
1912
+ * functionName: 'formatPhoneNumber'
2077
1913
  * ```
2078
1914
  */
2079
- formFieldState?: FormFieldStateMap;
2080
- /** Allow additional properties for flexible expression evaluation */
2081
- [key: string]: unknown;
2082
- }
2083
-
1915
+ functionName: string;
1916
+ /** Inline form is forbidden when `functionName` is set */
1917
+ fn?: never;
1918
+ }) | (FunctionDerivationShared & {
1919
+ /**
1920
+ * Inline custom derivation function. Mutually exclusive with `functionName`.
1921
+ *
1922
+ * NOT JSON-serializable — for code-only configs. For configs loaded
1923
+ * from JSON / OpenAPI / databases, use `functionName` to reference
1924
+ * a function registered in `customFnConfig.derivations`.
1925
+ */
1926
+ fn: CustomFunction;
1927
+ /** Registered form is forbidden when `fn` is set */
1928
+ functionName?: never;
1929
+ });
2084
1930
  /**
2085
- * Custom function signature for conditional expressions
1931
+ * HTTP derivation that evaluates immediately on change (default).
1932
+ * @internal
1933
+ */
1934
+ type OnChangeHttpDerivationLogicConfig = HttpDerivationBase & ImmediateDerivationTrigger;
1935
+ /**
1936
+ * HTTP derivation that evaluates after a debounce period.
1937
+ * @internal
1938
+ */
1939
+ type DebouncedHttpDerivationLogicConfig = HttpDerivationBase & DebouncedDerivationTrigger;
1940
+ /**
1941
+ * Async function derivation that evaluates immediately on change (default).
1942
+ * @internal
1943
+ */
1944
+ type OnChangeAsyncFunctionDerivationLogicConfig = AsyncFunctionDerivationBase & ImmediateDerivationTrigger;
1945
+ /**
1946
+ * Async function derivation that evaluates after a debounce period.
1947
+ * @internal
1948
+ */
1949
+ type DebouncedAsyncFunctionDerivationLogicConfig = AsyncFunctionDerivationBase & DebouncedDerivationTrigger;
1950
+ /**
1951
+ * Expression derivation that evaluates immediately on change (default).
1952
+ * @internal
1953
+ */
1954
+ type OnChangeExpressionDerivationLogicConfig = ExpressionDerivationBase & ImmediateDerivationTrigger;
1955
+ /**
1956
+ * Expression derivation that evaluates after a debounce period.
1957
+ * @internal
1958
+ */
1959
+ type DebouncedExpressionDerivationLogicConfig = ExpressionDerivationBase & DebouncedDerivationTrigger;
1960
+ /**
1961
+ * Value derivation that evaluates immediately on change (default).
1962
+ * @internal
1963
+ */
1964
+ type OnChangeValueDerivationLogicConfig = ValueDerivationBase & ImmediateDerivationTrigger;
1965
+ /**
1966
+ * Value derivation that evaluates after a debounce period.
1967
+ * @internal
1968
+ */
1969
+ type DebouncedValueDerivationLogicConfig = ValueDerivationBase & DebouncedDerivationTrigger;
1970
+ /**
1971
+ * Function derivation that evaluates immediately on change (default).
1972
+ * @internal
1973
+ */
1974
+ type OnChangeFunctionDerivationLogicConfig = FunctionDerivationBase & ImmediateDerivationTrigger;
1975
+ /**
1976
+ * Function derivation that evaluates after a debounce period.
1977
+ * @internal
1978
+ */
1979
+ type DebouncedFunctionDerivationLogicConfig = FunctionDerivationBase & DebouncedDerivationTrigger;
1980
+ /**
1981
+ * Configuration for value derivation logic.
2086
1982
  *
2087
- * Custom functions are used for conditional logic in:
2088
- * - `when` conditions (field visibility, conditional validation)
2089
- * - `readonly` logic (dynamic readonly state)
2090
- * - `disabled` logic (dynamic disabled state)
2091
- * - Dynamic value calculations
1983
+ * Enables programmatic value derivation based on conditions.
1984
+ * Derivations are self-targeting: the logic is placed on the field
1985
+ * that should receive the computed value.
2092
1986
  *
2093
- * Unlike validators, custom functions:
2094
- * - Return any value (typically boolean for conditions, but could be strings, numbers, etc.)
2095
- * - Do NOT return ValidationError objects
2096
- * - Receive EvaluationContext (field values) not FieldContext (field state)
1987
+ * @example
1988
+ * ```typescript
1989
+ * // Set phone prefix based on country selection
1990
+ * {
1991
+ * key: 'phonePrefix',
1992
+ * logic: [{
1993
+ * type: 'derivation',
1994
+ * value: '+1',
1995
+ * condition: {
1996
+ * type: 'fieldValue',
1997
+ * fieldPath: 'country',
1998
+ * operator: 'equals',
1999
+ * value: 'USA'
2000
+ * }
2001
+ * }]
2002
+ * }
2003
+ *
2004
+ * // Compute total from quantity and price
2005
+ * {
2006
+ * key: 'total',
2007
+ * derivation: 'formValue.quantity * formValue.unitPrice'
2008
+ * }
2009
+ *
2010
+ * // Use custom function for complex logic
2011
+ * {
2012
+ * key: 'currency',
2013
+ * logic: [{
2014
+ * type: 'derivation',
2015
+ * functionName: 'getCurrencyForCountry'
2016
+ * }]
2017
+ * }
2097
2018
  *
2098
- * @example
2099
- * ```typescript
2100
- * // Boolean condition function
2101
- * const isAdult: CustomFunction = (ctx) => {
2102
- * return ctx.age >= 18;
2103
- * };
2019
+ * // Self-transform with debounced trigger
2020
+ * // (applies after user stops typing)
2021
+ * {
2022
+ * key: 'email',
2023
+ * logic: [{
2024
+ * type: 'derivation',
2025
+ * expression: 'formValue.email.toLowerCase()',
2026
+ * trigger: 'debounced',
2027
+ * debounceMs: 500
2028
+ * }]
2029
+ * }
2104
2030
  *
2105
- * // Used in field configuration:
2031
+ * // Array item derivation (formValue is scoped to current item)
2106
2032
  * {
2107
- * key: 'alcoholPreference',
2108
- * type: 'select',
2109
- * when: { function: 'isAdult' } // Only show if isAdult returns true
2033
+ * key: 'lineTotal', // Inside array field
2034
+ * derivation: 'formValue.quantity * formValue.unitPrice'
2110
2035
  * }
2111
2036
  * ```
2112
2037
  *
2113
- * @example
2114
- * ```typescript
2115
- * // Calculation function
2116
- * const calculateDiscount: CustomFunction = (ctx) => {
2117
- * const price = ctx.price || 0;
2118
- * const isVip = ctx.isVip || false;
2119
- * return isVip ? price * 0.8 : price * 0.9;
2120
- * };
2121
- * ```
2038
+ * @public
2122
2039
  */
2123
- type CustomFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => unknown;
2124
-
2040
+ type DerivationLogicConfig = OnChangeHttpDerivationLogicConfig | DebouncedHttpDerivationLogicConfig | OnChangeAsyncFunctionDerivationLogicConfig | DebouncedAsyncFunctionDerivationLogicConfig | OnChangeExpressionDerivationLogicConfig | DebouncedExpressionDerivationLogicConfig | OnChangeValueDerivationLogicConfig | DebouncedValueDerivationLogicConfig | OnChangeFunctionDerivationLogicConfig | DebouncedFunctionDerivationLogicConfig;
2125
2041
  /**
2126
- * Async function for derivations — returns any value.
2127
- *
2128
- * The function receives the evaluation context and may return a Promise or Observable.
2129
- * Register functions in `customFnConfig.asyncDerivations`.
2042
+ * Union type for all logic configurations.
2130
2043
  *
2131
- * @example
2132
- * ```typescript
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
- * ```
2044
+ * - `StateLogicConfig`: For field state changes (hidden, readonly, disabled, required)
2045
+ * - `DerivationLogicConfig`: For value derivation (including property derivation via `targetProperty`)
2139
2046
  *
2140
2047
  * @public
2141
2048
  */
2142
- type AsyncDerivationFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => Promise<unknown> | Observable<unknown>;
2049
+ type LogicConfig = StateLogicConfig | DerivationLogicConfig;
2143
2050
  /**
2144
- * Async function for conditions — returns boolean.
2051
+ * Log level for derivation debug output.
2145
2052
  *
2146
- * The function receives the evaluation context and may return a Promise or Observable.
2147
- * Register functions in `customFnConfig.asyncConditions`.
2053
+ * - 'none': No debug logging
2054
+ * - 'summary': Log cycle completion with counts (default in dev mode)
2055
+ * - 'verbose': Log individual derivation evaluations with details
2148
2056
  *
2149
- * @example
2150
- * ```typescript
2151
- * const checkPermission: AsyncConditionFunction = async (context) => {
2152
- * const response = await fetch(`/api/permissions?user=${context.formValue.userId}`);
2153
- * const data = await response.json();
2154
- * return data.canEdit;
2155
- * };
2156
- * ```
2057
+ * @public
2058
+ */
2059
+ type DerivationLogLevel = 'none' | 'summary' | 'verbose';
2060
+ /**
2061
+ * Type guard to check if a condition is a FormStateCondition.
2062
+ *
2063
+ * @param condition - The condition to check
2064
+ * @returns true if the condition is a FormStateCondition
2157
2065
  *
2158
2066
  * @public
2159
2067
  */
2160
- type AsyncConditionFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => Promise<boolean> | Observable<boolean>;
2068
+ declare function isFormStateCondition(condition: StateLogicConfig['condition'] | DerivationLogicConfig['condition']): condition is FormStateCondition;
2069
+
2070
+ /**
2071
+ * Configuration for applying predefined schemas
2072
+ */
2073
+ interface SchemaApplicationConfig {
2074
+ /** Schema application type */
2075
+ type: 'apply' | 'applyWhen' | 'applyWhenValue' | 'applyEach';
2076
+ /** Schema identifier or inline schema definition */
2077
+ schema: string | SchemaDefinition;
2078
+ /** Condition for conditional application */
2079
+ condition?: ConditionalExpression;
2080
+ /** Type predicate for applyWhenValue */
2081
+ typePredicate?: string;
2082
+ }
2083
+ /**
2084
+ * Reusable schema definition that can be referenced by name
2085
+ */
2086
+ interface SchemaDefinition {
2087
+ /** Unique schema identifier */
2088
+ name: string;
2089
+ /** Schema description */
2090
+ description?: string;
2091
+ /** Field path pattern this schema applies to */
2092
+ pathPattern?: string;
2093
+ /** Validators to apply */
2094
+ validators?: ValidatorConfig[];
2095
+ /** Logic rules to apply */
2096
+ logic?: LogicConfig[];
2097
+ /** Nested schema applications */
2098
+ subSchemas?: SchemaApplicationConfig[];
2099
+ }
2100
+
2101
+ type DynamicText = string | Observable<string> | Signal<string>;
2161
2102
 
2162
2103
  /**
2163
2104
  * Re-export Angular's ValidationError for consistency
@@ -2244,7 +2185,6 @@ type SubmissionActionResult = Promise<TreeValidationResult> | Observable<TreeVal
2244
2185
  * @typeParam TValue - The form value type
2245
2186
  *
2246
2187
  * @public
2247
- * @experimental
2248
2188
  */
2249
2189
  interface SubmissionConfig<TValue = unknown> {
2250
2190
  /**
@@ -3257,6 +3197,58 @@ interface TextField extends FieldDef<TextProps> {
3257
3197
  readonly logic?: NonFieldLogicConfig[];
3258
3198
  }
3259
3199
 
3200
+ /**
3201
+ * Scalar types supported by hidden fields.
3202
+ * These are the primitive value types that can be stored.
3203
+ */
3204
+ type HiddenScalar = string | number | boolean;
3205
+ /**
3206
+ * All value types supported by hidden fields.
3207
+ * Supports both scalar values and arrays of scalars for storing
3208
+ * multiple IDs or similar data.
3209
+ */
3210
+ type HiddenValue = HiddenScalar | HiddenScalar[];
3211
+ /**
3212
+ * Hidden field definition for storing values without rendering UI.
3213
+ *
3214
+ * Hidden fields participate in the form schema and contribute to form values,
3215
+ * but do not render any visible component. They are useful for:
3216
+ * - Storing IDs when updating existing records
3217
+ * - Persisting metadata that shouldn't be user-editable
3218
+ * - Tracking computed values that need to be submitted
3219
+ *
3220
+ * Unlike the `hidden` property on other fields (which hides a rendered field),
3221
+ * this field type renders nothing at all - it's a componentless field.
3222
+ *
3223
+ * @example
3224
+ * ```typescript
3225
+ * // Store a record ID for updates
3226
+ * const idField: HiddenField<string> = {
3227
+ * type: 'hidden',
3228
+ * key: 'id',
3229
+ * value: '550e8400-e29b-41d4-a716-446655440000',
3230
+ * };
3231
+ *
3232
+ * // Store multiple tag IDs
3233
+ * const tagIdsField: HiddenField<number[]> = {
3234
+ * type: 'hidden',
3235
+ * key: 'tagIds',
3236
+ * value: [1, 2, 3],
3237
+ * };
3238
+ * ```
3239
+ *
3240
+ * @typeParam TValue - The type of value stored (must be HiddenValue compatible)
3241
+ */
3242
+ interface HiddenField<TValue extends HiddenValue = HiddenValue> extends FieldDef<never> {
3243
+ /** Discriminant for hidden field type */
3244
+ type: 'hidden';
3245
+ /**
3246
+ * The value to store in the form.
3247
+ * This is required - a hidden field without a value serves no purpose.
3248
+ */
3249
+ value: TValue;
3250
+ }
3251
+
3260
3252
  /**
3261
3253
  * Configuration for the built-in 'css' wrapper type.
3262
3254
  *
@@ -3284,17 +3276,129 @@ interface CssWrapper {
3284
3276
  * horizontal field arrangement with 12-column sizing utilities (`df-col-1`
3285
3277
  * through `df-col-12`) available to child fields via their own `col` property.
3286
3278
  *
3287
- * Applied automatically by `rowFieldMapper` when a user writes `{ type: 'row' }` —
3288
- * users do not construct this config directly.
3279
+ * Applied automatically by `rowFieldMapper` when a user writes `{ type: 'row' }` —
3280
+ * users do not construct this config directly.
3281
+ *
3282
+ * @example
3283
+ * ```typescript
3284
+ * const wrapper: RowWrapper = { type: 'row' };
3285
+ * ```
3286
+ */
3287
+ interface RowWrapper {
3288
+ readonly type: 'row';
3289
+ }
3290
+
3291
+ /**
3292
+ * Resolves a wrapper type name to its registered config interface.
3293
+ *
3294
+ * When `TWrappers` is a specific registered key (e.g., `'css'`), resolves to
3295
+ * the full config type from `FieldRegistryWrappers` (e.g., `CssWrapper`),
3296
+ * providing type-safe access to wrapper-specific properties like `cssClasses`.
3297
+ *
3298
+ * When `TWrappers` is the full `RegisteredWrapperTypes` union, distributes
3299
+ * to produce a discriminated union of all registered wrapper configs.
3300
+ *
3301
+ * @example
3302
+ * ```typescript
3303
+ * // Resolves to CssWrapper — cssClasses is typed
3304
+ * type CssConfig = WrapperConfig<'css'>;
3305
+ *
3306
+ * // Union of all registered wrapper configs
3307
+ * type AnyConfig = WrapperConfig;
3308
+ * ```
3309
+ */
3310
+ type WrapperConfig<TWrappers extends RegisteredWrapperTypes = RegisteredWrapperTypes> = TWrappers extends keyof FieldRegistryWrappers ? FieldRegistryWrappers[TWrappers] : {
3311
+ readonly type: TWrappers;
3312
+ };
3313
+ /**
3314
+ * Signature of a lazy component loader — either a direct component class or
3315
+ * an ES module whose `default` export is the component. Shared by field and
3316
+ * wrapper registrations so the `loadComponent: () => import('./x.component')`
3317
+ * idiom types cleanly in both places.
3318
+ */
3319
+ type LazyComponentLoader<T = unknown> = () => Promise<Type<T> | {
3320
+ default: Type<T>;
3321
+ }>;
3322
+ /**
3323
+ * Configuration interface for registering wrapper types.
3324
+ *
3325
+ * Defines how a wrapper component is loaded and identified. Wrapper components
3326
+ * provide visual decoration around field content (sections, headers, styling)
3327
+ * without affecting the form data structure.
3328
+ *
3329
+ * @example
3330
+ * ```typescript
3331
+ * const SectionWrapper: WrapperTypeDefinition = {
3332
+ * name: 'section',
3333
+ * loadComponent: () => import('./section-wrapper.component').then(m => m.SectionWrapperComponent),
3334
+ * };
3335
+ * ```
3336
+ */
3337
+ interface WrapperTypeDefinition<T extends WrapperConfig = WrapperConfig> {
3338
+ /** Unique identifier for the wrapper type (also serves as discriminant from FieldTypeDefinition) */
3339
+ wrapperName: string;
3340
+ /** Wrapper definition type marker (internal use) */
3341
+ _wrapper?: T;
3342
+ /**
3343
+ * Function to load the wrapper component (supports lazy loading).
3344
+ * Returns a Promise that resolves to the component class or module with default export.
3345
+ */
3346
+ loadComponent: LazyComponentLoader;
3347
+ /**
3348
+ * Field types this wrapper should auto-apply to.
3349
+ *
3350
+ * When a field's `type` matches any entry, the wrapper is injected into that
3351
+ * field's effective wrapper chain at the lowest priority (can be overridden
3352
+ * by `FormConfig.defaultWrappers` or the field-level `wrappers` array, and
3353
+ * fully cleared with `wrappers: null`).
3354
+ */
3355
+ types?: readonly string[];
3356
+ }
3357
+ /**
3358
+ * Type guard for WrapperTypeDefinition.
3359
+ *
3360
+ * Discriminates via `wrapperName` — field types use `name`, wrapper types use `wrapperName`.
3361
+ */
3362
+ declare function isWrapperTypeDefinition(value: unknown): value is WrapperTypeDefinition;
3363
+ /**
3364
+ * Contract that wrapper components must satisfy.
3365
+ *
3366
+ * Each wrapper exposes a `#fieldComponent` ViewContainerRef where the inner
3367
+ * content (next wrapper, or the field) is rendered imperatively. Config
3368
+ * properties (minus `type`) are set on the component via `setInput`; a
3369
+ * wrapper opts into a key by declaring a matching `input()`, and unknown
3370
+ * keys are silently dropped. Wrappers may additionally declare a
3371
+ * `fieldInputs` input for the wrapped field's mapper outputs (see
3372
+ * `WrapperFieldInputs` for when that's undefined).
3289
3373
  *
3290
3374
  * @example
3291
3375
  * ```typescript
3292
- * const wrapper: RowWrapper = { type: 'row' };
3376
+ * @Component({
3377
+ * template: `
3378
+ * <dbx-section [header]="header() ?? ''">
3379
+ * <ng-container #fieldComponent></ng-container>
3380
+ * </dbx-section>
3381
+ * `,
3382
+ * })
3383
+ * export class SectionWrapperComponent implements FieldWrapper {
3384
+ * readonly fieldComponent = viewChild.required('fieldComponent', { read: ViewContainerRef });
3385
+ * readonly header = input<string>();
3386
+ * readonly fieldInputs = input<WrapperFieldInputs>();
3387
+ * }
3293
3388
  * ```
3294
3389
  */
3295
- interface RowWrapper {
3296
- readonly type: 'row';
3390
+ interface FieldWrapper {
3391
+ /** ViewContainerRef slot where inner content is rendered */
3392
+ readonly fieldComponent: Signal<ViewContainerRef>;
3297
3393
  }
3394
+ /**
3395
+ * Injection token for the wrapper type registry.
3396
+ *
3397
+ * Provides access to the map of registered wrapper types. The registry is
3398
+ * populated via `provideDynamicForm()` and used by `ContainerFieldComponent` to
3399
+ * resolve wrapper types to their component implementations.
3400
+ */
3401
+ declare const WRAPPER_REGISTRY: InjectionToken<Map<string, WrapperTypeDefinition<RowWrapper | _ng_forge_dynamic_forms.CssWrapper>>>;
3298
3402
 
3299
3403
  /**
3300
3404
  * Container field interface for wrapping child fields with UI chrome.
@@ -3346,36 +3450,281 @@ interface RowWrapper {
3346
3450
  * };
3347
3451
  * ```
3348
3452
  */
3349
- interface ContainerField<TFields extends readonly ContainerAllowedChildren[] = readonly ContainerAllowedChildren[], TWrapperConfigs extends readonly WrapperConfig[] = readonly WrapperConfig[]> extends FieldDef<never> {
3350
- type: 'container';
3351
- /** Child definitions to render within this container */
3352
- readonly fields: TFields;
3453
+ interface ContainerField<TFields extends readonly ContainerAllowedChildren[] = readonly ContainerAllowedChildren[], TWrapperConfigs extends readonly WrapperConfig[] = readonly WrapperConfig[]> extends FieldDef<never> {
3454
+ type: 'container';
3455
+ /** Child definitions to render within this container */
3456
+ readonly fields: TFields;
3457
+ /**
3458
+ * Wrapper components to chain around the children.
3459
+ * Applied outermost-first: the first wrapper in the array is the outermost.
3460
+ * Each wrapper component receives the subsequent wrapper (or children) inside
3461
+ * its `#fieldComponent` ViewContainerRef slot.
3462
+ */
3463
+ readonly wrappers: TWrapperConfigs;
3464
+ /** Container fields do not have a label property */
3465
+ readonly label?: never;
3466
+ /** Containers do not support meta — they have no native form element */
3467
+ readonly meta?: never;
3468
+ /**
3469
+ * Logic configurations for conditional container visibility.
3470
+ * Only 'hidden' type logic is supported for containers.
3471
+ */
3472
+ readonly logic?: ContainerLogicConfig[];
3473
+ }
3474
+ /**
3475
+ * Type guard for ContainerField with proper type narrowing.
3476
+ *
3477
+ * `wrappers` is part of the type but optional in practice — many configs use
3478
+ * containers purely as flex/layout wrappers without any wrapper components.
3479
+ * Match on `type === 'container'` and the `fields` shape only, mirroring
3480
+ * `isPageField` / `isRowField`.
3481
+ */
3482
+ declare function isContainerTypedField(field: FieldDef<any>): field is ContainerField;
3483
+
3484
+ /**
3485
+ * Type constraints for field nesting rules
3486
+ * These ensure that container fields can only contain valid child field types
3487
+ *
3488
+ * Note: We explicitly list types instead of using Exclude to avoid circular dependencies
3489
+ */
3490
+ /**
3491
+ * Fields that are allowed as children of Page fields
3492
+ * Pages can contain: rows, groups, arrays, and leaf fields (but NOT other pages)
3493
+ */
3494
+ type PageAllowedChildren = LeafFieldTypes | RowField | GroupField | ArrayField | SimplifiedArrayField | ContainerField;
3495
+ /**
3496
+ * Fields that are allowed as children of Group fields
3497
+ * Groups can contain: rows and leaf fields (but NOT pages or other groups)
3498
+ */
3499
+ type GroupAllowedChildren = LeafFieldTypes | RowField | ContainerField;
3500
+ /**
3501
+ * Fields that are allowed as children of Array fields
3502
+ * Arrays can contain: rows, groups, and leaf fields (but NOT pages or other arrays)
3503
+ * Groups are used for creating object arrays where each array item is an object
3504
+ */
3505
+ type ArrayAllowedChildren = LeafFieldTypes | RowField | GroupField | ContainerField;
3506
+ /**
3507
+ * Fields that are allowed as children of Container fields.
3508
+ * Containers are pure layout primitives that flatten their children into the
3509
+ * parent form, so most registered field types may appear inside — including
3510
+ * hidden fields and other containers — but NOT pages.
3511
+ */
3512
+ type ContainerAllowedChildren = LeafFieldTypes | RowField | GroupField | ArrayField | SimplifiedArrayField | ContainerField;
3513
+ /**
3514
+ * Row is a synthetic field type that resolves to a Container at runtime, so
3515
+ * it accepts the same children as a Container.
3516
+ */
3517
+ type RowAllowedChildren = ContainerAllowedChildren;
3518
+
3519
+ /**
3520
+ * An array item template is an array of allowed children that defines one OBJECT array item's fields.
3521
+ * Used for object arrays where each item is `{ key1: value1, key2: value2, ... }`.
3522
+ */
3523
+ type ArrayItemTemplate = readonly ArrayAllowedChildren[];
3524
+ /**
3525
+ * An array item definition can be either:
3526
+ * - A single field (ArrayAllowedChildren) for PRIMITIVE array items - extracts field value directly
3527
+ * - An array of fields (ArrayItemTemplate) for OBJECT array items - merges fields into object
3528
+ *
3529
+ * This allows support for:
3530
+ * - Primitive arrays: `['tag1', 'tag2']`
3531
+ * - Object arrays: `[{ name: 'Alice', email: '...' }]`
3532
+ * - Heterogeneous arrays: `[{ value: 'x' }, 'y']` (mixed primitives and objects)
3533
+ */
3534
+ type ArrayItemDefinition = ArrayAllowedChildren | ArrayItemTemplate;
3535
+ /**
3536
+ * Array field interface for creating dynamic field collections that map to array values.
3537
+ *
3538
+ * Key concepts:
3539
+ * - The outer `fields` array defines INITIAL ITEMS (each element is one array item to render)
3540
+ * - Each element can be either:
3541
+ * - A single FieldDef (primitive item) - the field's value is extracted directly
3542
+ * - An array of FieldDefs (object item) - fields are merged into an object
3543
+ * - Items can have different field configurations (heterogeneous arrays supported)
3544
+ * - Empty `fields: []` means no initial items - user adds via buttons with explicit templates
3545
+ * - Initial values are declared via `value` property on each field definition
3546
+ *
3547
+ * @example
3548
+ * // Primitive array: ['angular', 'typescript']
3549
+ * {
3550
+ * key: 'tags',
3551
+ * type: 'array',
3552
+ * fields: [
3553
+ * { key: 'tag', type: 'input', value: 'angular' }, // Single field = primitive
3554
+ * { key: 'tag', type: 'input', value: 'typescript' },
3555
+ * ]
3556
+ * }
3557
+ *
3558
+ * @example
3559
+ * // Object array: [{ name: 'Alice', email: '...' }]
3560
+ * {
3561
+ * key: 'contacts',
3562
+ * type: 'array',
3563
+ * fields: [
3564
+ * [ // Array = object
3565
+ * { key: 'name', type: 'input', value: 'Alice' },
3566
+ * { key: 'email', type: 'input', value: 'alice@example.com' }
3567
+ * ],
3568
+ * ]
3569
+ * }
3570
+ *
3571
+ * @example
3572
+ * // Heterogeneous array: [{ label: 'Structured' }, 'Simple']
3573
+ * {
3574
+ * key: 'items',
3575
+ * type: 'array',
3576
+ * fields: [
3577
+ * [{ key: 'label', type: 'input', value: 'Structured' }], // Object item
3578
+ * { key: 'value', type: 'input', value: 'Simple' }, // Primitive item
3579
+ * ]
3580
+ * }
3581
+ *
3582
+ * @example
3583
+ * // Empty array (add items via buttons)
3584
+ * {
3585
+ * key: 'tags',
3586
+ * type: 'array',
3587
+ * fields: [] // No initial items - user adds via button with template
3588
+ * }
3589
+ *
3590
+ * Nesting constraints: Arrays can contain rows, leaf fields, or groups (for object arrays),
3591
+ * but NOT pages or other arrays. Runtime validation enforces these rules.
3592
+ *
3593
+ * Note: Arrays are container fields and do not support `meta` since they have no native form element.
3594
+ */
3595
+ interface ArrayField<TFields extends readonly ArrayItemDefinition[] = readonly ArrayItemDefinition[]> extends FieldDef<never> {
3596
+ type: 'array';
3597
+ /**
3598
+ * Array of item definitions. Each element defines one array item:
3599
+ * - Single FieldDef = primitive item (field's value is extracted directly)
3600
+ * - Array of FieldDefs = object item (fields are merged into an object)
3601
+ * - Empty array `[]` = no initial items (add via buttons with explicit templates)
3602
+ * - Items can have different configurations (heterogeneous arrays supported)
3603
+ * - Initial values are set via `value` property on each field definition
3604
+ */
3605
+ readonly fields: TFields;
3606
+ /** Array fields do not have a label property **/
3607
+ readonly label?: never;
3608
+ /** Arrays do not support meta - they have no native form element **/
3609
+ readonly meta?: never;
3610
+ /**
3611
+ * Logic configurations for conditional array visibility.
3612
+ * Only 'hidden' type logic is supported for arrays.
3613
+ */
3614
+ readonly logic?: ContainerLogicConfig[];
3615
+ /**
3616
+ * Minimum number of items required in the array.
3617
+ * Validation fails if the array has fewer items than this value.
3618
+ */
3619
+ readonly minLength?: number;
3620
+ /**
3621
+ * Maximum number of items allowed in the array.
3622
+ * Validation fails if the array has more items than this value.
3623
+ */
3624
+ readonly maxLength?: number;
3625
+ }
3626
+ /**
3627
+ * Type guard for ArrayField with proper type narrowing.
3628
+ * Validates that the field is an array type with a fields property that is an array.
3629
+ * Fields can contain either single FieldDefs (primitive items) or arrays of FieldDefs (object items).
3630
+ */
3631
+ declare function isArrayField(field: FieldDef<any>): field is ArrayField;
3632
+ /**
3633
+ * Configuration for auto-generated add/remove buttons in simplified array fields.
3634
+ */
3635
+ interface ArrayButtonConfig {
3636
+ /** Custom label for the button */
3637
+ readonly label?: string;
3638
+ /** Additional properties passed to the button component */
3639
+ readonly props?: Record<string, unknown>;
3640
+ }
3641
+ /**
3642
+ * Simplified array field interface for common use cases.
3643
+ *
3644
+ * Instead of manually specifying each item in `fields`, provide a `template` that defines
3645
+ * the structure of a single item, and a `value` array with initial data.
3646
+ * The library auto-generates add/remove buttons.
3647
+ *
3648
+ * Discriminant: `template` presence → simplified API; `Array.isArray(template)` → object vs primitive.
3649
+ *
3650
+ * @example
3651
+ * // Primitive array: ['angular', 'typescript']
3652
+ * {
3653
+ * key: 'tags',
3654
+ * type: 'array',
3655
+ * template: { key: 'value', type: 'input', label: 'Tag', required: true },
3656
+ * value: ['angular', 'typescript']
3657
+ * }
3658
+ *
3659
+ * @example
3660
+ * // Object array: [{ name: 'Jane', phone: '555' }]
3661
+ * {
3662
+ * key: 'contacts',
3663
+ * type: 'array',
3664
+ * template: [
3665
+ * { key: 'name', type: 'input', label: 'Contact Name', required: true },
3666
+ * { key: 'phone', type: 'input', label: 'Phone Number' },
3667
+ * ],
3668
+ * value: [{ name: 'Jane', phone: '555' }]
3669
+ * }
3670
+ *
3671
+ * @example
3672
+ * // Button customization / opt-out
3673
+ * {
3674
+ * key: 'tags',
3675
+ * type: 'array',
3676
+ * template: { key: 'value', type: 'input', label: 'Tag' },
3677
+ * addButton: { label: 'Add Tag', props: { color: 'primary' } },
3678
+ * removeButton: false
3679
+ * }
3680
+ */
3681
+ interface SimplifiedArrayField extends FieldDef<never> {
3682
+ type: 'array';
3353
3683
  /**
3354
- * Wrapper components to chain around the children.
3355
- * Applied outermost-first: the first wrapper in the array is the outermost.
3356
- * Each wrapper component receives the subsequent wrapper (or children) inside
3357
- * its `#fieldComponent` ViewContainerRef slot.
3684
+ * Template defining the structure of a single array item.
3685
+ * - Single field (ArrayAllowedChildren) → primitive array (each item is a single value)
3686
+ * - Array of fields (readonly ArrayAllowedChildren[]) → object array (each item is an object)
3358
3687
  */
3359
- readonly wrappers: TWrapperConfigs;
3360
- /** Container fields do not have a label property */
3688
+ readonly template: ArrayAllowedChildren | readonly ArrayAllowedChildren[];
3689
+ /** Initial values for the array. Each element creates one array item. */
3690
+ readonly value?: readonly unknown[];
3691
+ /**
3692
+ * Configuration for the auto-generated "Add" button, or `false` to disable it.
3693
+ * Defaults to a button with label "Add".
3694
+ */
3695
+ readonly addButton?: ArrayButtonConfig | false;
3696
+ /**
3697
+ * Configuration for the auto-generated "Remove" button on each item, or `false` to disable it.
3698
+ * Defaults to a button with label "Remove".
3699
+ */
3700
+ readonly removeButton?: ArrayButtonConfig | false;
3701
+ /** Simplified arrays do not support the label property */
3361
3702
  readonly label?: never;
3362
- /** Containers do not support meta — they have no native form element */
3703
+ /** Simplified arrays do not support meta */
3363
3704
  readonly meta?: never;
3364
3705
  /**
3365
- * Logic configurations for conditional container visibility.
3366
- * Only 'hidden' type logic is supported for containers.
3706
+ * Logic configurations for conditional array visibility.
3707
+ * Only 'hidden' type logic is supported for arrays.
3367
3708
  */
3368
3709
  readonly logic?: ContainerLogicConfig[];
3710
+ /**
3711
+ * Minimum number of items required in the array.
3712
+ * Validation fails if the array has fewer items than this value.
3713
+ */
3714
+ readonly minLength?: number;
3715
+ /**
3716
+ * Maximum number of items allowed in the array.
3717
+ * Validation fails if the array has more items than this value.
3718
+ */
3719
+ readonly maxLength?: number;
3720
+ /** Mutually exclusive with `template` — use `fields` for the full API instead */
3721
+ readonly fields?: never;
3369
3722
  }
3370
3723
  /**
3371
- * Type guard for ContainerField with proper type narrowing.
3372
- *
3373
- * `wrappers` is part of the type but optional in practice — many configs use
3374
- * containers purely as flex/layout wrappers without any wrapper components.
3375
- * Match on `type === 'container'` and the `fields` shape only, mirroring
3376
- * `isPageField` / `isRowField`.
3724
+ * Type guard for SimplifiedArrayField.
3725
+ * Checks for `type: 'array'` with a `template` property (discriminant from full ArrayField).
3377
3726
  */
3378
- declare function isContainerTypedField(field: FieldDef<any>): field is ContainerField;
3727
+ declare function isSimplifiedArrayField(field: FieldDef<any>): field is SimplifiedArrayField;
3379
3728
 
3380
3729
  /**
3381
3730
  * Container fields registry - augment this interface to add custom container fields
@@ -3525,180 +3874,6 @@ type NarrowField<T> = T extends {
3525
3874
  */
3526
3875
  type NarrowFields = readonly NarrowField<RegisteredFieldTypes>[];
3527
3876
 
3528
- /**
3529
- * Type constraints for field nesting rules
3530
- * These ensure that container fields can only contain valid child field types
3531
- *
3532
- * Note: We explicitly list types instead of using Exclude to avoid circular dependencies
3533
- */
3534
- /**
3535
- * Fields that are allowed as children of Page fields
3536
- * Pages can contain: rows, groups, arrays, and leaf fields (but NOT other pages)
3537
- */
3538
- type PageAllowedChildren = LeafFieldTypes | RowField | GroupField | ArrayField | SimplifiedArrayField | ContainerField;
3539
- /**
3540
- * Fields that are allowed as children of Group fields
3541
- * Groups can contain: rows and leaf fields (but NOT pages or other groups)
3542
- */
3543
- type GroupAllowedChildren = LeafFieldTypes | RowField | ContainerField;
3544
- /**
3545
- * Fields that are allowed as children of Array fields
3546
- * Arrays can contain: rows, groups, and leaf fields (but NOT pages or other arrays)
3547
- * Groups are used for creating object arrays where each array item is an object
3548
- */
3549
- type ArrayAllowedChildren = LeafFieldTypes | RowField | GroupField | ContainerField;
3550
- /**
3551
- * Fields that are allowed as children of Container fields.
3552
- * Containers are pure layout primitives that flatten their children into the
3553
- * parent form, so most registered field types may appear inside — including
3554
- * hidden fields and other containers — but NOT pages.
3555
- */
3556
- type ContainerAllowedChildren = LeafFieldTypes | RowField | GroupField | ArrayField | SimplifiedArrayField | ContainerField;
3557
- /**
3558
- * Row is a synthetic field type that resolves to a Container at runtime, so
3559
- * it accepts the same children as a Container.
3560
- */
3561
- type RowAllowedChildren = ContainerAllowedChildren;
3562
-
3563
- /**
3564
- * Row field interface for creating horizontal layouts.
3565
- * A row is a synthetic field type that resolves to a Container at runtime,
3566
- * with a synthesized `{ type: 'row' }` wrapper applied for layout. The row
3567
- * itself does not hold a value — its children flatten into the parent form.
3568
- *
3569
- * Note: Rows do not support `meta` since they have no native form element.
3570
- */
3571
- interface RowField<TFields extends readonly RowAllowedChildren[] = readonly RowAllowedChildren[]> extends FieldDef<never> {
3572
- type: 'row';
3573
- /** Child definitions to render within this row */
3574
- readonly fields: TFields;
3575
- /** Row fields do not have a label property **/
3576
- readonly label?: never;
3577
- /** Rows do not support meta - they have no native form element **/
3578
- readonly meta?: never;
3579
- /**
3580
- * Logic configurations for conditional row visibility.
3581
- * Only 'hidden' type logic is supported for rows.
3582
- */
3583
- readonly logic?: ContainerLogicConfig[];
3584
- }
3585
- /**
3586
- * Type guard for RowField with proper type narrowing
3587
- */
3588
- declare function isRowField(field: FieldDef<any>): field is RowField;
3589
-
3590
- /**
3591
- * Resolves a wrapper type name to its registered config interface.
3592
- *
3593
- * When `TWrappers` is a specific registered key (e.g., `'css'`), resolves to
3594
- * the full config type from `FieldRegistryWrappers` (e.g., `CssWrapper`),
3595
- * providing type-safe access to wrapper-specific properties like `cssClasses`.
3596
- *
3597
- * When `TWrappers` is the full `RegisteredWrapperTypes` union, distributes
3598
- * to produce a discriminated union of all registered wrapper configs.
3599
- *
3600
- * @example
3601
- * ```typescript
3602
- * // Resolves to CssWrapper — cssClasses is typed
3603
- * type CssConfig = WrapperConfig<'css'>;
3604
- *
3605
- * // Union of all registered wrapper configs
3606
- * type AnyConfig = WrapperConfig;
3607
- * ```
3608
- */
3609
- type WrapperConfig<TWrappers extends RegisteredWrapperTypes = RegisteredWrapperTypes> = TWrappers extends keyof FieldRegistryWrappers ? FieldRegistryWrappers[TWrappers] : {
3610
- readonly type: TWrappers;
3611
- };
3612
- /**
3613
- * Signature of a lazy component loader — either a direct component class or
3614
- * an ES module whose `default` export is the component. Shared by field and
3615
- * wrapper registrations so the `loadComponent: () => import('./x.component')`
3616
- * idiom types cleanly in both places.
3617
- */
3618
- type LazyComponentLoader<T = unknown> = () => Promise<Type<T> | {
3619
- default: Type<T>;
3620
- }>;
3621
- /**
3622
- * Configuration interface for registering wrapper types.
3623
- *
3624
- * Defines how a wrapper component is loaded and identified. Wrapper components
3625
- * provide visual decoration around field content (sections, headers, styling)
3626
- * without affecting the form data structure.
3627
- *
3628
- * @example
3629
- * ```typescript
3630
- * const SectionWrapper: WrapperTypeDefinition = {
3631
- * name: 'section',
3632
- * loadComponent: () => import('./section-wrapper.component').then(m => m.SectionWrapperComponent),
3633
- * };
3634
- * ```
3635
- */
3636
- interface WrapperTypeDefinition<T extends WrapperConfig = WrapperConfig> {
3637
- /** Unique identifier for the wrapper type (also serves as discriminant from FieldTypeDefinition) */
3638
- wrapperName: string;
3639
- /** Wrapper definition type marker (internal use) */
3640
- _wrapper?: T;
3641
- /**
3642
- * Function to load the wrapper component (supports lazy loading).
3643
- * Returns a Promise that resolves to the component class or module with default export.
3644
- */
3645
- loadComponent: LazyComponentLoader;
3646
- /**
3647
- * Field types this wrapper should auto-apply to.
3648
- *
3649
- * When a field's `type` matches any entry, the wrapper is injected into that
3650
- * field's effective wrapper chain at the lowest priority (can be overridden
3651
- * by `FormConfig.defaultWrappers` or the field-level `wrappers` array, and
3652
- * fully cleared with `wrappers: null`).
3653
- */
3654
- types?: readonly string[];
3655
- }
3656
- /**
3657
- * Type guard for WrapperTypeDefinition.
3658
- *
3659
- * Discriminates via `wrapperName` — field types use `name`, wrapper types use `wrapperName`.
3660
- */
3661
- declare function isWrapperTypeDefinition(value: unknown): value is WrapperTypeDefinition;
3662
- /**
3663
- * Contract that wrapper components must satisfy.
3664
- *
3665
- * Each wrapper exposes a `#fieldComponent` ViewContainerRef where the inner
3666
- * content (next wrapper, or the field) is rendered imperatively. Config
3667
- * properties (minus `type`) are set on the component via `setInput`; a
3668
- * wrapper opts into a key by declaring a matching `input()`, and unknown
3669
- * keys are silently dropped. Wrappers may additionally declare a
3670
- * `fieldInputs` input for the wrapped field's mapper outputs (see
3671
- * `WrapperFieldInputs` for when that's undefined).
3672
- *
3673
- * @example
3674
- * ```typescript
3675
- * @Component({
3676
- * template: `
3677
- * <dbx-section [header]="header() ?? ''">
3678
- * <ng-container #fieldComponent></ng-container>
3679
- * </dbx-section>
3680
- * `,
3681
- * })
3682
- * export class SectionWrapperComponent implements FieldWrapper {
3683
- * readonly fieldComponent = viewChild.required('fieldComponent', { read: ViewContainerRef });
3684
- * readonly header = input<string>();
3685
- * readonly fieldInputs = input<WrapperFieldInputs>();
3686
- * }
3687
- * ```
3688
- */
3689
- interface FieldWrapper {
3690
- /** ViewContainerRef slot where inner content is rendered */
3691
- readonly fieldComponent: Signal<ViewContainerRef>;
3692
- }
3693
- /**
3694
- * Injection token for the wrapper type registry.
3695
- *
3696
- * Provides access to the map of registered wrapper types. The registry is
3697
- * populated via `provideDynamicForm()` and used by `ContainerFieldComponent` to
3698
- * resolve wrapper types to their component implementations.
3699
- */
3700
- declare const WRAPPER_REGISTRY: InjectionToken<Map<string, WrapperTypeDefinition<RowWrapper | _ng_forge_dynamic_forms.CssWrapper>>>;
3701
-
3702
3877
  /**
3703
3878
  * Base interface for native HTML attributes that can be passed to form field elements.
3704
3879
  *
@@ -3798,18 +3973,26 @@ interface FieldDef<TProps, TMeta extends FieldMeta = FieldMeta> {
3798
3973
  /**
3799
3974
  * Field type identifier for component selection.
3800
3975
  *
3801
- * Determines which component will be rendered for this field.
3802
- * Must match a registered field type name in the field registry.
3976
+ * Determines which component will be rendered for this field. Names
3977
+ * registered through module augmentation of `DynamicFormFieldRegistry`
3978
+ * are surfaced as IDE autocomplete suggestions via `RegisteredFieldTypes`;
3979
+ * the `(string & {})` branch keeps arbitrary custom identifiers accepted
3980
+ * as an escape hatch for fields that haven't been augmented into the
3981
+ * registry.
3803
3982
  *
3804
3983
  * @example
3805
3984
  * ```typescript
3806
- * type: 'input' // Text input field
3807
- * type: 'select' // Dropdown selection
3808
- * type: 'checkbox' // Boolean checkbox
3809
- * type: 'group' // Field group container
3985
+ * type: 'input' // Text input field (registered)
3986
+ * type: 'select' // Dropdown selection (registered)
3987
+ * type: 'checkbox' // Boolean checkbox (registered)
3988
+ * type: 'group' // Field group container (registered)
3989
+ * type: 'my-custom' // Unregistered — still accepted, no autocomplete
3810
3990
  * ```
3991
+ *
3992
+ * Wrapper types are intentionally excluded — wrappers are configured through
3993
+ * `wrappers` / `withFormWrappers()` rather than via the field-type discriminant.
3811
3994
  */
3812
- type: string;
3995
+ type: RegisteredFieldTypes['type'] | (string & {});
3813
3996
  /**
3814
3997
  * Human-readable field label displayed to users.
3815
3998
  *
@@ -4083,6 +4266,21 @@ interface ArrayContext {
4083
4266
  /** The array field definition. */
4084
4267
  field: FieldDef<unknown>;
4085
4268
  }
4269
+ /**
4270
+ * Group context for fields rendered inside `group` containers.
4271
+ *
4272
+ * Carries a reactive dot-separated chain of group ancestors (including the
4273
+ * current group). Used by `mapFieldToInputs` to scope DOM IDs of group
4274
+ * children, so the same leaf key can appear inside different groups without
4275
+ * producing duplicate `id` attributes (issue #401).
4276
+ *
4277
+ * Re-provided by every group's child injector — a nested group reads its
4278
+ * parent's context signal and composes its own path through `computed()`.
4279
+ */
4280
+ interface GroupContext {
4281
+ /** Dot-separated path of group ancestors, including the current group's key (e.g. `'address'` or `'user.address'`). */
4282
+ groupPath: Signal<string>;
4283
+ }
4086
4284
  /**
4087
4285
  * Mapper function that converts a field definition to component inputs.
4088
4286
  *
@@ -4183,7 +4381,7 @@ interface FieldTypeDefinition<T extends FieldDef<any> = any> {
4183
4381
  /** Semantic scope for tooling to discover interchangeable field alternatives */
4184
4382
  scope?: FieldScope | FieldScope[];
4185
4383
  /** Mapped component inputs that must exist before the renderer instantiates the component */
4186
- renderReadyWhen?: RenderReadyInput[];
4384
+ renderReadyWhen?: readonly RenderReadyInput[];
4187
4385
  }
4188
4386
  /**
4189
4387
  * Injection token for the global field type registry.
@@ -4212,6 +4410,15 @@ interface ResolvedField {
4212
4410
  injector: Injector;
4213
4411
  inputs: Signal<Record<string, unknown>>;
4214
4412
  renderReady: Signal<boolean>;
4413
+ /**
4414
+ * Whether the field is currently hidden. Sourced from the FieldTree's
4415
+ * `state.hidden()` for value-bearing fields (set by Angular Signal Forms in
4416
+ * response to `hidden(path, …)` calls), or from the mapper's explicit
4417
+ * `hidden` input for non-value fields (button, text, container, …). Consumers
4418
+ * must `@if (!field.hidden())` around `*dfFieldOutlet` so hidden fields are
4419
+ * removed from the DOM — Angular Signal Forms warns (NG01916) otherwise.
4420
+ */
4421
+ hidden: Signal<boolean>;
4215
4422
  }
4216
4423
 
4217
4424
  /**
@@ -4782,6 +4989,26 @@ declare const FIELD_SIGNAL_CONTEXT: InjectionToken<FieldSignalContext<Record<str
4782
4989
  * ```
4783
4990
  */
4784
4991
  declare const ARRAY_CONTEXT: InjectionToken<ArrayContext>;
4992
+ /**
4993
+ * Injection token for providing group ancestry to mappers and components.
4994
+ *
4995
+ * Re-provided in two places:
4996
+ * 1. `GroupFieldComponent` provides a context whose `groupPath()` is the
4997
+ * parent path extended by its own key (e.g. `'address'` or `'user.address'`).
4998
+ * 2. `createArrayItemInjector` provides a sentinel with an empty `groupPath`,
4999
+ * resetting the chain at array boundaries — descendants of an array item
5000
+ * that re-enter a group compose paths INSIDE the array item, matching the
5001
+ * property-derivation collector's keying.
5002
+ *
5003
+ * `mapFieldToInputs` reads this to scope DOM IDs and override-store keys so
5004
+ * the same leaf key can appear in different groups without colliding (#401).
5005
+ *
5006
+ * Inject with `{ optional: true }` because top-level fields with no group
5007
+ * ancestor (and no enclosing array) won't have the token provided. The token
5008
+ * IS present for fields inside an array even when no inner group exists — the
5009
+ * array-boundary sentinel makes injection succeed with an empty `groupPath`.
5010
+ */
5011
+ declare const GROUP_CONTEXT: InjectionToken<GroupContext>;
4785
5012
  /**
4786
5013
  * Injection token for form-level default props.
4787
5014
  *
@@ -6284,8 +6511,6 @@ declare function applyMetaToElement(element: Element, meta: FieldMeta | undefine
6284
6511
  * );
6285
6512
  * // user.value() keeps the old user data during loading transitions
6286
6513
  * ```
6287
- *
6288
- * @experimental Uses Angular's experimental resource composition APIs.
6289
6514
  */
6290
6515
  declare function withPreviousValue<T>(input: Resource<T>): Resource<T>;
6291
6516
 
@@ -6336,6 +6561,7 @@ declare class GroupFieldComponent<TModel extends Record<string, unknown> = Recor
6336
6561
  private readonly destroyRef;
6337
6562
  private readonly fieldRegistry;
6338
6563
  private readonly parentFieldSignalContext;
6564
+ private readonly parentGroupContext;
6339
6565
  private readonly injector;
6340
6566
  protected readonly environmentInjector: EnvironmentInjector;
6341
6567
  private readonly eventBus;
@@ -6365,6 +6591,13 @@ declare class GroupFieldComponent<TModel extends Record<string, unknown> = Recor
6365
6591
  readonly disabled: _angular_core.Signal<boolean>;
6366
6592
  private readonly nestedFieldTree;
6367
6593
  private readonly groupFieldSignalContext;
6594
+ /**
6595
+ * Group context propagated to descendants — carries the dot-separated path of
6596
+ * group ancestors so child fields can scope their DOM IDs (issue #401).
6597
+ * Reactive via `computed()`: a nested group composes its path from the parent
6598
+ * context signal + its own key, so the chain stays consistent if either changes.
6599
+ */
6600
+ private readonly groupContext;
6368
6601
  private readonly groupInjector;
6369
6602
  readonly validityChange: _angular_core.OutputRef<boolean>;
6370
6603
  readonly dirtyChange: _angular_core.OutputRef<boolean>;
@@ -6396,6 +6629,12 @@ interface ResolvedArrayItemField {
6396
6629
  inputs: Signal<Record<string, unknown>>;
6397
6630
  /** Whether required mapped inputs are available for safe component instantiation. */
6398
6631
  renderReady: Signal<boolean>;
6632
+ /**
6633
+ * Whether the array-item field is currently hidden — mirrors `ResolvedField.hidden`
6634
+ * so the same `@if (!field.hidden())` template gate works for top-level and
6635
+ * array-item fields.
6636
+ */
6637
+ hidden: Signal<boolean>;
6399
6638
  }
6400
6639
  /**
6401
6640
  * Resolved array item ready for declarative rendering with ngComponentOutlet.
@@ -6426,7 +6665,9 @@ declare class ArrayFieldComponent<TModel extends Record<string, unknown> = Recor
6426
6665
  protected readonly environmentInjector: EnvironmentInjector;
6427
6666
  private readonly eventBus;
6428
6667
  private readonly logger;
6429
- private readonly templateRegistry;
6668
+ private readonly arrayItemRegistry;
6669
+ private readonly slot;
6670
+ /** Bound id-generator reference threaded to `resolveArrayItem` so callsites stay terse. */
6430
6671
  private readonly generateItemId;
6431
6672
  field: _angular_core.InputSignal<ArrayField<readonly ArrayItemDefinition[]>>;
6432
6673
  key: _angular_core.InputSignal<string>;
@@ -6491,9 +6732,7 @@ declare class ArrayFieldComponent<TModel extends Record<string, unknown> = Recor
6491
6732
  private readonly itemTemplates;
6492
6733
  private readonly arrayFieldTrees;
6493
6734
  private readonly resolvedItemsSignal;
6494
- private readonly updateVersion;
6495
- private readonly pendingInitializationCycle;
6496
- private readonly settledInitializationCycle;
6735
+ private readonly fsm;
6497
6736
  /**
6498
6737
  * Map of item IDs to their current positions. O(1) lookup vs O(n) indexOf().
6499
6738
  * Used by child linkedSignals to reactively track their position in the array.
@@ -6756,5 +6995,5 @@ declare class RootFormRegistryService {
6756
6995
  */
6757
6996
  declare const INITIALIZATION_TIMEOUT_MS: InjectionToken<number>;
6758
6997
 
6759
- 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, withValidationExecutionDefaults, withValueExclusionDefaults, wrapperProps };
6760
- 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, 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 };
6998
+ 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 };
6999
+ 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 };