@ng-forge/dynamic-forms 0.10.0-next.1 → 0.10.0-next.3

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 (41) hide show
  1. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-GiaVHO9N.mjs → ng-forge-dynamic-forms-array-field.component-D6GEsH72.mjs} +99 -476
  2. package/fesm2022/ng-forge-dynamic-forms-array-field.component-D6GEsH72.mjs.map +1 -0
  3. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-C554hOxN.mjs → ng-forge-dynamic-forms-container-field.component-BluTT44A.mjs} +3 -14
  4. package/fesm2022/ng-forge-dynamic-forms-container-field.component-BluTT44A.mjs.map +1 -0
  5. package/fesm2022/ng-forge-dynamic-forms-container-utils-diYAWMa_.mjs +23 -0
  6. package/fesm2022/ng-forge-dynamic-forms-container-utils-diYAWMa_.mjs.map +1 -0
  7. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-CnJgFWLB.mjs → ng-forge-dynamic-forms-css-wrapper.component-CgMQOaG7.mjs} +2 -22
  8. package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-CgMQOaG7.mjs.map +1 -0
  9. package/fesm2022/ng-forge-dynamic-forms-emit-initialization-CB86IMsQ.mjs +17 -0
  10. package/fesm2022/ng-forge-dynamic-forms-emit-initialization-CB86IMsQ.mjs.map +1 -0
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BJ8j7Sh0.mjs → ng-forge-dynamic-forms-group-field.component-2bWHql4u.mjs} +15 -63
  12. package/fesm2022/ng-forge-dynamic-forms-group-field.component-2bWHql4u.mjs.map +1 -0
  13. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +72 -844
  14. package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
  15. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CPkVjU_N.mjs +10876 -0
  16. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CPkVjU_N.mjs.map +1 -0
  17. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs → ng-forge-dynamic-forms-page-field.component-zF-vEv5V.mjs} +10 -38
  18. package/fesm2022/ng-forge-dynamic-forms-page-field.component-zF-vEv5V.mjs.map +1 -0
  19. package/fesm2022/{ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs → ng-forge-dynamic-forms-row-wrapper.component-CwkYn5yl.mjs} +2 -18
  20. package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CwkYn5yl.mjs.map +1 -0
  21. package/fesm2022/ng-forge-dynamic-forms-schema.mjs +0 -73
  22. package/fesm2022/ng-forge-dynamic-forms-schema.mjs.map +1 -1
  23. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BGw6T-4h.mjs → ng-forge-dynamic-forms-text-field.component-_8_vVeH0.mjs} +2 -2
  24. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BGw6T-4h.mjs.map → ng-forge-dynamic-forms-text-field.component-_8_vVeH0.mjs.map} +1 -1
  25. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  26. package/package.json +1 -1
  27. package/types/ng-forge-dynamic-forms-integration.d.ts +27 -462
  28. package/types/ng-forge-dynamic-forms-schema.d.ts +2 -117
  29. package/types/ng-forge-dynamic-forms.d.ts +286 -4650
  30. package/fesm2022/ng-forge-dynamic-forms-array-field.component-GiaVHO9N.mjs.map +0 -1
  31. package/fesm2022/ng-forge-dynamic-forms-container-field.component-C554hOxN.mjs.map +0 -1
  32. package/fesm2022/ng-forge-dynamic-forms-container-utils-BS4d0cCk.mjs +0 -48
  33. package/fesm2022/ng-forge-dynamic-forms-container-utils-BS4d0cCk.mjs.map +0 -1
  34. package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-CnJgFWLB.mjs.map +0 -1
  35. package/fesm2022/ng-forge-dynamic-forms-emit-initialization-3PyXVwsn.mjs +0 -29
  36. package/fesm2022/ng-forge-dynamic-forms-emit-initialization-3PyXVwsn.mjs.map +0 -1
  37. package/fesm2022/ng-forge-dynamic-forms-group-field.component-BJ8j7Sh0.mjs.map +0 -1
  38. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms--PuL0AKc.mjs +0 -18773
  39. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms--PuL0AKc.mjs.map +0 -1
  40. package/fesm2022/ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs.map +0 -1
  41. package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs.map +0 -1
@@ -9,47 +9,18 @@ import { explicitEffect } from 'ngxtension/explicit-effect';
9
9
  import { TestBed } from '@angular/core/testing';
10
10
  import { form, schema } from '@angular/forms/signals';
11
11
 
