@ng-forge/dynamic-forms 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,7 @@ Core library for building type-safe, dynamic Angular forms with signal forms int
18
18
 
19
19
  | @ng-forge/dynamic-forms | Angular |
20
20
  | ----------------------- | ------------- |
21
+ | 0.6.x | >=21.1.0 |
21
22
  | 0.5.x | >=21.1.0 |
22
23
  | 0.4.x | >=21.1.0 |
23
24
  | 0.3.x | 21.0.7-21.0.x |
@@ -89,19 +90,19 @@ This core library requires a UI integration. Choose one:
89
90
  | [@ng-forge/dynamic-forms-primeng](https://www.npmjs.com/package/@ng-forge/dynamic-forms-primeng) | PrimeNG |
90
91
  | [@ng-forge/dynamic-forms-ionic](https://www.npmjs.com/package/@ng-forge/dynamic-forms-ionic) | Ionic |
91
92
 
92
- Or [create your own](https://ng-forge.com/dynamic-forms/deep-dive/custom-integrations).
93
+ Or [create your own](https://ng-forge.com/dynamic-forms/advanced/custom-integrations).
93
94
 
94
95
  ## Documentation
95
96
 
96
97
  - [Installation](https://ng-forge.com/dynamic-forms/installation)
97
- - [Field Types](https://ng-forge.com/dynamic-forms/core/field-types)
98
- - [Validation](https://ng-forge.com/dynamic-forms/core/validation)
99
- - [Conditional Logic](https://ng-forge.com/dynamic-forms/core/conditional-logic)
100
- - [Value Derivation](https://ng-forge.com/dynamic-forms/core/derivation)
101
- - [Type Safety](https://ng-forge.com/dynamic-forms/core/type-safety)
102
- - [Events](https://ng-forge.com/dynamic-forms/core/events)
103
- - [i18n](https://ng-forge.com/dynamic-forms/core/i18n)
104
- - [Custom Integrations](https://ng-forge.com/dynamic-forms/deep-dive/custom-integrations)
98
+ - [Field Types](https://ng-forge.com/dynamic-forms/field-types)
99
+ - [Validation](https://ng-forge.com/dynamic-forms/validation/basics)
100
+ - [Conditional Logic](https://ng-forge.com/dynamic-forms/dynamic-behavior/overview)
101
+ - [Value Derivation](https://ng-forge.com/dynamic-forms/dynamic-behavior/derivation)
102
+ - [Type Safety](https://ng-forge.com/dynamic-forms/advanced/basics)
103
+ - [Events](https://ng-forge.com/dynamic-forms/advanced/events)
104
+ - [i18n](https://ng-forge.com/dynamic-forms/dynamic-behavior/i18n)
105
+ - [Custom Integrations](https://ng-forge.com/dynamic-forms/advanced/custom-integrations)
105
106
 
106
107
  ## Changelog
107
108
 
@@ -1,29 +1,46 @@
1
1
  import { inject, computed, signal, Injector, afterRenderEffect } from '@angular/core';
2
- import { FIELD_SIGNAL_CONTEXT, omit, buildBaseInputs, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, DynamicFormLogger, dynamicTextToObservable, interpolateParams, isEqual, applyMetaToElement } from '@ng-forge/dynamic-forms';
3
- export { isEqual } from '@ng-forge/dynamic-forms';
2
+ import { FIELD_SIGNAL_CONTEXT, omit, buildBaseInputs, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, resolveNonFieldHidden, resolveNonFieldDisabled, RootFormRegistryService, ARRAY_CONTEXT, DynamicFormLogger, AppendArrayItemEvent, PrependArrayItemEvent, InsertArrayItemEvent, RemoveAtIndexEvent, PopArrayItemEvent, ShiftArrayItemEvent, FORM_OPTIONS, resolveSubmitButtonDisabled, resolveNextButtonDisabled, NextPageEvent, PreviousPageEvent, dynamicTextToObservable, interpolateParams, isEqual, applyMetaToElement } from '@ng-forge/dynamic-forms';
3
+ export { isEqual, resolveNonFieldDisabled, resolveNonFieldHidden } from '@ng-forge/dynamic-forms';
4
4
  import { toObservable, toSignal } from '@angular/core/rxjs-interop';
5
5
  import { combineLatest, of } from 'rxjs';
6
6
  import { switchMap, map } from 'rxjs/operators';
7
7
 
8
8
  /**
9
- * Resolves the field tree from the form context.
9
+ * Creates a value field context from the injected FIELD_SIGNAL_CONTEXT.
10
10
  * Must be called within an injection context.
11
11
  *
12
- * Uses type-safe field tree utilities to access child FieldTrees from the parent form.
12
+ * With direct root form binding for array items, the FIELD_SIGNAL_CONTEXT.form
13
+ * uses a getter that evaluates reactively. This means:
14
+ * - For regular fields: direct access to the form's FieldTree
15
+ * - For array items: the getter resolves rootForm['arrayKey'][index] dynamically
13
16
  *
14
- * @param fieldKey The key of the field to resolve
15
- * @returns The resolved context with field tree
17
+ * The returned context provides a getFieldTree function that should be called
18
+ * inside a computed to establish proper reactive dependencies.
19
+ *
20
+ * @returns The value field context with reactive field tree accessor
16
21
  */
17
- function resolveValueFieldContext(fieldKey) {
22
+ function resolveValueFieldContext() {
18
23
  const context = inject(FIELD_SIGNAL_CONTEXT);
19
- const formRoot = context.form;
20
- const fieldTree = formRoot[fieldKey];
21
- return { fieldTree };
24
+ return {
25
+ getFieldTree: (fieldKey) => {
26
+ // Access context.form here (inside a function that will be called from a computed)
27
+ // This allows the getter to be evaluated reactively
28
+ const formRoot = context.form;
29
+ // Handle case where form is undefined (e.g., newly added array items before root form updates)
30
+ if (!formRoot) {
31
+ return undefined;
32
+ }
33
+ return formRoot[fieldKey];
34
+ },
35
+ };
22
36
  }
23
37
  /**
24
38
  * Builds the base inputs for a value field.
25
39
  * This is a helper function used by valueFieldMapper and specialized mappers.
26
40
  *
41
+ * Note: This function should be called inside a computed signal to ensure
42
+ * proper reactive dependency tracking for the field tree resolution.
43
+ *
27
44
  * @param fieldDef The value field definition
28
45
  * @param ctx The resolved value field context
29
46
  * @param defaultProps Default props from the form configuration (may be undefined if not configured)
@@ -43,8 +60,11 @@ function buildValueFieldInputs(fieldDef, ctx, defaultProps, defaultValidationMes
43
60
  if (defaultValidationMessages !== undefined) {
44
61
  inputs['defaultValidationMessages'] = defaultValidationMessages;
45
62
  }
46
- if (ctx.fieldTree !== undefined) {
47
- inputs['field'] = ctx.fieldTree;
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);
66
+ if (fieldTree !== undefined) {
67
+ inputs['field'] = fieldTree;
48
68
  }
49
69
  return inputs;
50
70
  }
@@ -54,6 +74,12 @@ function buildValueFieldInputs(fieldDef, ctx, defaultProps, defaultValidationMes
54
74
  * Value fields are input fields that contribute to the form's value (text, number, etc.).
55
75
  * This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field proxy.
56
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
+ *
57
83
  * For fields with specific properties (select, datepicker, textarea, slider), use the specialized mappers:
58
84
  * - selectFieldMapper: for fields with options (select, radio, multi-checkbox)
59
85
  * - datepickerFieldMapper: for fields with minDate, maxDate, startAt
@@ -64,10 +90,17 @@ function buildValueFieldInputs(fieldDef, ctx, defaultProps, defaultValidationMes
64
90
  * @returns Signal containing Record of input names to values for ngComponentOutlet
65
91
  */
66
92
  function valueFieldMapper(fieldDef) {
67
- const ctx = resolveValueFieldContext(fieldDef.key);
93
+ // Get the context once at injection time (captures the FIELD_SIGNAL_CONTEXT reference)
94
+ const ctx = resolveValueFieldContext();
68
95
  const defaultProps = inject(DEFAULT_PROPS);
69
96
  const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
70
- return computed(() => buildValueFieldInputs(fieldDef, ctx, defaultProps(), defaultValidationMessages()));
97
+ // The computed calls ctx.getFieldTree(fieldDef.key) inside, which:
98
+ // 1. Accesses context.form (triggering the getter for array items)
99
+ // 2. Returns the field tree if available, or undefined if not yet
100
+ // 3. Re-runs when the form structure updates (reactive dependency)
101
+ return computed(() => {
102
+ return buildValueFieldInputs(fieldDef, ctx, defaultProps(), defaultValidationMessages());
103
+ });
71
104
  }
72
105
 
73
106
  /**
@@ -83,8 +116,6 @@ function checkboxFieldMapper(fieldDef) {
83
116
  const context = inject(FIELD_SIGNAL_CONTEXT);
84
117
  const defaultProps = inject(DEFAULT_PROPS);
85
118
  const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
86
- const formRoot = context.form;
87
- const fieldTree = formRoot[fieldDef.key];
88
119
  return computed(() => {
89
120
  const omittedFields = omit(fieldDef, ['value']);
90
121
  const baseInputs = buildBaseInputs(omittedFields, defaultProps());
@@ -99,6 +130,10 @@ function checkboxFieldMapper(fieldDef) {
99
130
  if (validationMessages !== undefined) {
100
131
  inputs['defaultValidationMessages'] = validationMessages;
101
132
  }
133
+ // Access form inside computed for reactivity and to handle cases where
134
+ // form may not be immediately available (e.g., during array item initialization)
135
+ const formRoot = context.form;
136
+ const fieldTree = formRoot?.[fieldDef.key];
102
137
  if (fieldTree !== undefined) {
103
138
  inputs['field'] = fieldTree;
104
139
  }
@@ -136,7 +171,7 @@ function toDate(value) {
136
171
  * @returns Signal containing Record of input names to values for ngComponentOutlet
137
172
  */
138
173
  function datepickerFieldMapper(fieldDef) {
139
- const ctx = resolveValueFieldContext(fieldDef.key);
174
+ const ctx = resolveValueFieldContext();
140
175
  const defaultProps = inject(DEFAULT_PROPS);
141
176
  const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
142
177
  return computed(() => {
@@ -164,7 +199,7 @@ function datepickerFieldMapper(fieldDef) {
164
199
  * @returns Signal containing Record of input names to values for ngComponentOutlet
165
200
  */
166
201
  function optionsFieldMapper(fieldDef) {
167
- const ctx = resolveValueFieldContext(fieldDef.key);
202
+ const ctx = resolveValueFieldContext();
168
203
  const defaultProps = inject(DEFAULT_PROPS);
169
204
  const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
170
205
  return computed(() => {
@@ -188,7 +223,7 @@ function optionsFieldMapper(fieldDef) {
188
223
  * @returns Signal containing Record of input names to values for ngComponentOutlet
189
224
  */
190
225
  function textareaFieldMapper(fieldDef) {
191
- const ctx = resolveValueFieldContext(fieldDef.key);
226
+ const ctx = resolveValueFieldContext();
192
227
  const defaultProps = inject(DEFAULT_PROPS);
193
228
  const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
194
229
  return computed(() => {
@@ -223,7 +258,7 @@ function textareaFieldMapper(fieldDef) {
223
258
  * @returns Signal containing Record of input names to values for ngComponentOutlet
224
259
  */
225
260
  function sliderFieldMapper(fieldDef) {
226
- const ctx = resolveValueFieldContext(fieldDef.key);
261
+ const ctx = resolveValueFieldContext();
227
262
  const defaultProps = inject(DEFAULT_PROPS);
228
263
  const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
229
264
  return computed(() => {
@@ -242,6 +277,72 @@ function sliderFieldMapper(fieldDef) {
242
277
  });
243
278
  }
244
279
 
280
+ /**
281
+ * Applies hidden and disabled logic to a non-form-bound field.
282
+ * Must be called inside a computed() context (reads form signals).
283
+ */
284
+ function applyNonFieldLogic(rootFormRegistry, fieldDef) {
285
+ const result = {};
286
+ const rootForm = rootFormRegistry.rootForm();
287
+ if (rootForm) {
288
+ if (fieldDef.hidden !== undefined || fieldDef.logic?.some((l) => l.type === 'hidden')) {
289
+ const hiddenSignal = resolveNonFieldHidden({
290
+ form: rootForm,
291
+ fieldLogic: fieldDef.logic,
292
+ explicitValue: fieldDef.hidden,
293
+ });
294
+ result.hidden = hiddenSignal();
295
+ }
296
+ // Resolve disabled state if explicitly set or has logic
297
+ if (fieldDef.disabled !== undefined || fieldDef.logic?.some((l) => l.type === 'disabled')) {
298
+ // Create the signal and READ IT to establish reactive dependencies
299
+ const disabledSignal = resolveNonFieldDisabled({
300
+ form: rootForm,
301
+ fieldLogic: fieldDef.logic,
302
+ explicitValue: fieldDef.disabled,
303
+ });
304
+ result.disabled = disabledSignal();
305
+ }
306
+ }
307
+ else {
308
+ // Fallback to static values when rootForm is not available
309
+ if (fieldDef.hidden !== undefined) {
310
+ result.hidden = fieldDef.hidden;
311
+ }
312
+ if (fieldDef.disabled !== undefined) {
313
+ result.disabled = fieldDef.disabled;
314
+ }
315
+ }
316
+ return result;
317
+ }
318
+ /**
319
+ * Applies hidden logic to a non-form-bound field.
320
+ *
321
+ * This is a focused utility for fields that only need hidden state resolution.
322
+ * Use this when you need hidden logic but handle disabled separately
323
+ * (e.g., navigation buttons with custom disabled logic).
324
+ *
325
+ * NOTE: This function must be called inside a computed() context.
326
+ *
327
+ * @param rootForm The root form FieldTree (can be undefined)
328
+ * @param fieldDef The field definition with optional hidden/logic
329
+ * @returns The resolved hidden boolean value, or undefined if no hidden logic
330
+ */
331
+ function resolveHiddenValue(rootForm, fieldDef) {
332
+ if (!rootForm) {
333
+ return fieldDef.hidden;
334
+ }
335
+ if (fieldDef.hidden !== undefined || fieldDef.logic?.some((l) => l.type === 'hidden')) {
336
+ return resolveNonFieldHidden({
337
+ form: rootForm,
338
+ fieldLogic: fieldDef.logic,
339
+ explicitValue: fieldDef.hidden,
340
+ // formValue omitted intentionally - resolver will read form.value() reactively
341
+ })();
342
+ }
343
+ return undefined;
344
+ }
345
+
245
346
  /**
246
347
  * Maps a button field to component inputs.
247
348
  *
@@ -252,11 +353,16 @@ function sliderFieldMapper(fieldDef) {
252
353
  * - event: The FormEvent constructor to emit when clicked
253
354
  * - eventArgs: Optional arguments to pass to the event constructor
254
355
  *
356
+ * Hidden and disabled states are resolved using non-field logic resolvers which consider:
357
+ * 1. Explicit `hidden: true` / `disabled: true` on the field definition
358
+ * 2. Field-level `logic` array with `type: 'hidden'` or `type: 'disabled'` conditions
359
+ *
255
360
  * @param fieldDef The button field definition
256
361
  * @returns Signal containing Record of input names to values for ngComponentOutlet
257
362
  */
258
363
  function buttonFieldMapper(fieldDef) {
259
364
  const defaultProps = inject(DEFAULT_PROPS);
365
+ const rootFormRegistry = inject(RootFormRegistryService);
260
366
  return computed(() => {
261
367
  const inputs = buildBaseInputs(fieldDef, defaultProps());
262
368
  // Add button-specific properties
@@ -264,9 +370,296 @@ function buttonFieldMapper(fieldDef) {
264
370
  if (fieldDef.eventArgs !== undefined) {
265
371
  inputs['eventArgs'] = fieldDef.eventArgs;
266
372
  }
373
+ // Apply hidden/disabled logic
374
+ return {
375
+ ...inputs,
376
+ ...applyNonFieldLogic(rootFormRegistry, fieldDef),
377
+ };
378
+ });
379
+ }
380
+
381
+ /**
382
+ * Resolves array button context for button mappers.
383
+ *
384
+ * Handles both inside-array (context from ARRAY_CONTEXT) and
385
+ * outside-array (explicit arrayKey) placements.
386
+ *
387
+ * Must be called in injection context.
388
+ *
389
+ * @param fieldKey The button field key (for logging)
390
+ * @param buttonType The button type name (for logging)
391
+ * @param explicitArrayKey Optional explicit arrayKey from field definition
392
+ * @returns The array button context with reactive index signal
393
+ */
394
+ function resolveArrayButtonContext(fieldKey, buttonType, explicitArrayKey) {
395
+ const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
396
+ const logger = inject(DynamicFormLogger);
397
+ // Priority: explicit arrayKey from fieldDef > arrayKey from context
398
+ const targetArrayKey = explicitArrayKey ?? arrayContext?.arrayKey;
399
+ if (!targetArrayKey) {
400
+ logger.warn(`${buttonType} button "${fieldKey}" has no array context. ` +
401
+ 'Either place it inside an array field, or provide an explicit arrayKey property.');
402
+ }
403
+ // Create index signal once at injection time - computed to track arrayContext.index changes
404
+ const indexSignal = computed(() => {
405
+ if (!arrayContext)
406
+ return -1;
407
+ return arrayContext.index();
408
+ }, ...(ngDevMode ? [{ debugName: "indexSignal" }] : []));
409
+ return {
410
+ targetArrayKey,
411
+ indexSignal,
412
+ formValue: (arrayContext?.formValue ?? {}),
413
+ isInsideArray: arrayContext !== null,
414
+ };
415
+ }
416
+ /**
417
+ * Builds the event context passed to the button component.
418
+ *
419
+ * Should be called inside a computed to track index signal changes.
420
+ *
421
+ * @param fieldKey The button field key
422
+ * @param ctx The resolved array button context
423
+ * @param template Optional template for add operations (single field or array of fields)
424
+ * @returns The event context for the button component
425
+ */
426
+ function buildArrayButtonEventContext(fieldKey, ctx, template) {
427
+ const eventContext = {
428
+ key: fieldKey,
429
+ index: ctx.indexSignal(), // Evaluated inside computed
430
+ arrayKey: ctx.targetArrayKey ?? '',
431
+ formValue: ctx.formValue,
432
+ };
433
+ if (template) {
434
+ eventContext.template = template;
435
+ }
436
+ return eventContext;
437
+ }
438
+ /**
439
+ * Resolves eventArgs with defaults for array buttons.
440
+ *
441
+ * @param fieldEventArgs The eventArgs from field definition (may be undefined)
442
+ * @param defaultEventArgs The default eventArgs to use if not specified
443
+ * @returns The resolved eventArgs array
444
+ */
445
+ function resolveArrayButtonEventArgs(fieldEventArgs, defaultEventArgs = ['$arrayKey', '$template']) {
446
+ return fieldEventArgs ?? defaultEventArgs;
447
+ }
448
+
449
+ // =============================================================================
450
+ // Add Array Item Mappers
451
+ // =============================================================================
452
+ /**
453
+ * Mapper for add array item button — appends new item to end of array.
454
+ * Template is REQUIRED to define the field structure for new items.
455
+ */
456
+ function addArrayItemButtonMapper(fieldDef) {
457
+ const defaultProps = inject(DEFAULT_PROPS);
458
+ const rootFormRegistry = inject(RootFormRegistryService);
459
+ const ctx = resolveArrayButtonContext(fieldDef.key, 'addArrayItem', fieldDef.arrayKey);
460
+ return computed(() => {
461
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
462
+ return {
463
+ ...baseInputs,
464
+ event: AppendArrayItemEvent,
465
+ eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', '$template']),
466
+ eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),
467
+ ...applyNonFieldLogic(rootFormRegistry, fieldDef),
468
+ };
469
+ });
470
+ }
471
+ /**
472
+ * Mapper for prepend array item button — inserts new item at beginning of array.
473
+ * Template is REQUIRED to define the field structure for new items.
474
+ */
475
+ function prependArrayItemButtonMapper(fieldDef) {
476
+ const defaultProps = inject(DEFAULT_PROPS);
477
+ const rootFormRegistry = inject(RootFormRegistryService);
478
+ const ctx = resolveArrayButtonContext(fieldDef.key, 'prependArrayItem', fieldDef.arrayKey);
479
+ return computed(() => {
480
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
481
+ return {
482
+ ...baseInputs,
483
+ event: PrependArrayItemEvent,
484
+ eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', '$template']),
485
+ eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),
486
+ ...applyNonFieldLogic(rootFormRegistry, fieldDef),
487
+ };
488
+ });
489
+ }
490
+ /**
491
+ * Mapper for insert array item button — inserts new item at specific index.
492
+ * Template and index are REQUIRED.
493
+ */
494
+ function insertArrayItemButtonMapper(fieldDef) {
495
+ const defaultProps = inject(DEFAULT_PROPS);
496
+ const rootFormRegistry = inject(RootFormRegistryService);
497
+ const ctx = resolveArrayButtonContext(fieldDef.key, 'insertArrayItem', fieldDef.arrayKey);
498
+ return computed(() => {
499
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
500
+ return {
501
+ ...baseInputs,
502
+ event: InsertArrayItemEvent,
503
+ eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', fieldDef.index, '$template']),
504
+ eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),
505
+ ...applyNonFieldLogic(rootFormRegistry, fieldDef),
506
+ };
507
+ });
508
+ }
509
+ // =============================================================================
510
+ // Remove Array Item Mappers
511
+ // =============================================================================
512
+ /**
513
+ * Mapper for remove array item button.
514
+ * Inside array: RemoveAtIndexEvent. Outside array: PopArrayItemEvent.
515
+ */
516
+ function removeArrayItemButtonMapper(fieldDef) {
517
+ const defaultProps = inject(DEFAULT_PROPS);
518
+ const rootFormRegistry = inject(RootFormRegistryService);
519
+ const ctx = resolveArrayButtonContext(fieldDef.key, 'removeArrayItem', fieldDef.arrayKey);
520
+ // Choose event type based on context:
521
+ // - Inside array: RemoveAtIndexEvent (remove specific item)
522
+ // - Outside array: PopArrayItemEvent (remove last item)
523
+ const eventType = ctx.isInsideArray ? RemoveAtIndexEvent : PopArrayItemEvent;
524
+ const defaultEventArgs = ctx.isInsideArray ? ['$arrayKey', '$index'] : ['$arrayKey'];
525
+ return computed(() => {
526
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
527
+ return {
528
+ ...baseInputs,
529
+ event: eventType,
530
+ eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, defaultEventArgs),
531
+ eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),
532
+ ...applyNonFieldLogic(rootFormRegistry, fieldDef),
533
+ };
534
+ });
535
+ }
536
+ /** Mapper for pop array item button — removes the last item from array. */
537
+ function popArrayItemButtonMapper(fieldDef) {
538
+ const defaultProps = inject(DEFAULT_PROPS);
539
+ const rootFormRegistry = inject(RootFormRegistryService);
540
+ const ctx = resolveArrayButtonContext(fieldDef.key, 'popArrayItem', fieldDef.arrayKey);
541
+ return computed(() => {
542
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
543
+ return {
544
+ ...baseInputs,
545
+ event: PopArrayItemEvent,
546
+ eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey']),
547
+ eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),
548
+ ...applyNonFieldLogic(rootFormRegistry, fieldDef),
549
+ };
550
+ });
551
+ }
552
+ /** Mapper for shift array item button — removes the first item from array. */
553
+ function shiftArrayItemButtonMapper(fieldDef) {
554
+ const defaultProps = inject(DEFAULT_PROPS);
555
+ const rootFormRegistry = inject(RootFormRegistryService);
556
+ const ctx = resolveArrayButtonContext(fieldDef.key, 'shiftArrayItem', fieldDef.arrayKey);
557
+ return computed(() => {
558
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());
559
+ return {
560
+ ...baseInputs,
561
+ event: ShiftArrayItemEvent,
562
+ eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey']),
563
+ eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),
564
+ ...applyNonFieldLogic(rootFormRegistry, fieldDef),
565
+ };
566
+ });
567
+ }
568
+
569
+ // =============================================================================
570
+ // Submit Button Mapper
571
+ // =============================================================================
572
+ /**
573
+ * Mapper for submit button — configures native form submission via type="submit".
574
+ * Triggers native form submission rather than dispatching events directly.
575
+ */
576
+ function submitButtonFieldMapper(fieldDef) {
577
+ const rootFormRegistry = inject(RootFormRegistryService);
578
+ const defaultProps = inject(DEFAULT_PROPS);
579
+ const formOptions = inject(FORM_OPTIONS);
580
+ const fieldWithLogic = fieldDef;
581
+ return computed(() => {
582
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
583
+ const rootForm = rootFormRegistry.rootForm();
584
+ // Use rootForm (not fieldSignalContext.form) — submit needs root form validity (#157)
585
+ const disabled = resolveSubmitButtonDisabled({
586
+ form: rootForm,
587
+ formOptions: formOptions(),
588
+ fieldLogic: fieldWithLogic.logic,
589
+ explicitlyDisabled: fieldDef.disabled,
590
+ })();
591
+ const inputs = {
592
+ ...baseInputs,
593
+ // type="submit" triggers native form submission (no event dispatch needed)
594
+ props: { ...fieldDef.props, type: 'submit' },
595
+ disabled,
596
+ };
597
+ const hidden = resolveHiddenValue(rootForm, fieldWithLogic);
598
+ if (hidden !== undefined) {
599
+ inputs['hidden'] = hidden;
600
+ }
267
601
  return inputs;
268
602
  });
269
603
  }
604
+ // =============================================================================
605
+ // Next Page Button Mapper
606
+ // =============================================================================
607
+ /** Mapper for next page button — preconfigures NextPageEvent. */
608
+ function nextButtonFieldMapper(fieldDef) {
609
+ const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
610
+ const rootFormRegistry = inject(RootFormRegistryService);
611
+ const defaultProps = inject(DEFAULT_PROPS);
612
+ const formOptions = inject(FORM_OPTIONS);
613
+ const fieldWithLogic = fieldDef;
614
+ return computed(() => {
615
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
616
+ const rootForm = rootFormRegistry.rootForm();
617
+ const disabled = resolveNextButtonDisabled({
618
+ form: fieldSignalContext.form,
619
+ formOptions: formOptions(),
620
+ fieldLogic: fieldWithLogic.logic,
621
+ currentPageValid: fieldSignalContext.currentPageValid,
622
+ })();
623
+ const inputs = {
624
+ ...baseInputs,
625
+ event: NextPageEvent,
626
+ disabled,
627
+ };
628
+ const hidden = resolveHiddenValue(rootForm, fieldWithLogic);
629
+ if (hidden !== undefined) {
630
+ inputs['hidden'] = hidden;
631
+ }
632
+ return inputs;
633
+ });
634
+ }
635
+ // =============================================================================
636
+ // Previous Page Button Mapper
637
+ // =============================================================================
638
+ /** Mapper for previous page button — preconfigures PreviousPageEvent. */
639
+ function previousButtonFieldMapper(fieldDef) {
640
+ const defaultProps = inject(DEFAULT_PROPS);
641
+ const rootFormRegistry = inject(RootFormRegistryService);
642
+ const fieldWithLogic = fieldDef;
643
+ return computed(() => {
644
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
645
+ const rootForm = rootFormRegistry.rootForm();
646
+ const inputs = {
647
+ ...baseInputs,
648
+ event: PreviousPageEvent,
649
+ };
650
+ // Add disabled binding only if explicitly set by user
651
+ if (fieldDef.disabled !== undefined) {
652
+ inputs['disabled'] = fieldDef.disabled;
653
+ }
654
+ const hidden = resolveHiddenValue(rootForm, fieldWithLogic);
655
+ if (hidden !== undefined) {
656
+ inputs['hidden'] = hidden;
657
+ }
658
+ return inputs;
659
+ });
660
+ }
661
+
662
+ // Button field mapper (generic)
270
663
 
271
664
  // UI-specific field mappers
272
665
 
@@ -359,8 +752,8 @@ function resolveErrorMessage(error, fieldMessages, defaultMessages, injector, lo
359
752
  }
360
753
 
361
754
  /**
362
- * Computed signal that determines if errors should be displayed
363
- * Based on field's invalid, touched, and error count
755
+ * Computed signal that determines if errors should be displayed.
756
+ * Based on field's invalid, touched, and error count.
364
757
  *
365
758
  * @param field - Signal containing FieldTree
366
759
  * @returns Signal<boolean> - True if errors should be displayed
@@ -467,5 +860,5 @@ function setupMetaTracking(elementRef, meta, options) {
467
860
  * Generated bundle index. Do not edit.
468
861
  */
469
862
 
470
- export { buildValueFieldInputs, checkboxFieldMapper, createResolvedErrorsSignal, datepickerFieldMapper, optionsFieldMapper, resolveValueFieldContext, setupMetaTracking, shouldShowErrors, valueFieldMapper };
863
+ export { addArrayItemButtonMapper, buildArrayButtonEventContext, buildValueFieldInputs, buttonFieldMapper, checkboxFieldMapper, createResolvedErrorsSignal, datepickerFieldMapper, insertArrayItemButtonMapper, nextButtonFieldMapper, optionsFieldMapper, popArrayItemButtonMapper, prependArrayItemButtonMapper, previousButtonFieldMapper, removeArrayItemButtonMapper, resolveArrayButtonContext, resolveArrayButtonEventArgs, resolveValueFieldContext, setupMetaTracking, shiftArrayItemButtonMapper, shouldShowErrors, submitButtonFieldMapper, valueFieldMapper };
471
864
  //# sourceMappingURL=ng-forge-dynamic-forms-integration.mjs.map