12
- /**
13
- * Creates a value field context from the injected FIELD_SIGNAL_CONTEXT.
14
- * Must be called within an injection context.
15
- *
16
- * With direct root form binding for array items, the FIELD_SIGNAL_CONTEXT.form
17
- * uses a getter that evaluates reactively. This means:
18
- * - For regular fields: direct access to the form's FieldTree
19
- * - For array items: the getter resolves rootForm['arrayKey'][index] dynamically
20
- *
21
- * The returned context provides a getFieldTree function that should be called
22
- * inside a computed to establish proper reactive dependencies.
23
- *
24
- * @returns The value field context with reactive field tree accessor
25
- */
26
12
  function resolveValueFieldContext() {
27
13
  const context = injectFieldSignalContext();
28
14
  return {
29
15
  getFieldTree: (fieldKey) => {
30
- // Access context.form here (inside a function that will be called from a computed)
31
- // This allows the getter to be evaluated reactively
32
- const formRoot = context.form;
33
- // Handle case where form is undefined (e.g., newly added array items before root form updates)
34
- if (!formRoot) {
16
+ const formRoot = context.form;
17
+ if (!formRoot) {
35
18
  return undefined;
36
19
  }
37
20
  return formRoot[fieldKey];
38
21
  },
39
22
  };
40
23
  }
41
- /**
42
- * Builds the base inputs for a value field.
43
- * This is a helper function used by valueFieldMapper and specialized mappers.
44
- *
45
- * Note: This function should be called inside a computed signal to ensure
46
- * proper reactive dependency tracking for the field tree resolution.
47
- *
48
- * @param fieldDef The value field definition
49
- * @param ctx The resolved value field context
50
- * @param defaultProps Default props from the form configuration (may be undefined if not configured)
51
- * @returns Record of input names to values
52
- */
53
24
  function buildValueFieldInputs(fieldDef, ctx, defaultProps) {
54
25
  const omittedFields = omit(fieldDef, ['value']);
55
26
  const baseInputs = buildBaseInputs(omittedFields, defaultProps);
@@ -60,56 +31,20 @@ function buildValueFieldInputs(fieldDef, ctx, defaultProps) {
60
31
  if (fieldDef.placeholder !== undefined) {
61
32
  inputs['placeholder'] = fieldDef.placeholder;
62
33
  }
63
- // Resolve field tree reactively - this call is inside a computed,
64
- // so it establishes the reactive dependency correctly
65
- const fieldTree = ctx.getFieldTree(fieldDef.key);
34
+ const fieldTree = ctx.getFieldTree(fieldDef.key);
66
35
  if (fieldTree !== undefined) {
67
36
  inputs['field'] = fieldTree;
68
37
  }
69
38
  return inputs;
70
39
  }
71
- /**
72
- * Maps a value field definition to component inputs.
73
- *
74
- * Value fields are input fields that contribute to the form's value (text, number, etc.).
75
- * This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field proxy.
76
- *
77
- * For array items, the FIELD_SIGNAL_CONTEXT.form uses a reactive getter that resolves
78
- * rootForm['arrayKey'][index] dynamically. This means:
79
- * - Zod/StandardSchema validation errors are automatically available
80
- * - The field tree updates reactively when the root form structure changes
81
- * - Newly added array items will get their field tree once the form value updates
82
- *
83
- * For fields with specific properties, use the specialized mappers:
84
- * - optionsFieldMapper: for fields with options (select, radio, multi-checkbox)
85
- * - datepickerFieldMapper: for fields with minDate, maxDate, startAt
86
- * - checkboxFieldMapper: for boolean fields (checkbox, toggle)
87
- *
88
- * @param fieldDef The value field definition
89
- * @returns Signal containing Record of input names to values for ngComponentOutlet
90
- */
91
40
  function valueFieldMapper(fieldDef) {
92
- // Get the context once at injection time (captures the FIELD_SIGNAL_CONTEXT reference)
93
- const ctx = resolveValueFieldContext();
41
+ const ctx = resolveValueFieldContext();
94
42
  const defaultProps = inject(DEFAULT_PROPS);
95
- // The computed calls ctx.getFieldTree(fieldDef.key) inside, which:
96
- // 1. Accesses context.form (triggering the getter for array items)
97
- // 2. Returns the field tree if available, or undefined if not yet
98
- // 3. Re-runs when the form structure updates (reactive dependency)
99
- return computed(() => {
43
+ return computed(() => {
100
44
  return buildValueFieldInputs(fieldDef, ctx, defaultProps());
101
45
  });
102
46
  }
103
47
 
104
- /**
105
- * Maps a checkbox/toggle field definition to component inputs.
106
- *
107
- * Checkbox fields are checked fields that contribute to the form's value as boolean.
108
- * This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field tree.
109
- *
110
- * @param fieldDef The checkbox field definition
111
- * @returns Signal containing Record of input names to values for ngComponentOutlet
112
- */
113
48
  function checkboxFieldMapper(fieldDef) {
114
49
  const context = injectFieldSignalContext();
115
50
  const defaultProps = inject(DEFAULT_PROPS);
@@ -123,9 +58,7 @@ function checkboxFieldMapper(fieldDef) {
123
58
  if (fieldDef.placeholder !== undefined) {
124
59
  inputs['placeholder'] = fieldDef.placeholder;
125
60
  }
126
- // Access form inside computed for reactivity and to handle cases where
127
- // form may not be immediately available (e.g., during array item initialization)
128
- const formRoot = context.form;
61
+ const formRoot = context.form;
129
62
  const fieldTree = formRoot?.[fieldDef.key];
130
63
  if (fieldTree !== undefined) {
131
64
  inputs['field'] = fieldTree;
@@ -134,10 +67,6 @@ function checkboxFieldMapper(fieldDef) {
134
67
  });
135
68
  }
136
69
 
137
- /**
138
- * Converts a date value (string, Date, or null) to a Date object or null.
139
- * This ensures UI libraries that expect Date objects receive the correct type.
140
- */
141
70
  function toDate(value) {
142
71
  if (value === null || value === undefined) {
143
72
  return null;
@@ -145,31 +74,15 @@ function toDate(value) {
145
74
  if (value instanceof Date) {
146
75
  return value;
147
76
  }
148
- // Parse string to Date
149
- const date = new Date(value);
77
+ const date = new Date(value);
150
78
  return isNaN(date.getTime()) ? null : date;
151
79
  }
152
- /**
153
- * Maps a datepicker field to component inputs.
154
- *
155
- * Extends the base value field mapper by adding datepicker-specific properties:
156
- * - minDate: Minimum selectable date
157
- * - maxDate: Maximum selectable date
158
- * - startAt: Initial date to display when opening the picker
159
- *
160
- * Date values are automatically converted from strings to Date objects
161
- * since UI libraries (Material, PrimeNG, etc.) expect Date objects.
162
- *
163
- * @param fieldDef The datepicker field definition
164
- * @returns Signal containing Record of input names to values for ngComponentOutlet
165
- */
166
80
  function datepickerFieldMapper(fieldDef) {
167
81
  const ctx = resolveValueFieldContext();
168
82
  const defaultProps = inject(DEFAULT_PROPS);
169
83
  return computed(() => {
170
84
  const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
171
- // Add datepicker-specific properties, converting strings to Date objects
172
- if (fieldDef.minDate !== undefined) {
85
+ if (fieldDef.minDate !== undefined) {
173
86
  inputs['minDate'] = toDate(fieldDef.minDate);
174
87
  }
175
88
  if (fieldDef.maxDate !== undefined) {
@@ -182,44 +95,22 @@ function datepickerFieldMapper(fieldDef) {
182
95
  });
183
96
  }
184
97
 
185
- /**
186
- * Maps a field with options (select, radio, multi-checkbox) to component inputs.
187
- *
188
- * Extends the base value field mapper by adding the options property.
189
- *
190
- * @param fieldDef The field definition with options
191
- * @returns Signal containing Record of input names to values for ngComponentOutlet
192
- */
193
98
  function optionsFieldMapper(fieldDef) {
194
99
  const ctx = resolveValueFieldContext();
195
100
  const defaultProps = inject(DEFAULT_PROPS);
196
101
  return computed(() => {
197
- // Cast to BaseValueField - safe because all FieldWithOptions types extend BaseValueField
198
- const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
199
- // Add options property
200
- inputs['options'] = fieldDef.options;
102
+ const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
103
+ inputs['options'] = fieldDef.options;
201
104
  return inputs;
202
105
  });
203
106
  }
204
107
 
205
- /**
206
- * Maps a slider field to component inputs.
207
- *
208
- * Extends the base value field mapper by adding slider-specific properties:
209
- * - minValue: Minimum slider value
210
- * - maxValue: Maximum slider value
211
- * - step: Step increment for the slider
212
- *
213
- * @param fieldDef The slider field definition
214
- * @returns Signal containing Record of input names to values for ngComponentOutlet
215
- */
216
108
  function sliderFieldMapper(fieldDef) {
217
109
  const ctx = resolveValueFieldContext();
218
110
  const defaultProps = inject(DEFAULT_PROPS);
219
111
  return computed(() => {
220
112
  const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
221
- // Add slider-specific properties (these are at field level, not in props)
222
- if (fieldDef.minValue !== undefined) {
113
+ if (fieldDef.minValue !== undefined) {
223
114
  inputs['minValue'] = fieldDef.minValue;
224
115
  }
225
116
  if (fieldDef.maxValue !== undefined) {
@@ -232,10 +123,6 @@ function sliderFieldMapper(fieldDef) {
232
123
  });
233
124
  }
234
125
 
235
- /**
236
- * Applies hidden and disabled logic to a non-form-bound field.
237
- * Must be called inside a computed() context (reads form signals).
238
- */
239
126
  function applyNonFieldLogic(rootFormRegistry, fieldDef) {
240
127
  const result = {};
241
128
  const rootForm = rootFormRegistry.rootForm();
@@ -248,10 +135,8 @@ function applyNonFieldLogic(rootFormRegistry, fieldDef) {
248
135
  });
249
136
  result.hidden = hiddenSignal();
250
137
  }
251
- // Resolve disabled state if explicitly set or has logic
252
- if (fieldDef.disabled !== undefined || fieldDef.logic?.some((l) => l.type === 'disabled')) {
253
- // Create the signal and READ IT to establish reactive dependencies
254
- const disabledSignal = resolveNonFieldDisabled({
138
+ if (fieldDef.disabled !== undefined || fieldDef.logic?.some((l) => l.type === 'disabled')) {
139
+ const disabledSignal = resolveNonFieldDisabled({
255
140
  form: rootForm,
256
141
  fieldLogic: fieldDef.logic,
257
142
  explicitValue: fieldDef.disabled,
@@ -260,8 +145,7 @@ function applyNonFieldLogic(rootFormRegistry, fieldDef) {
260
145
  }
261
146
  }
262
147
  else {
263
- // Fallback to static values when rootForm is not available
264
- if (fieldDef.hidden !== undefined) {
148
+ if (fieldDef.hidden !== undefined) {
265
149
  result.hidden = fieldDef.hidden;
266
150
  }
267
151
  if (fieldDef.disabled !== undefined) {
@@ -270,19 +154,6 @@ function applyNonFieldLogic(rootFormRegistry, fieldDef) {
270
154
  }
271
155
  return result;
272
156
  }
273
- /**
274
- * Applies hidden logic to a non-form-bound field.
275
- *
276
- * This is a focused utility for fields that only need hidden state resolution.
277
- * Use this when you need hidden logic but handle disabled separately
278
- * (e.g., navigation buttons with custom disabled logic).
279
- *
280
- * NOTE: This function must be called inside a computed() context.
281
- *
282
- * @param rootForm The root form FieldTree (can be undefined)
283
- * @param fieldDef The field definition with optional hidden/logic
284
- * @returns The resolved hidden boolean value, or undefined if no hidden logic
285
- */
286
157
  function resolveHiddenValue(rootForm, fieldDef) {
287
158
  if (!rootForm) {
288
159
  return fieldDef.hidden;
@@ -292,71 +163,36 @@ function resolveHiddenValue(rootForm, fieldDef) {
292
163
  form: rootForm,
293
164
  fieldLogic: fieldDef.logic,
294
165
  explicitValue: fieldDef.hidden,
295
- // formValue omitted intentionally - resolver will read form.value() reactively
296
- })();
166
+ })();
297
167
  }
298
168
  return undefined;
299
169
  }
300
170
 
301
- /**
302
- * Maps a button field to component inputs.
303
- *
304
- * Unlike value field mappers, button fields do not participate in form values
305
- * and do not need field tree resolution or validation messages.
306
- *
307
- * Button-specific properties:
308
- * - event: The FormEvent constructor to emit when clicked
309
- * - eventArgs: Optional arguments to pass to the event constructor
310
- *
311
- * Hidden and disabled states are resolved using non-field logic resolvers which consider:
312
- * 1. Explicit `hidden: true` / `disabled: true` on the field definition
313
- * 2. Field-level `logic` array with `type: 'hidden'` or `type: 'disabled'` conditions
314
- *
315
- * @param fieldDef The button field definition
316
- * @returns Signal containing Record of input names to values for ngComponentOutlet
317
- */
318
171
  function buttonFieldMapper(fieldDef) {
319
172
  const defaultProps = inject(DEFAULT_PROPS);
320
173
  const rootFormRegistry = inject(RootFormRegistryService);
321
174
  return computed(() => {
322
175
  const inputs = buildBaseInputs(fieldDef, defaultProps());
323
- // Add button-specific properties
324
- inputs['event'] = fieldDef.event;
176
+ inputs['event'] = fieldDef.event;
325
177
  if (fieldDef.eventArgs !== undefined) {
326
178
  inputs['eventArgs'] = fieldDef.eventArgs;
327
179
  }
328
- // Apply hidden/disabled logic
329
- return {
180
+ return {
330
181
  ...inputs,
331
182
  ...applyNonFieldLogic(rootFormRegistry, fieldDef),
332
183
  };
333
184
  });
334
185
  }
335
186
 
336
- /**
337
- * Resolves array button context for button mappers.
338
- *
339
- * Handles both inside-array (context from ARRAY_CONTEXT) and
340
- * outside-array (explicit arrayKey) placements.
341
- *
342
- * Must be called in injection context.
343
- *
344
- * @param fieldKey The button field key (for logging)
345
- * @param buttonType The button type name (for logging)
346
- * @param explicitArrayKey Optional explicit arrayKey from field definition
347
- * @returns The array button context with reactive index signal
348
- */
349
187
  function resolveArrayButtonContext(fieldKey, buttonType, explicitArrayKey) {
350
188
  const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
351
189
  const logger = inject(DynamicFormLogger);
352
- // Priority: explicit arrayKey from fieldDef > arrayKey from context
353
- const targetArrayKey = explicitArrayKey ?? arrayContext?.arrayKey;
190
+ const targetArrayKey = explicitArrayKey ?? arrayContext?.arrayKey;
354
191
  if (!targetArrayKey) {
355
192
  logger.warn(`${buttonType} button "${fieldKey}" has no array context. ` +
356
193
  'Either place it inside an array field, or provide an explicit arrayKey property.');
357
194
  }
358
- // Create index signal once at injection time - computed to track arrayContext.index changes
359
- const indexSignal = computed(() => {
195
+ const indexSignal = computed(() => {
360
196
  if (!arrayContext)
361
197
  return -1;
362
198
  return arrayContext.index();
@@ -368,21 +204,10 @@ function resolveArrayButtonContext(fieldKey, buttonType, explicitArrayKey) {
368
204
  isInsideArray: arrayContext !== null,
369
205
  };
370
206
  }
371
- /**
372
- * Builds the event context passed to the button component.
373
- *
374
- * Should be called inside a computed to track index signal changes.
375
- *
376
- * @param fieldKey The button field key
377
- * @param ctx The resolved array button context
378
- * @param template Optional template for add operations (single field or array of fields)
379
- * @returns The event context for the button component
380
- */
381
207
  function buildArrayButtonEventContext(fieldKey, ctx, template) {
382
208
  const eventContext = {
383
209
  key: fieldKey,
384
- index: ctx.indexSignal(), // Evaluated inside computed
385
- arrayKey: ctx.targetArrayKey ?? '',
210
+ index: ctx.indexSignal(), arrayKey: ctx.targetArrayKey ?? '',
386
211
  formValue: ctx.formValue,
387
212
  };
388
213
  if (template) {
@@ -390,24 +215,10 @@ function buildArrayButtonEventContext(fieldKey, ctx, template) {
390
215
  }
391
216
  return eventContext;
392
217
  }
393
- /**
394
- * Resolves eventArgs with defaults for array buttons.
395
- *
396
- * @param fieldEventArgs The eventArgs from field definition (may be undefined)
397
- * @param defaultEventArgs The default eventArgs to use if not specified
398
- * @returns The resolved eventArgs array
399
- */
400
218
  function resolveArrayButtonEventArgs(fieldEventArgs, defaultEventArgs = ['$arrayKey', '$template']) {
401
219
  return fieldEventArgs ?? defaultEventArgs;
402
220
  }
403
221
 
404
- // =============================================================================
405
- // Add Array Item Mappers
406
- // =============================================================================
407
- /**
408
- * Mapper for add array item button — appends new item to end of array.
409
- * Template is REQUIRED to define the field structure for new items.
410
- */
411
222
  function addArrayItemButtonMapper(fieldDef) {
412
223
  const defaultProps = inject(DEFAULT_PROPS);
413
224
  const rootFormRegistry = inject(RootFormRegistryService);
@@ -423,10 +234,6 @@ function addArrayItemButtonMapper(fieldDef) {
423
234
  };
424
235
  });
425
236
  }
426
- /**
427
- * Mapper for prepend array item button — inserts new item at beginning of array.
428
- * Template is REQUIRED to define the field structure for new items.
429
- */
430
237
  function prependArrayItemButtonMapper(fieldDef) {
431
238
  const defaultProps = inject(DEFAULT_PROPS);
432
239
  const rootFormRegistry = inject(RootFormRegistryService);
@@ -442,10 +249,6 @@ function prependArrayItemButtonMapper(fieldDef) {
442
249
  };
443
250
  });
444
251
  }
445
- /**
446
- * Mapper for insert array item button — inserts new item at specific index.
447
- * Template and index are REQUIRED.
448
- */
449
252
  function insertArrayItemButtonMapper(fieldDef) {
450
253
  const defaultProps = inject(DEFAULT_PROPS);
451
254
  const rootFormRegistry = inject(RootFormRegistryService);
@@ -461,21 +264,11 @@ function insertArrayItemButtonMapper(fieldDef) {
461
264
  };
462
265
  });
463
266
  }
464
- // =============================================================================
465
- // Remove Array Item Mappers
466
- // =============================================================================
467
- /**
468
- * Mapper for remove array item button.
469
- * Inside array: RemoveAtIndexEvent. Outside array: PopArrayItemEvent.
470
- */
471
267
  function removeArrayItemButtonMapper(fieldDef) {
472
268
  const defaultProps = inject(DEFAULT_PROPS);
473
269
  const rootFormRegistry = inject(RootFormRegistryService);
474
270
  const ctx = resolveArrayButtonContext(fieldDef.key, 'removeArrayItem', fieldDef.arrayKey);
475
- // Choose event type based on context:
476
- // - Inside array: RemoveAtIndexEvent (remove specific item)
477
- // - Outside array: PopArrayItemEvent (remove last item)
478
- const eventType = ctx.isInsideArray ? RemoveAtIndexEvent : PopArrayItemEvent;
271
+ const eventType = ctx.isInsideArray ? RemoveAtIndexEvent : PopArrayItemEvent;
479
272
  const defaultEventArgs = ctx.isInsideArray ? ['$arrayKey', '$index'] : ['$arrayKey'];
480
273
  return computed(() => {
481
274
  const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
@@ -488,7 +281,6 @@ function removeArrayItemButtonMapper(fieldDef) {
488
281
  };
489
282
  });
490
283
  }
491
- /** Mapper for pop array item button — removes the last item from array. */
492
284
  function popArrayItemButtonMapper(fieldDef) {
493
285
  const defaultProps = inject(DEFAULT_PROPS);
494
286
  const rootFormRegistry = inject(RootFormRegistryService);
@@ -504,7 +296,6 @@ function popArrayItemButtonMapper(fieldDef) {
504
296
  };
505
297
  });
506
298
  }
507
- /** Mapper for shift array item button — removes the first item from array. */
508
299
  function shiftArrayItemButtonMapper(fieldDef) {
509
300
  const defaultProps = inject(DEFAULT_PROPS);
510
301
  const rootFormRegistry = inject(RootFormRegistryService);
@@ -521,13 +312,6 @@ function shiftArrayItemButtonMapper(fieldDef) {
521
312
  });
522
313
  }
523
314
 
524
- // =============================================================================
525
- // Submit Button Mapper
526
- // =============================================================================
527
- /**
528
- * Mapper for submit button — configures native form submission via type="submit".
529
- * Triggers native form submission rather than dispatching events directly.
530
- */
531
315
  function submitButtonFieldMapper(fieldDef) {
532
316
  const rootFormRegistry = inject(RootFormRegistryService);
533
317
  const defaultProps = inject(DEFAULT_PROPS);
@@ -536,8 +320,7 @@ function submitButtonFieldMapper(fieldDef) {
536
320
  return computed(() => {
537
321
  const baseInputs = buildBaseInputs(fieldDef, defaultProps());
538
322
  const rootForm = rootFormRegistry.rootForm();
539
- // Use rootForm (not fieldSignalContext.form) — submit needs root form validity (#157)
540
- const disabled = resolveSubmitButtonDisabled({
323
+ const disabled = resolveSubmitButtonDisabled({
541
324
  form: rootForm,
542
325
  formOptions: formOptions(),
543
326
  fieldLogic: fieldWithLogic.logic,
@@ -545,8 +328,7 @@ function submitButtonFieldMapper(fieldDef) {
545
328
  })();
546
329
  const inputs = {
547
330
  ...baseInputs,
548
- // type="submit" triggers native form submission (no event dispatch needed)
549
- props: { ...fieldDef.props, type: 'submit' },
331
+ props: { ...fieldDef.props, type: 'submit' },
550
332
  disabled,
551
333
  };
552
334
  const hidden = resolveHiddenValue(rootForm, fieldWithLogic);
@@ -556,10 +338,6 @@ function submitButtonFieldMapper(fieldDef) {
556
338
  return inputs;
557
339
  });
558
340
  }
559
- // =============================================================================
560
- // Next Page Button Mapper
561
- // =============================================================================
562
- /** Mapper for next page button — preconfigures NextPageEvent. */
563
341
  function nextButtonFieldMapper(fieldDef) {
564
342
  const fieldSignalContext = injectFieldSignalContext();
565
343
  const rootFormRegistry = inject(RootFormRegistryService);
@@ -587,10 +365,6 @@ function nextButtonFieldMapper(fieldDef) {
587
365
  return inputs;
588
366
  });
589
367
  }
590
- // =============================================================================
591
- // Previous Page Button Mapper
592
- // =============================================================================
593
- /** Mapper for previous page button — preconfigures PreviousPageEvent. */
594
368
  function previousButtonFieldMapper(fieldDef) {
595
369
  const defaultProps = inject(DEFAULT_PROPS);
596
370
  const rootFormRegistry = inject(RootFormRegistryService);
@@ -602,8 +376,7 @@ function previousButtonFieldMapper(fieldDef) {
602
376
  ...baseInputs,
603
377
  event: PreviousPageEvent,
604
378
  };
605
- // Add disabled binding only if explicitly set by user
606
- if (fieldDef.disabled !== undefined) {
379
+ if (fieldDef.disabled !== undefined) {
607
380
  inputs['disabled'] = fieldDef.disabled;
608
381
  }
609
382
  const hidden = resolveHiddenValue(rootForm, fieldWithLogic);
@@ -614,105 +387,48 @@ function previousButtonFieldMapper(fieldDef) {
614
387
  });
615
388
  }
616
389
 
617
- // Button field mapper (generic)
618
390
 
619
- // UI-specific field mappers
620
391
 
621
- /**
622
- * Factory function that creates a signal of resolved validation errors
623
- * Handles async resolution of DynamicText validation messages
624
- *
625
- * @param field - Signal containing FieldTree
626
- * @param validationMessages - Signal containing custom field-level validation messages
627
- * @param defaultValidationMessages - Signal containing default validation messages (fallback)
628
- * @param injector - Optional injector for DynamicText resolution
629
- * @returns Signal<ResolvedError[]> - Reactively resolved error messages
630
- *
631
- * @example
632
- * ```typescript
633
- * readonly resolvedErrors = createResolvedErrorsSignal(
634
- * this.field,
635
- * this.validationMessages,
636
- * this.defaultValidationMessages
637
- * );
638
- * ```
639
- */
640
392
  function createResolvedErrorsSignal(field, validationMessages, defaultValidationMessages = signal(undefined), injector = inject(Injector)) {
641
- // Get logger from injector
642
- const logger = injector.get(DynamicFormLogger);
643
- // Ensure validationMessages is never undefined (mappers pass {} if not defined)
644
- const messages = computed(() => validationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
393
+ const logger = injector.get(DynamicFormLogger);
394
+ const messages = computed(() => validationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
645
395
  const defaultMessages = computed(() => defaultValidationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "defaultMessages" }] : /* istanbul ignore next */ []));
646
- // Create a computed signal that reads the actual errors from the field
647
- // This ensures the signal tracks changes to field().errors(), not just the field reference
648
- const errors = computed(() => {
396
+ const errors = computed(() => {
649
397
  const control = field()();
650
398
  return control.errors();
651
399
  }, ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
652
- // Convert signals to observables using toObservable with injector
653
- const errors$ = toObservable(errors, { injector });
400
+ const errors$ = toObservable(errors, { injector });
654
401
  const messages$ = toObservable(messages, { injector });
655
402
  const defaultMessages$ = toObservable(defaultMessages, { injector });
656
- // Combine observables and process errors
657
- const resolvedErrors$ = combineLatest([errors$, messages$, defaultMessages$]).pipe(switchMap(([currentErrors, msgs, defaultMsgs]) => {
658
- // No errors - return empty array
659
- if (!currentErrors || currentErrors.length === 0) {
403
+ const resolvedErrors$ = combineLatest([errors$, messages$, defaultMessages$]).pipe(switchMap(([currentErrors, msgs, defaultMsgs]) => {
404
+ if (!currentErrors || currentErrors.length === 0) {
660
405
  return of([]);
661
406
  }
662
- // Create observable for each error's resolved message
663
- const errorResolvers = currentErrors.map((error) => resolveErrorMessage(error, msgs, defaultMsgs, injector, logger));
664
- // Combine all error message observables into single array emission, filtering out nulls
665
- return errorResolvers.length > 0
407
+ const errorResolvers = currentErrors.map((error) => resolveErrorMessage(error, msgs, defaultMsgs, injector, logger));
408
+ return errorResolvers.length > 0
666
409
  ? combineLatest(errorResolvers).pipe(map((errors) => errors.filter((e) => e !== null)))
667
410
  : of([]);
668
411
  }));
669
- // Convert observable back to signal using toSignal with injector
670
- // toSignal properly handles the injection context and manages subscriptions
671
- return toSignal(resolvedErrors$, { initialValue: [], injector });
412
+ return toSignal(resolvedErrors$, { initialValue: [], injector });
672
413
  }
673
- /**
674
- * Resolves a single error message from DynamicText sources with fallback logic
675
- * Priority: field-level message → default message → error.message → no message (logs warning)
676
- *
677
- * The error.message fallback enables schema validation libraries (Zod, Valibot, etc.)
678
- * to provide their own messages without requiring explicit validationMessages configuration.
679
- * Users who want translations can still override by configuring validationMessages.
680
- *
681
- * @internal
682
- */
683
414
  function resolveErrorMessage(error, fieldMessages, defaultMessages, injector, logger) {
684
- // Check for field-level custom message first
685
- const fieldMessage = fieldMessages[error.kind];
686
- // Fall back to default message if no field-level message exists
687
- const defaultMessage = defaultMessages[error.kind];
688
- // Determine which message to use: field-level → default → error.message (from schema validators)
689
- const messageToUse = fieldMessage ?? defaultMessage ?? error.message;
690
- // If no message found, log warning and return null (will be filtered out)
691
- if (!messageToUse) {
415
+ const fieldMessage = fieldMessages[error.kind];
416
+ const defaultMessage = defaultMessages[error.kind];
417
+ const messageToUse = fieldMessage ?? defaultMessage ?? error.message;
418
+ if (!messageToUse) {
692
419
  logger.warn(`No validation message found for error kind "${error.kind}". ` +
693
420
  `Please provide a message via field-level validationMessages or form-level defaultValidationMessages.`);
694
421
  return of(null);
695
422
  }
696
- // If using error.message directly (from schema validators), wrap in of() instead of dynamicTextToObservable
697
- // error.message is always a plain string, not DynamicText
698
- const isSchemaMessage = fieldMessage === undefined && defaultMessage === undefined;
423
+ const isSchemaMessage = fieldMessage === undefined && defaultMessage === undefined;
699
424
  const messageObservable = isSchemaMessage
700
- ? of(messageToUse) // error.message is always string
701
- : dynamicTextToObservable(messageToUse, injector);
702
- // Apply parameter interpolation to support {{param}} syntax
703
- return messageObservable.pipe(map((msg) => ({
425
+ ? of(messageToUse) : dynamicTextToObservable(messageToUse, injector);
426
+ return messageObservable.pipe(map((msg) => ({
704
427
  kind: error.kind,
705
428
  message: interpolateParams(msg, error),
706
429
  })));
707
430
  }
708
431
 
709
- /**
710
- * Computed signal that determines if errors should be displayed.
711
- * Based on field's invalid, touched, and error count.
712
- *
713
- * @param field - Signal containing FieldTree
714
- * @returns Signal<boolean> - True if errors should be displayed
715
- */
716
432
  function shouldShowErrors(field) {
717
433
  return computed(() => {
718
434
  const fieldTree = field();
@@ -721,13 +437,6 @@ function shouldShowErrors(field) {
721
437
  });
722
438
  }
723
439
 
724
- /**
725
- * Resolve target set, compute combined attrs, apply via applyMetaToElement.
726
- * Target resolution is cached by selector identity — re-resolves only on
727
- * selector change or `dependents` change. afterRenderEffect is signal-tracked,
728
- * so the callback re-runs only when computeAttrs / dependents differ.
729
- * @internal
730
- */
731
440
  function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
732
441
  const appliedByElement = new WeakMap();
733
442
  let cachedSelector;
@@ -737,8 +446,7 @@ function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
737
446
  const warnedSelectors = isDevMode() ? new Set() : undefined;
738
447
  afterRenderEffect({
739
448
  write: () => {
740
- // Read dependents first so afterRenderEffect tracks them.
741
- const currentDeps = options.dependents?.map((d) => d());
449
+ const currentDeps = options.dependents?.map((d) => d());
742
450
  const attrs = computeAttrs();
743
451
  const selector = (typeof options.selector === 'function' ? options.selector() : options.selector)?.trim() || undefined;
744
452
  const selectorChanged = selector !== cachedSelector;
@@ -749,11 +457,7 @@ function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
749
457
  cachedTargets = selector ? Array.from(elementRef.nativeElement.querySelectorAll(selector)) : [elementRef.nativeElement];
750
458
  }
751
459
  previousDeps = currentDeps;
752
- // Dev-mode safety net: selector typo or wrong descendant query leaves
753
- // meta + aria unapplied. Warn the first time we'd apply non-empty attrs
754
- // to zero matches for a given selector. Re-warns only when the selector
755
- // string changes (so authors fixing one typo see the next).
756
- if (warnedSelectors && selector && cachedTargets.length === 0 && attrs && Object.keys(attrs).length > 0) {
460
+ if (warnedSelectors && selector && cachedTargets.length === 0 && attrs && Object.keys(attrs).length > 0) {
757
461
  if (!warnedSelectors.has(selector)) {
758
462
  warnedSelectors.add(selector);
759
463
  const tag = elementRef.nativeElement.tagName.toLowerCase();
@@ -761,9 +465,7 @@ function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
761
465
  `Check the selector for typos, or supply 'dependents' if the target is rendered after a signal emits.`);
762
466
  }
763
467
  }
764
- // Bail when attrs unchanged AND nothing structural moved — keeps repeated
765
- // effect runs (driven by an unrelated tracked-signal flicker) cheap.
766
- if (!selectorChanged && !depsChanged && isEqual(attrs, previousAttrs)) {
468
+ if (!selectorChanged && !depsChanged && isEqual(attrs, previousAttrs)) {
767
469
  return;
768
470
  }
769
471
  previousAttrs = attrs;
@@ -774,58 +476,14 @@ function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
774
476
  },
775
477
  });
776
478
  }
777
- /**
778
- * Sets up automatic meta attribute tracking and application for wrapped components.
779
- *
780
- * This utility creates an `afterRenderEffect` that applies meta attributes to DOM elements
781
- * after Angular's render cycle. It efficiently tracks signal dependencies and only re-applies
782
- * when the meta signal or any dependent signals change.
783
- *
784
- * Use this instead of MutationObserver for better performance and integration with Angular's
785
- * change detection.
786
- *
787
- * @param elementRef - Reference to the host element
788
- * @param meta - Signal containing meta attributes to apply
789
- * @param options - Configuration options
790
- *
791
- * @example
792
- * ```typescript
793
- * // For a radio group with dynamic options
794
- * constructor() {
795
- * setupMetaTracking(inject(ElementRef), this.meta, {
796
- * selector: 'input[type="radio"]',
797
- * dependents: [this.options]
798
- * });
799
- * }
800
- *
801
- * // For a simple checkbox
802
- * constructor() {
803
- * setupMetaTracking(inject(ElementRef), this.meta, {
804
- * selector: 'input[type="checkbox"]'
805
- * });
806
- * }
807
- *
808
- * // For Shadow DOM components (apply to host)
809
- * constructor() {
810
- * setupMetaTracking(inject(ElementRef), this.meta);
811
- * }
812
- * ```
813
- */
814
479
  function setupMetaTracking(elementRef, meta, options) {
815
480
  applyTrackedAttrsToTargets(elementRef, () => meta(), { selector: options?.selector, dependents: options?.dependents });
816
481
  }
817
- /**
818
- * Marker-directive variant: writes parent meta + aria to resolved targets.
819
- * Parent aria wins on key collision. Internal to NgForgeControl/HostControl.
820
- * @internal
821
- */
822
482
  function setupMarkerAttrTracking(elementRef, parent, selector) {
823
483
  applyTrackedAttrsToTargets(elementRef, () => {
824
484
  const ariaRequired = parent.ariaRequired();
825
485
  const ariaDescribedBy = parent.ariaDescribedBy();
826
- // Skip null aria-required/describedby so applyMetaToElement removes the
827
- // attr (rather than serializing "null") on transitions.
828
- return {
486
+ return {
829
487
  ...(parent.meta() ?? {}),
830
488
  'aria-invalid': parent.ariaInvalid(),
831
489
  ...(ariaRequired === null ? {} : { 'aria-required': ariaRequired }),
@@ -834,17 +492,6 @@ function setupMarkerAttrTracking(elementRef, parent, selector) {
834
492
  }, { selector });
835
493
  }
836
494
 
837
- /**
838
- * Creates a signal that computes the aria-describedby value based on errors and hint state.
839
- * Errors take precedence over hints - when errors are displayed, the hint is hidden.
840
- * Only the first error is displayed (single error ID, not indexed).
841
- *
842
- * @param errorsToDisplay Signal containing the array of errors currently being displayed
843
- * @param errorId Signal containing the ID for the error element (single error only)
844
- * @param hintId Signal containing the ID for the hint element
845
- * @param hasHint Function that returns true if a hint is configured
846
- * @returns Signal containing the aria-describedby value or null
847
- */
848
495
  function createAriaDescribedBySignal(errorsToDisplay, errorId, hintId, hasHint) {
849
496
  return computed(() => {
850
497
  if (errorsToDisplay().length > 0) {
@@ -857,24 +504,7 @@ function createAriaDescribedBySignal(errorsToDisplay, errorId, hintId, hasHint)
857
504
  });
858
505
  }
859
506
 
860
- // Error display utilities for field components
861
507
 
862
- /**
863
- * Base directive composed into every ng-forge component via `hostDirectives`.
864
- * Owns the universal identity inputs (`key`, `className`) and the host
865
- * bindings that every field shape — value-bearing, action, future container —
866
- * shares: `[id]`, `[attr.data-testid]`, `[class]`.
867
- *
868
- * Shape-specific add-ons (`NgForgeField` for value fields, `NgForgeAction`
869
- * for actions) inject this directive and read `key()` / `className()` from
870
- * it. Source-of-truth concerns like `hidden` / `disabled` live on the add-on
871
- * because they vary by shape (value fields derive from `field()()`; actions
872
- * receive direct inputs).
873
- *
874
- * Selectorless — usage is exclusively via `hostDirectives`. Prefer the
875
- * `NG_FORGE_FIELD` / `NG_FORGE_ACTION` presets rather than composing Shell
876
- * directly.
877
- */
878
508
  class NgForgeFieldShell {
879
509
  key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
880
510
  className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
@@ -891,76 +521,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
891
521
  },
892
522
  }]
893
523
  }], propDecorators: { key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }] } });
894
- /** Input names forwarded onto `NgForgeFieldShell` via `hostDirectives`. */
895
524
  const NG_FORGE_FIELD_SHELL_INPUTS = ['key', 'className'];
896
525
 
897
- /**
898
- * Value-bearing add-on directive — composed alongside `NgForgeFieldShell` via
899
- * the `NG_FORGE_FIELD` preset. Owns the value/validation/aria plumbing every
900
- * value-bearing field needs.
901
- *
902
- * The Shell owns universal identity (`key`, `className`, `[id]`,
903
- * `[attr.data-testid]`, `[class]`); this directive injects Shell and adds
904
- * field-driven bindings (`[attr.hidden]`, `[attr.aria-disabled]`), the
905
- * standard value-field inputs (`field`, `label`, `placeholder`, `tabIndex`,
906
- * `props`, `meta`, `validationMessages`), the derived error/aria signals,
907
- * and the meta-tracking claim contract.
908
- *
909
- * @example
910
- * ```ts
911
- * \@Component({
912
- * hostDirectives: NG_FORGE_FIELD,
913
- * imports: [NgForgeControl, FormField],
914
- * template: `
915
- * <label [for]="ngf.key() + '-input'">{{ ngf.label() | dynamicText | async }}</label>
916
- * <input ngForgeControl [id]="ngf.key() + '-input'" [formField]="ngf.field()" />
917
- * @if (ngf.errorsToDisplay()[0]; as e) {
918
- * <span [id]="ngf.errorId()">{{ e.message }}</span>
919
- * }
920
- * `,
921
- * })
922
- * export class MyInputField {
923
- * protected readonly ngf = injectNgForgeField<string>();
924
- * }
925
- * ```
926
- *
927
- * Selectorless — usage is exclusively via `hostDirectives`. Prefer the
928
- * `NG_FORGE_FIELD` preset over composing this directive manually.
929
- */
930
526
  class NgForgeField {
931
- // ───────────────────────────────────────────────────────────────────────────
932
- // Shell-owned identity — re-exported so component templates keep using
933
- // `ngf.key()` / `ngf.className()` without reaching for the Shell directly.
934
- // ───────────────────────────────────────────────────────────────────────────
935
- shell = inject(NgForgeFieldShell);
936
- /** Shell-owned key, re-exported as plain `Signal<string>` so it doesn't get flagged by the host-directive input-lockstep assertion. */
937
- key = this.shell.key;
938
- /** Shell-owned className, re-exported as plain `Signal<string>` for the same reason. */
939
- className = this.shell.className;
940
- // ───────────────────────────────────────────────────────────────────────────
941
- // Forwarded inputs — the value-field-specific subset of mapper output.
942
- // ───────────────────────────────────────────────────────────────────────────
943
- field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
527
+ shell = inject(NgForgeFieldShell);
528
+ key = this.shell.key;
529
+ className = this.shell.className;
530
+ field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
944
531
  label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
945
532
  placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
946
533
  tabIndex = input(...(ngDevMode ? [undefined, { debugName: "tabIndex" }] : /* istanbul ignore next */ []));
947
534
  props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : /* istanbul ignore next */ []));
948
535
  meta = input(...(ngDevMode ? [undefined, { debugName: "meta" }] : /* istanbul ignore next */ []));
949
536
  validationMessages = input(...(ngDevMode ? [undefined, { debugName: "validationMessages" }] : /* istanbul ignore next */ []));
950
- // ───────────────────────────────────────────────────────────────────────────
951
- // Wiring
952
- // ───────────────────────────────────────────────────────────────────────────
953
- defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES, { optional: true }) ?? signal(undefined);
954
- // ───────────────────────────────────────────────────────────────────────────
955
- // Error display
956
- // ───────────────────────────────────────────────────────────────────────────
957
- errors = createResolvedErrorsSignal(this.field, this.validationMessages, this.defaultValidationMessages);
537
+ defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES, { optional: true }) ?? signal(undefined);
538
+ errors = createResolvedErrorsSignal(this.field, this.validationMessages, this.defaultValidationMessages);
958
539
  showErrors = shouldShowErrors(this.field);
959
540
  errorsToDisplay = computed(() => (this.showErrors() ? this.errors() : []), ...(ngDevMode ? [{ debugName: "errorsToDisplay" }] : /* istanbul ignore next */ []));
960
- // ───────────────────────────────────────────────────────────────────────────
961
- // Accessibility helpers
962
- // ───────────────────────────────────────────────────────────────────────────
963
- errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
541
+ errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
964
542
  hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
965
543
  ariaInvalid = computed(() => {
966
544
  const state = this.field()();
@@ -970,21 +548,13 @@ class NgForgeField {
970
548
  return this.field()().required?.() === true ? true : null;
971
549
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : /* istanbul ignore next */ []));
972
550
  ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => Boolean(this.props()?.hint));
973
- // ───────────────────────────────────────────────────────────────────────────
974
- // Claim tracking — markers and ambient sub-components flip this to true so
975
- // dev-mode can warn when meta() is non-empty but nothing consumed it.
976
- // ───────────────────────────────────────────────────────────────────────────
977
- _claimed = signal(false, ...(ngDevMode ? [{ debugName: "_claimed" }] : /* istanbul ignore next */ []));
978
- /** @internal Marker directives + ambient sub-components call this on construction. */
979
- markClaimed() {
551
+ _claimed = signal(false, ...(ngDevMode ? [{ debugName: "_claimed" }] : /* istanbul ignore next */ []));
552
+ markClaimed() {
980
553
  this._claimed.set(true);
981
554
  }
982
555
  logger = inject(DynamicFormLogger, { optional: true });
983
556
  constructor() {
984
- // Dev-mode safety net: a mapper or template-author binds meta() but no
985
- // NgForgeControl / NgForgeHostControl / ambient sub-component is in
986
- // place to consume it. Fires at most once per directive instance.
987
- if (isDevMode()) {
557
+ if (isDevMode()) {
988
558
  let warned = false;
989
559
  afterRenderEffect({
990
560
  write: () => {
@@ -996,8 +566,7 @@ class NgForgeField {
996
566
  if (untracked(this._claimed))
997
567
  return;
998
568
  warned = true;
999
- // `untracked` safe — afterRenderEffect.write runs after first CD, so Shell's required `key` is bound (no NG0950).
1000
- const key = untracked(this.key);
569
+ const key = untracked(this.key);
1001
570
  const message = `NgForgeField - meta() provided for field "${key}" but no NgForgeControl / NgForgeHostControl / ` +
1002
571
  `ambient sub-component claimed it. Meta will not be applied. ` +
1003
572
  `See https://ng-forge.dev/building-an-adapter#meta-forwarding`;
@@ -1023,70 +592,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1023
592
  },
1024
593
  }]
1025
594
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }] } });
1026
- /**
1027
- * Inputs forwarded onto `NgForgeField` via `hostDirectives`. Excludes
1028
- * `key` / `className` (forwarded to `NgForgeFieldShell` separately via
1029
- * `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this
1030
- * directly — use the `NG_FORGE_FIELD` preset.
1031
- */
1032
595
  const NG_FORGE_VALUE_FIELD_INPUTS = ['field', 'label', 'placeholder', 'tabIndex', 'props', 'meta', 'validationMessages'];
1033
- // Compile-time lockstep: NG_FORGE_VALUE_FIELD_INPUTS must equal the declared
1034
- // `input()` properties on NgForgeField. Drift in either direction fails the build.
1035
596
  const _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP = true;
1036
597
  void _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP;
1037
- /**
1038
- * Typed wrapper around `inject(NgForgeField)`. The generic narrows the
1039
- * `field` signal to `Signal<FieldTree<T>>` for the calling component's value
1040
- * type. The cast is unchecked — the runtime contract is that mappers only
1041
- * forward a `field` whose value type matches the field-type definition.
1042
- *
1043
- * @example
1044
- * ```ts
1045
- * protected readonly ngf = injectNgForgeField<string>();
1046
- * // ngf.field() is Signal<FieldTree<string>>; ngf.errors() etc. unchanged
1047
- * ```
1048
- */
1049
598
  function injectNgForgeField() {
1050
599
  return inject(NgForgeField);
1051
600
  }
1052
601
 
1053
- /**
1054
- * Action add-on directive — composed alongside `NgForgeFieldShell` via the
1055
- * `NG_FORGE_ACTION` preset. Owns the inputs + dispatch logic every button /
1056
- * action component needs (label, disabled, event, eventArgs, …) plus the
1057
- * `[attr.hidden]` / `[attr.aria-disabled]` host bindings driven by its own
1058
- * inputs (no field tree).
1059
- *
1060
- * Components call `action.dispatch()` from their click handler; this
1061
- * directive resolves any `eventArgs` tokens via the optional `ARRAY_CONTEXT`
1062
- * and dispatches the configured `event` through the `EventBus`.
1063
- *
1064
- * @example
1065
- * ```ts
1066
- * \@Component({
1067
- * hostDirectives: NG_FORGE_ACTION,
1068
- * template: `<button (click)="action.dispatch()" [disabled]="action.disabled()">{{ action.label() }}</button>`,
1069
- * })
1070
- * export class MyButton {
1071
- * protected readonly action = inject(NgForgeAction);
1072
- * }
1073
- * ```
1074
- *
1075
- * Selectorless — usage is exclusively via `hostDirectives`. Prefer the
1076
- * `NG_FORGE_ACTION` preset over composing this directive manually.
1077
- */
1078
602
  class NgForgeAction {
1079
- // ───────────────────────────────────────────────────────────────────────────
1080
- // Shell-owned identity — re-exported as plain `Signal<T>` so they don't get
1081
- // flagged by the lockstep assertion below.
1082
- // ───────────────────────────────────────────────────────────────────────────
1083
- shell = inject(NgForgeFieldShell);
603
+ shell = inject(NgForgeFieldShell);
1084
604
  key = this.shell.key;
1085
605
  className = this.shell.className;
1086
- // ───────────────────────────────────────────────────────────────────────────
1087
- // Forwarded inputs — the action-specific subset of mapper output.
1088
- // ───────────────────────────────────────────────────────────────────────────
1089
- label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
606
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1090
607
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1091
608
  hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
1092
609
  tabIndex = input(...(ngDevMode ? [undefined, { debugName: "tabIndex" }] : /* istanbul ignore next */ []));
@@ -1094,19 +611,9 @@ class NgForgeAction {
1094
611
  eventArgs = input(...(ngDevMode ? [undefined, { debugName: "eventArgs" }] : /* istanbul ignore next */ []));
1095
612
  eventContext = input(...(ngDevMode ? [undefined, { debugName: "eventContext" }] : /* istanbul ignore next */ []));
1096
613
  props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : /* istanbul ignore next */ []));
1097
- // ───────────────────────────────────────────────────────────────────────────
1098
- // Dispatch
1099
- // ───────────────────────────────────────────────────────────────────────────
1100
- eventBus = inject(EventBus);
614
+ eventBus = inject(EventBus);
1101
615
  arrayContext = inject(ARRAY_CONTEXT, { optional: true });
1102
- /**
1103
- * Dispatch the configured event through the EventBus. Resolves `eventArgs`
1104
- * tokens (`$key`, `$index`, `$arrayKey`, `formValue`, …) against the
1105
- * ambient array context if present, falling back to a static `eventContext`
1106
- * input or the field's own `key` otherwise. No-op when `event` is unset
1107
- * (e.g. submit buttons handled by the native form lifecycle).
1108
- */
1109
- dispatch() {
616
+ dispatch() {
1110
617
  const event = this.event();
1111
618
  if (!event)
1112
619
  return;
@@ -1137,47 +644,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1137
644
  },
1138
645
  }]
1139
646
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], event: [{ type: i0.Input, args: [{ isSignal: true, alias: "event", required: false }] }], eventArgs: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventArgs", required: false }] }], eventContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventContext", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }] } });
1140
- /**
1141
- * Inputs forwarded onto `NgForgeAction` via `hostDirectives`. Excludes
1142
- * `key` / `className` (forwarded to `NgForgeFieldShell` separately via
1143
- * `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this
1144
- * directly — use the `NG_FORGE_ACTION` preset.
1145
- */
1146
647
  const NG_FORGE_ACTION_INPUTS = ['label', 'disabled', 'hidden', 'tabIndex', 'event', 'eventArgs', 'eventContext', 'props'];
1147
- // Compile-time lockstep: NG_FORGE_ACTION_INPUTS must equal the declared
1148
- // `input()` properties on NgForgeAction. Drift fails the build.
1149
648
  const _NG_FORGE_ACTION_INPUTS_LOCKSTEP = true;
1150
649
  void _NG_FORGE_ACTION_INPUTS_LOCKSTEP;
1151
- /**
1152
- * Typed wrapper around `inject(NgForgeAction)`. The generic narrows the
1153
- * `event` input to a specific FormEvent subclass.
1154
- *
1155
- * @example
1156
- * ```ts
1157
- * protected readonly action = injectNgForgeAction<SubmitEvent>();
1158
- * ```
1159
- */
1160
650
  function injectNgForgeAction() {
1161
651
  return inject(NgForgeAction);
1162
652
  }
1163
653
 
1164
- /**
1165
- * Universal addon-array plumbing: signal-cached `hidden` resolution and
1166
- * slot bucketing. Used internally by the `NgForgeAddons` host wrapper —
1167
- * consumers should compose `NgForgeAddons` directly and inject the
1168
- * directive instance via `injectNgForgeAddons<TAddon>()`.
1169
- */
1170
654
  class NgForgeAddonsBase {
1171
- /** Capture the host's injector so `toSignal` (inside `resolveDynamicValue`) ties to the host's `DestroyRef`. */
1172
- hostInjector = inject(Injector);
1173
- /** Addon array mapped from `FieldDef.addons` by `buildBaseInputs`. */
1174
- addons = input(undefined, ...(ngDevMode ? [{ debugName: "addons" }] : /* istanbul ignore next */ []));
1175
- /**
1176
- * Per-addon `hidden` signal cache. Built via `explicitEffect` rather
1177
- * than `computed` because `toSignal` (inside `resolveDynamicValue` for
1178
- * Observable inputs) is disallowed in reactive contexts (NG0602).
1179
- */
1180
- _hiddenSignalCache = signal(new Map(), ...(ngDevMode ? [{ debugName: "_hiddenSignalCache" }] : /* istanbul ignore next */ []));
655
+ hostInjector = inject(Injector);
656
+ addons = input(undefined, ...(ngDevMode ? [{ debugName: "addons" }] : /* istanbul ignore next */ []));
657
+ _hiddenSignalCache = signal(new Map(), ...(ngDevMode ? [{ debugName: "_hiddenSignalCache" }] : /* istanbul ignore next */ []));
1181
658
  hiddenSignalCache = this._hiddenSignalCache.asReadonly();
1182
659
  visibleAddons = computed(() => {
1183
660
  const list = this.addons() ?? [];
@@ -1187,11 +664,7 @@ class NgForgeAddonsBase {
1187
664
  hasAddons = computed(() => this.visibleAddons().length > 0, ...(ngDevMode ? [{ debugName: "hasAddons" }] : /* istanbul ignore next */ []));
1188
665
  prefixAddons = computed(() => this.visibleAddons().filter((a) => a.slot === 'prefix'), ...(ngDevMode ? [{ debugName: "prefixAddons" }] : /* istanbul ignore next */ []));
1189
666
  suffixAddons = computed(() => this.visibleAddons().filter((a) => a.slot === 'suffix'), ...(ngDevMode ? [{ debugName: "suffixAddons" }] : /* istanbul ignore next */ []));
1190
- /**
1191
- * Visible addons grouped by their `slot` string. Convenience view for
1192
- * adapters with non-universal slots (e.g., Ionic `start` / `end`).
1193
- */
1194
- addonsBySlot = computed(() => {
667
+ addonsBySlot = computed(() => {
1195
668
  const map = new Map();
1196
669
  for (const a of this.visibleAddons()) {
1197
670
  const bucket = map.get(a.slot) ?? [];
@@ -1200,24 +673,15 @@ class NgForgeAddonsBase {
1200
673
  }
1201
674
  return map;
1202
675
  }, ...(ngDevMode ? [{ debugName: "addonsBySlot" }] : /* istanbul ignore next */ []));
1203
- /** Secondary index keyed by `hidden` value identity. Catches the common case
1204
- * where a FormConfig re-emit produces structurally-different-but-content-
1205
- * identical addon objects — without this, every re-emit would spawn a new
1206
- * `toSignal` subscription for each addon's hidden axis. */
1207
- _hiddenByValue = new WeakMap();
676
+ _hiddenByValue = new WeakMap();
1208
677
  constructor() {
1209
678
  explicitEffect([this.addons], ([list]) => {
1210
679
  const prev = this._hiddenSignalCache();
1211
680
  const next = new Map();
1212
681
  for (const a of list ?? []) {
1213
- // Tier 1: reference identity — the cheapest hit when the same addon
1214
- // object is reused across emissions.
1215
- let resolved = prev.get(a);
682
+ let resolved = prev.get(a);
1216
683
  if (!resolved) {
1217
- // Tier 2: hidden-value identity — when `hidden` is an object
1218
- // (Signal / Observable / function), the same instance can be passed
1219
- // through multiple new addon objects on FormConfig re-emit.
1220
- const h = a.hidden;
684
+ const h = a.hidden;
1221
685
  const valueKey = typeof h === 'object' && h !== null ? h : null;
1222
686
  resolved = valueKey ? this._hiddenByValue.get(valueKey) : undefined;
1223
687
  if (!resolved) {
@@ -1238,36 +702,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1238
702
  type: Directive,
1239
703
  args: [{}]
1240
704
  }], ctorParameters: () => [], propDecorators: { addons: [{ type: i0.Input, args: [{ isSignal: true, alias: "addons", required: false }] }] } });
1241
- /** Forwarded onto `NgForgeAddonsBase` via the `NgForgeAddons` wrapper. */
1242
705
  const NG_FORGE_ADDONS_INPUTS = ['addons'];
1243
- // Compile-time lockstep — drift between the tuple and the declared inputs fails the build.
1244
706
  const _NG_FORGE_ADDONS_INPUTS_LOCKSTEP = true;
1245
707
  void _NG_FORGE_ADDONS_INPUTS_LOCKSTEP;
1246
- /**
1247
- * Host wrapper for `NgForgeAddonsBase`. Consumers compose this directly:
1248
- *
1249
- * ```ts
1250
- * \@Component({
1251
- * hostDirectives: [NgForgeFieldHost, NgForgeAddons],
1252
- * template: `
1253
- * @if (ngfa.hasAddons()) {
1254
- * <p-inputgroup>
1255
- * @for (a of ngfa.prefixAddons(); track $index) {
1256
- * <df-addon-slot [addon]="a" [hidden]="ngfa.hiddenSignalCache().get(a)" />
1257
- * }
1258
- * <input ngForgeControl ... />
1259
- * </p-inputgroup>
1260
- * }
1261
- * `,
1262
- * })
1263
- * export class MyInputField {
1264
- * protected readonly ngfa = injectNgForgeAddons<MyInputAddon>();
1265
- * }
1266
- * ```
1267
- *
1268
- * The wrapper exists so `hostDirectives: [NgForgeAddons]` resolves
1269
- * cross-package — same constraint that drives `NgForgeFieldHost`.
1270
- */
1271
708
  class NgForgeAddons {
1272
709
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddons, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1273
710
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeAddons, isStandalone: true, hostDirectives: [{ directive: NgForgeAddonsBase, inputs: ["addons", "addons"] }], ngImport: i0 });
@@ -1278,49 +715,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1278
715
  hostDirectives: [{ directive: NgForgeAddonsBase, inputs: [...NG_FORGE_ADDONS_INPUTS] }],
1279
716
  }]
1280
717
  }] });
1281
- /**
1282
- * Typed inject helper. Always resolves to the underlying
1283
- * `NgForgeAddonsBase` instance — the wrapper class has no instance
1284
- * fields of its own.
1285
- */
1286
718
  function injectNgForgeAddons() {
1287
719
  return inject(NgForgeAddonsBase);
1288
720
  }
1289
721
 
1290
722
  const ADDON_PRESET_HANDLER = new InjectionToken('ADDON_PRESET_HANDLER');
1291
723
 
1292
- /**
1293
- * Universal click-dispatch plumbing for action-style addons (icon buttons,
1294
- * labelled buttons, etc.): `disabled` / `loading` resolution from
1295
- * `DynamicValue<boolean>`, action context construction, and the
1296
- * `preset / actionRef / action` precedence dispatcher.
1297
- *
1298
- * Adapter button kinds (e.g., `prime-button`) compose `NgForgeAddonAction`
1299
- * and read state via `injectNgForgeAddonAction<TAddon>()`. Preset semantics
1300
- * are adapter-specific — register an `ADDON_PRESET_HANDLER` at the host
1301
- * field scope to receive preset dispatches.
1302
- */
1303
724
  class NgForgeAddonActionBase {
1304
- // The action registry is form-scoped. `optional: true` matches the
1305
- // preset handler so addons rendered outside a form that called
1306
- // `provideAddonActions(...)` degrade gracefully instead of throwing —
1307
- // `actionRef` dispatches then log an actionable warning and no-op.
1308
- actionRegistry = inject(ADDON_ACTION_REGISTRY, { optional: true });
725
+ actionRegistry = inject(ADDON_ACTION_REGISTRY, { optional: true });
1309
726
  presetHandler = inject(ADDON_PRESET_HANDLER, { optional: true });
1310
727
  logger = inject(DynamicFormLogger);
1311
728
  hostInjector = inject(Injector);
1312
729
  addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
1313
730
  fieldInputs = input(undefined, ...(ngDevMode ? [{ debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
1314
- /**
1315
- * Per-value caches for `disabled` / `loading` signal resolution. Keyed by
1316
- * the `DynamicValue` identity so a single `toSignal` subscription is
1317
- * shared across addon-reference swaps that pass the same Observable.
1318
- * Mirrors `NgForgeAddonsBase._hiddenByValue` — `resolveDynamicValue`
1319
- * cannot run inside `computed`/`linkedSignal.computation` because
1320
- * `toSignal` throws in reactive contexts (NG0602), and re-running it on
1321
- * every recomputation would leak subscriptions on each cycle.
1322
- */
1323
- _disabledByValue = new WeakMap();
731
+ _disabledByValue = new WeakMap();
1324
732
  _loadingByValue = new WeakMap();
1325
733
  _resolvedDisabled = signal(undefined, ...(ngDevMode ? [{ debugName: "_resolvedDisabled" }] : /* istanbul ignore next */ []));
1326
734
  _resolvedLoading = signal(undefined, ...(ngDevMode ? [{ debugName: "_resolvedLoading" }] : /* istanbul ignore next */ []));
@@ -1342,13 +750,7 @@ class NgForgeAddonActionBase {
1342
750
  }
1343
751
  return resolved;
1344
752
  }
1345
- /**
1346
- * Resolve the click handler from the addon's three-axis click variant and
1347
- * invoke it. Precedence on multi-set: `preset > actionRef > action`. The
1348
- * validator drops multi-set configs at config-init time; this is
1349
- * defence-in-depth + a logged warning so the misconfiguration is loud.
1350
- */
1351
- dispatch() {
753
+ dispatch() {
1352
754
  const addon = this.addon();
1353
755
  const ctx = this.buildActionContext();
1354
756
  const variantsSet = [addon.preset, addon.actionRef, addon.action].filter((v) => v !== undefined).length;
@@ -1380,39 +782,14 @@ class NgForgeAddonActionBase {
1380
782
  addon.action(ctx);
1381
783
  return;
1382
784
  }
1383
- // Decorative — no handler configured.
1384
- }
1385
- /**
1386
- * Build an `AddonActionContext` for handlers from the wrapper-style
1387
- * `fieldInputs` bag. Returns the field-bound variant when a host field
1388
- * is reachable, or the orphan variant otherwise.
1389
- *
1390
- * Resolution order for `setValue`:
1391
- * 1. `inputs.setValue` from the forwarded bag (preferred — populated by
1392
- * `buildFieldInputs` in `DfFieldOutlet`).
1393
- * 2. Fallback reconstructed from `inputs.field` itself: although
1394
- * `ReadonlyFieldTree.value` is type-narrowed to `Signal<T>`, the
1395
- * underlying runtime object is the same `WritableSignal<T>` created
1396
- * by Signal Forms (the read-only wrapping is type-level only, see
1397
- * `toReadonlyFieldTree`). Casting back at the writer boundary
1398
- * produces a working setValue for ANY field depth — top-level OR
1399
- * nested under groups. This avoids the previous FSC-key navigation
1400
- * that broke for grouped fields, where `inputs.key` is DOM-mangled
1401
- * (`'user_address_street'`) and `fsc.form[key]` returns undefined.
1402
- *
1403
- * The orphan variant fires only when the bag carries no field tree at
1404
- * all — genuine "rendered outside a field" scenario.
1405
- */
1406
- buildActionContext() {
785
+ }
786
+ buildActionContext() {
1407
787
  const inputs = this.fieldInputs();
1408
788
  const key = inputs?.key ?? '';
1409
789
  const type = inputs?.type ?? '';
1410
790
  const tree = inputs?.field;
1411
791
  if (tree) {
1412
- // `writeToFieldValue` centralises the `Signal → WritableSignal` cast and
1413
- // warns at runtime if Signal Forms ever stops storing a writable signal
1414
- // here (the alternative — a silent no-op preset — is much worse).
1415
- const setValue = inputs?.setValue ?? ((next) => writeToFieldValue(tree.value, next, this.logger));
792
+ const setValue = inputs?.setValue ?? ((next) => writeToFieldValue(tree.value, next, this.logger));
1416
793
  return {
1417
794
  field: { key, type },
1418
795
  form: tree,
@@ -1433,26 +810,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1433
810
  type: Directive,
1434
811
  args: [{}]
1435
812
  }], ctorParameters: () => [], propDecorators: { addon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addon", required: true }] }], fieldInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldInputs", required: false }] }] } });
1436
- /** Forwarded onto `NgForgeAddonActionBase` via the `NgForgeAddonAction` wrapper. */
1437
813
  const NG_FORGE_ADDON_ACTION_INPUTS = ['addon', 'fieldInputs'];
1438
814
  const _NG_FORGE_ADDON_ACTION_INPUTS_LOCKSTEP = true;
1439
815
  void _NG_FORGE_ADDON_ACTION_INPUTS_LOCKSTEP;
1440
- /**
1441
- * Host wrapper for `NgForgeAddonActionBase`. Consumers compose this directly:
1442
- *
1443
- * ```ts
1444
- * \@Component({
1445
- * hostDirectives: [NgForgeAddonAction],
1446
- * template: `
1447
- * <p-button [loading]="action.loading()" [disabled]="action.disabled()"
1448
- * (onClick)="action.dispatch()" />
1449
- * `,
1450
- * })
1451
- * export class MyButtonAddon {
1452
- * protected readonly action = injectNgForgeAddonAction<MyButtonAddon>();
1453
- * }
1454
- * ```
1455
- */
1456
816
  class NgForgeAddonAction {
1457
817
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1458
818
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeAddonAction, isStandalone: true, hostDirectives: [{ directive: NgForgeAddonActionBase, inputs: ["addon", "addon", "fieldInputs", "fieldInputs"] }], ngImport: i0 });
@@ -1467,40 +827,6 @@ function injectNgForgeAddonAction() {
1467
827
  return inject(NgForgeAddonActionBase);
1468
828
  }
1469
829
 
1470
- /**
1471
- * Marker directive applied to the canonical control element of an
1472
- * `NgForgeField`-using component. Forwards the field's `meta` AND
1473
- * parent-derived aria signals (`aria-invalid`, `aria-required`,
1474
- * `aria-describedby`) onto a target element.
1475
- *
1476
- * By default attributes land on the directive's own host. For wrapped
1477
- * controls (e.g. `<mat-checkbox>`, `<p-toggleSwitch>`) where the canonical
1478
- * native input is rendered as a descendant inside the wrapper, pass a CSS
1479
- * selector through the input alias — the directive caches the resolved
1480
- * descendant set and re-resolves only when the selector changes.
1481
- *
1482
- * @example Bare native control
1483
- * ```html
1484
- * <input ngForgeControl [formField]="ngf.field()" />
1485
- * ```
1486
- *
1487
- * @example Wrapped control with inner native input
1488
- * ```html
1489
- * <mat-checkbox ngForgeControl="input[type='checkbox']" [formField]="ngf.field()">
1490
- * {{ ngf.label() }}
1491
- * </mat-checkbox>
1492
- * ```
1493
- *
1494
- * @example Dynamic option list (radios rendered in @for)
1495
- * ```html
1496
- * @for (option of options(); track option.value) {
1497
- * <input type="radio" ngForgeControl [value]="option.value" />
1498
- * }
1499
- * ```
1500
- *
1501
- * Each `@for` iteration creates its own directive instance; Angular's
1502
- * structural lifecycle handles add/remove naturally.
1503
- */
1504
830
  class NgForgeControl {
1505
831
  target = input(undefined, { ...(ngDevMode ? { debugName: "target" } : /* istanbul ignore next */ {}), alias: 'ngForgeControl' });
1506
832
  constructor() {
@@ -1515,31 +841,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1515
841
  type: Directive,
1516
842
  args: [{ selector: '[ngForgeControl]' }]
1517
843
  }], ctorParameters: () => [], propDecorators: { target: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngForgeControl", required: false }] }] } });
1518
- /**
1519
- * Selectorless companion directive for shadow-DOM-encapsulated components
1520
- * (PrimeNG `p-select`, Ionic web components, etc.) where no light-DOM child
1521
- * is the canonical control. Added to the component's `hostDirectives` so the
1522
- * field's `meta` + parent aria land on the component's own host element.
1523
- *
1524
- * For split parent/control components (e.g. `df-prime-select` rendering
1525
- * `df-prime-select-control` internally), place `NgForgeHostControl` on the
1526
- * INNER control component's `hostDirectives` — the inner element is the
1527
- * canonical control, and the marker's `inject(NgForgeField)` walks up the
1528
- * element-injector tree to find the parent's directive instance.
1529
- *
1530
- * @example
1531
- * ```ts
1532
- * \@Component({
1533
- * hostDirectives: [
1534
- * { directive: NgForgeField, inputs: [...NG_FORGE_FIELD_INPUTS] },
1535
- * NgForgeHostControl,
1536
- * ],
1537
- * template: `...`,
1538
- * })
1539
- * export class IonicInputField { ... }
1540
- * ```
1541
- */
1542
- // Empty `@Directive({})` is intentional — selectorless; used only via `hostDirectives`.
1543
844
  class NgForgeHostControl {
1544
845
  constructor() {
1545
846
  const parent = inject(NgForgeField);
@@ -1554,32 +855,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1554
855
  args: [{}]
1555
856
  }], ctorParameters: () => [] });
1556
857
 
1557
- /**
1558
- * Value-bearing field host wrapper. Composes `NgForgeFieldShell` (universal
1559
- * identity) with `NgForgeField` (value + validation + aria + meta-tracking)
1560
- * in a single host-directive entry, so consumer components compose just one
1561
- * directive instead of two.
1562
- *
1563
- * Why a directive instead of a `const` preset: cross-package partial
1564
- * compilation can't resolve a const reference inside `hostDirectives:`, so
1565
- * an exported const array doesn't work for library consumers. A wrapper
1566
- * directive's own `hostDirectives` IS a literal at compile time (in this
1567
- * file), so it works everywhere.
1568
- *
1569
- * Authoring shape for adapter components:
1570
- * ```ts
1571
- * \@Component({
1572
- * hostDirectives: [NgForgeFieldHost],
1573
- * ...
1574
- * })
1575
- * export class MyInputField {
1576
- * protected readonly ngf = injectNgForgeField<string>();
1577
- * }
1578
- * ```
1579
- *
1580
- * Pattern reference: Taiga UI uses the same wrapper-directive technique
1581
- * for the same reason. See https://www.angularspace.com/host-directives-decomposition-unleashed/
1582
- */
1583
858
  class NgForgeFieldHost {
1584
859
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeFieldHost, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1585
860
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeFieldHost, isStandalone: true, hostDirectives: [{ directive: NgForgeFieldShell, inputs: ["key", "key", "className", "className"] }, { directive: NgForgeField, inputs: ["field", "field", "label", "label", "placeholder", "placeholder", "tabIndex", "tabIndex", "props", "props", "meta", "meta", "validationMessages", "validationMessages"] }], ngImport: i0 });
@@ -1593,22 +868,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1593
868
  ],
1594
869
  }]
1595
870
  }] });
1596
- /**
1597
- * Action / button host wrapper. Composes `NgForgeFieldShell` with
1598
- * `NgForgeAction` (event dispatch + aria-disabled) for button-shaped
1599
- * components.
1600
- *
1601
- * Authoring shape:
1602
- * ```ts
1603
- * \@Component({
1604
- * hostDirectives: [NgForgeActionHost],
1605
- * ...
1606
- * })
1607
- * export class MyButton {
1608
- * protected readonly action = injectNgForgeAction();
1609
- * }
1610
- * ```
1611
- */
1612
871
  class NgForgeActionHost {
1613
872
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeActionHost, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1614
873
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeActionHost, isStandalone: true, hostDirectives: [{ directive: NgForgeFieldShell, inputs: ["key", "key", "className", "className"] }, { directive: NgForgeAction, inputs: ["label", "label", "disabled", "disabled", "hidden", "hidden", "tabIndex", "tabIndex", "event", "event", "eventArgs", "eventArgs", "eventContext", "eventContext", "props", "props"] }], ngImport: i0 });
@@ -1623,14 +882,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1623
882
  }]
1624
883
  }] });
1625
884
 
1626
- /**
1627
- * Builds a `ComponentFixture` for an `NgForgeField`-composing component with
1628
- * `field` + `key` bound BEFORE `detectChanges()` — sidesteps the NG0950 that
1629
- * Shell's required `key` and Field's required `field` would otherwise throw.
1630
- *
1631
- * The caller drives `detectChanges()` themselves (so they can `setInput`
1632
- * additional component-specific props in between).
1633
- */
1634
885
  function createNgForgeFieldFixture(component, options) {
1635
886
  TestBed.configureTestingModule({ imports: [component], providers: [...(options.providers ?? [])] });
1636
887
  const key = options.key;
@@ -1657,11 +908,6 @@ function createNgForgeFieldFixture(component, options) {
1657
908
  }
1658
909
  return { fixture, field, rootValue };
1659
910
  }
1660
- /**
1661
- * Builds a `ComponentFixture` for an `NgForgeAction`-composing component with
1662
- * `key` + `label` bound and `EventBus` provided. The caller calls
1663
- * `detectChanges()`; the harness handles the NG0950-avoidance bindings.
1664
- */
1665
911
  function createNgForgeActionFixture(component, options) {
1666
912
  TestBed.configureTestingModule({ imports: [component], providers: [EventBus, ...(options.providers ?? [])] });
1667
913
  const fixture = TestBed.createComponent(component);
@@ -1682,29 +928,11 @@ function createNgForgeActionFixture(component, options) {
1682
928
  }
1683
929
  return { fixture, eventBus: TestBed.inject(EventBus) };
1684
930
  }
1685
- /** Provider for `DEFAULT_VALIDATION_MESSAGES` in field tests — supply the default-messages map and the directive resolves it from DI. */
1686
931
  function provideTestValidationMessages(messages) {
1687
932
  return { provide: DEFAULT_VALIDATION_MESSAGES, useValue: signal(messages) };
1688
933
  }
1689
934
 
1690
- /**
1691
- * @ng-forge/dynamic-forms/integration
1692
- *
1693
- * Integration API for UI library authors building field implementations
1694
- * (Material, Bootstrap, PrimeNG, Ionic, etc.)
1695
- *
1696
- * This entrypoint provides:
1697
- * - Specific field type definitions (InputField, SelectField, etc.)
1698
- * - Field mappers for creating FieldTypeDefinition
1699
- * - Error display utilities for field components
1700
- */
1701
- // =============================================================================
1702
- // Field Mappers
1703
- // =============================================================================
1704
935
 
1705
- /**
1706
- * Generated bundle index. Do not edit.
1707
- */
1708
936
 
1709
937
  export { ADDON_PRESET_HANDLER, NG_FORGE_ACTION_INPUTS, NG_FORGE_ADDONS_INPUTS, NG_FORGE_ADDON_ACTION_INPUTS, NG_FORGE_FIELD_SHELL_INPUTS, NG_FORGE_VALUE_FIELD_INPUTS, NgForgeAction, NgForgeActionHost, NgForgeAddonAction, NgForgeAddonActionBase, NgForgeAddons, NgForgeAddonsBase, NgForgeControl, NgForgeField, NgForgeFieldHost, NgForgeFieldShell, NgForgeHostControl, addArrayItemButtonMapper, buildArrayButtonEventContext, buildValueFieldInputs, buttonFieldMapper, checkboxFieldMapper, createAriaDescribedBySignal, createNgForgeActionFixture, createNgForgeFieldFixture, createResolvedErrorsSignal, datepickerFieldMapper, injectNgForgeAction, injectNgForgeAddonAction, injectNgForgeAddons, injectNgForgeField, insertArrayItemButtonMapper, nextButtonFieldMapper, optionsFieldMapper, popArrayItemButtonMapper, prependArrayItemButtonMapper, previousButtonFieldMapper, provideTestValidationMessages, removeArrayItemButtonMapper, resolveArrayButtonContext, resolveArrayButtonEventArgs, resolveValueFieldContext, setupMetaTracking, shiftArrayItemButtonMapper, shouldShowErrors, submitButtonFieldMapper, valueFieldMapper };
1710
938
  //# sourceMappingURL=ng-forge-dynamic-forms-integration.mjs.map