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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs → ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs} +167 -69
  2. package/fesm2022/ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs.map +1 -0
  3. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs → ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs} +9 -5
  4. package/fesm2022/ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs.map +1 -0
  5. package/fesm2022/{ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs → ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs} +2 -2
  6. package/fesm2022/{ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs.map → ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs.map} +1 -1
  7. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs → ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs} +2 -2
  8. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs.map} +1 -1
  9. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs → ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs} +2 -2
  10. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs.map → ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs.map} +1 -1
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs → ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs} +30 -6
  12. package/fesm2022/ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs.map +1 -0
  13. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs} +713 -318
  14. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs.map +1 -0
  15. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs → ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs} +9 -5
  16. package/fesm2022/ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs.map +1 -0
  17. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs → ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs} +2 -2
  18. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs.map → ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs.map} +1 -1
  19. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  20. package/package.json +1 -1
  21. package/types/ng-forge-dynamic-forms.d.ts +2042 -1803
  22. package/fesm2022/ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs.map +0 -1
  23. package/fesm2022/ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs.map +0 -1
  24. package/fesm2022/ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs.map +0 -1
  25. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs.map +0 -1
  26. package/fesm2022/ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, reflectComponentType, Injector, inject, EnvironmentInjector, DestroyRef, computed, input, ViewContainerRef, signal, Directive, Injectable, isSignal, untracked, linkedSignal, ChangeDetectionStrategy, Component, resourceFromSnapshots, runInInjectionContext, afterNextRender, isWritableSignal, model, makeEnvironmentProviders, Pipe } from '@angular/core';
2
+ import { InjectionToken, reflectComponentType, Injector, inject, EnvironmentInjector, DestroyRef, computed, signal, input, ViewContainerRef, Directive, Injectable, isSignal, untracked, linkedSignal, ChangeDetectionStrategy, Component, resourceFromSnapshots, runInInjectionContext, afterNextRender, isWritableSignal, model, makeEnvironmentProviders, Pipe } from '@angular/core';
3
3
  import { explicitEffect } from 'ngxtension/explicit-effect';
4
- import { of, forkJoin, from, catchError, map, switchMap, isObservable, firstValueFrom, exhaustMap, EMPTY, Subject, filter, TimeoutError, throwError, defer, combineLatest, pipe, debounceTime, distinctUntilChanged, tap, startWith, scan as scan$1, Observable, concatMap, take as take$1, pairwise, takeUntil, combineLatestWith, auditTime, scheduled, queueScheduler, merge, timer } from 'rxjs';
5
4
  import { takeUntilDestroyed, toObservable, toSignal, rxResource, outputFromObservable } from '@angular/core/rxjs-interop';
5
+ import { of, forkJoin, from, catchError, map, switchMap, isObservable, firstValueFrom, exhaustMap, EMPTY, Subject, filter, TimeoutError, throwError, defer, combineLatest, pipe, debounceTime, distinctUntilChanged, tap, startWith, scan as scan$1, Observable, concatMap, take as take$1, pairwise, takeUntil, combineLatestWith, auditTime, scheduled, queueScheduler, merge, timer } from 'rxjs';
6
6
  import { submit, email, min, max, minLength, maxLength, pattern, required, validate, validateAsync, validateHttp, disabled, readonly, hidden, applyEach, applyWhenValue, applyWhen, apply, schema, validateStandardSchema, validateTree, form, provideSignalFormsConfig } from '@angular/forms/signals';
7
7
  import { scan, map as map$1, filter as filter$1, take, switchMap as switchMap$1, timeout, catchError as catchError$1, shareReplay } from 'rxjs/operators';
8
8
  import { HttpClient } from '@angular/common/http';
@@ -139,6 +139,32 @@ const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT', {
139
139
  * ```
140
140
  */
141
141
  const ARRAY_CONTEXT = new InjectionToken('ARRAY_CONTEXT');
142
+ /**
143
+ * Injection token for providing group ancestry to mappers and components.
144
+ *
145
+ * Re-provided in two places:
146
+ * 1. `GroupFieldComponent` provides a context whose `groupPath()` is the
147
+ * parent path extended by its own key (e.g. `'address'` or `'user.address'`).
148
+ * 2. `createArrayItemInjector` provides a sentinel with an empty `groupPath`,
149
+ * resetting the chain at array boundaries — descendants of an array item
150
+ * that re-enter a group compose paths INSIDE the array item, matching the
151
+ * property-derivation collector's keying.
152
+ *
153
+ * `mapFieldToInputs` reads this to scope DOM IDs and override-store keys so
154
+ * the same leaf key can appear in different groups without colliding (#401).
155
+ *
156
+ * Inject with `{ optional: true }` because top-level fields with no group
157
+ * ancestor (and no enclosing array) won't have the token provided. The token
158
+ * IS present for fields inside an array even when no inner group exists — the
159
+ * array-boundary sentinel makes injection succeed with an empty `groupPath`.
160
+ */
161
+ // No factory: GROUP_CONTEXT is optional-by-design — `GroupFieldComponent` and
162
+ // `createArrayItemInjector` re-provide it where it makes sense, and consumers
163
+ // inject with `{ optional: true }` to handle the top-level case. Mirrors the
164
+ // ARRAY_CONTEXT pattern. Adding a throwing factory here fires even when
165
+ // injected optionally (Angular still evaluates the tree-shakable factory
166
+ // before honoring `optional`), so non-group fields would crash on every render.
167
+ const GROUP_CONTEXT = new InjectionToken('GROUP_CONTEXT');
142
168
  /**
143
169
  * Injection token for form-level default props.
144
170
  *
@@ -226,65 +252,57 @@ const FORM_OPTIONS = new InjectionToken('FORM_OPTIONS');
226
252
  * and inherited by all children via Angular's hierarchical injector.
227
253
  */
228
254
  const EXTERNAL_DATA = new InjectionToken('EXTERNAL_DATA');
255
+
229
256
  /**
230
- * Injection token for array-level template registry.
231
- *
232
- * This registry tracks which template was used to create each array item,
233
- * keyed by the item's unique ID. This is essential for "recreate" operations
234
- * (e.g., after removing items from the middle) where we need to re-resolve
235
- * items using their original templates, not a fallback.
236
- *
237
- * The token is provided by ArrayFieldComponent at its level and shared
238
- * across all items in that array. Each nested array gets its own registry.
239
- *
240
- * @example
241
- * ```typescript
242
- * // In ArrayFieldComponent
243
- * providers: [
244
- * { provide: ARRAY_TEMPLATE_REGISTRY, useValue: new Map() }
245
- * ]
246
- *
247
- * // When adding items
248
- * const registry = inject(ARRAY_TEMPLATE_REGISTRY);
249
- * registry.set(itemId, templates);
257
+ * Injection token for the dynamic forms logger.
250
258
  *
251
- * // During recreate, look up original template
252
- * const originalTemplate = registry.get(existingItemId);
253
- * ```
259
+ * Provided by provideDynamicForm() with ConsoleLogger as default.
260
+ * Override with withLoggerConfig(false) to disable logging.
254
261
  */
255
- const ARRAY_TEMPLATE_REGISTRY = new InjectionToken('ARRAY_TEMPLATE_REGISTRY');
262
+ const DynamicFormLogger = new InjectionToken('DynamicFormLogger');
263
+
256
264
  /**
257
- * Injection token for array-level item ID generator.
258
- *
259
- * Provides a function that generates unique IDs for array items. Each array
260
- * component instance gets its own generator (via useFactory), ensuring:
261
- * - SSR hydration compatibility (server and client generate same IDs for same array)
262
- * - No global state pollution between form instances
263
- * - Deterministic IDs within each array's lifecycle
264
- *
265
- * The token is provided by ArrayFieldComponent at its level with a factory
266
- * that creates a fresh counter for each array instance.
267
- *
268
- * @example
269
- * ```typescript
270
- * // In ArrayFieldComponent
271
- * providers: [
272
- * { provide: ARRAY_ITEM_ID_GENERATOR, useFactory: createArrayItemIdGenerator }
273
- * ]
274
- *
275
- * // Usage
276
- * const generateId = inject(ARRAY_ITEM_ID_GENERATOR);
277
- * const itemId = generateId(); // 'item-0', 'item-1', etc.
278
- * ```
265
+ * Shared empty chain — returned when no wrappers apply so callers get a
266
+ * stable reference. Freezing makes accidental mutation observable.
279
267
  */
280
- const ARRAY_ITEM_ID_GENERATOR = new InjectionToken('ARRAY_ITEM_ID_GENERATOR');
268
+ const EMPTY_WRAPPERS = Object.freeze([]);
281
269
  /**
282
- * Factory function that creates a new array item ID generator.
283
- * Each invocation creates an independent counter starting at 0.
270
+ * Element-wise identity (`===`) comparator for wrapper chains. Used as the
271
+ * `equal` option on signal memoisation so reconciled `FieldDef`s with the
272
+ * same chain don't churn downstream — relies on `WrapperConfig` objects
273
+ * being stable across ticks, which holds for configs declared in
274
+ * `FormConfig` / `createWrappers(...)`.
284
275
  */
285
- function createArrayItemIdGenerator() {
286
- let counter = 0;
287
- return () => `item-${counter++}`;
276
+ function isSameWrapperChain(a, b) {
277
+ if (a === b)
278
+ return true;
279
+ if (a.length !== b.length)
280
+ return false;
281
+ for (let i = 0; i < a.length; i++) {
282
+ if (a[i] !== b[i])
283
+ return false;
284
+ }
285
+ return true;
286
+ }
287
+ /**
288
+ * Resolves the wrapper chain for a field.
289
+ *
290
+ * Merge order (outermost → innermost): auto-associations, form defaults,
291
+ * field-level wrappers. `wrappers: null` skips all three; `skipAutoWrappers` /
292
+ * `skipDefaultWrappers` on the field opt out of individual layers. Fresh
293
+ * array per call — downstream memoisation is on the consuming computed.
294
+ */
295
+ function resolveWrappers(field, defaultWrappers, autoAssociations) {
296
+ if (field.wrappers === null) {
297
+ return EMPTY_WRAPPERS;
298
+ }
299
+ const autoWrappers = field.skipAutoWrappers ? EMPTY_WRAPPERS : (autoAssociations.get(field.type) ?? EMPTY_WRAPPERS);
300
+ const defaults = field.skipDefaultWrappers ? EMPTY_WRAPPERS : (defaultWrappers ?? EMPTY_WRAPPERS);
301
+ const fieldLevel = field.wrappers ?? EMPTY_WRAPPERS;
302
+ if (autoWrappers.length === 0 && defaults.length === 0 && fieldLevel.length === 0) {
303
+ return EMPTY_WRAPPERS;
304
+ }
305
+ return [...autoWrappers, ...defaults, ...fieldLevel];
288
306
  }
289
307
 
290
308
  /**
@@ -481,58 +499,6 @@ class WrapperAwareInjector extends Injector {
481
499
  }
482
500
  }
483
501
 
484
- /**
485
- * Injection token for the dynamic forms logger.
486
- *
487
- * Provided by provideDynamicForm() with ConsoleLogger as default.
488
- * Override with withLoggerConfig(false) to disable logging.
489
- */
490
- const DynamicFormLogger = new InjectionToken('DynamicFormLogger');
491
-
492
- /**
493
- * Shared empty chain — returned when no wrappers apply so callers get a
494
- * stable reference. Freezing makes accidental mutation observable.
495
- */
496
- const EMPTY_WRAPPERS = Object.freeze([]);
497
- /**
498
- * Element-wise identity (`===`) comparator for wrapper chains. Used as the
499
- * `equal` option on signal memoisation so reconciled `FieldDef`s with the
500
- * same chain don't churn downstream — relies on `WrapperConfig` objects
501
- * being stable across ticks, which holds for configs declared in
502
- * `FormConfig` / `createWrappers(...)`.
503
- */
504
- function isSameWrapperChain(a, b) {
505
- if (a === b)
506
- return true;
507
- if (a.length !== b.length)
508
- return false;
509
- for (let i = 0; i < a.length; i++) {
510
- if (a[i] !== b[i])
511
- return false;
512
- }
513
- return true;
514
- }
515
- /**
516
- * Resolves the wrapper chain for a field.
517
- *
518
- * Merge order (outermost → innermost): auto-associations, form defaults,
519
- * field-level wrappers. `wrappers: null` skips all three; `skipAutoWrappers` /
520
- * `skipDefaultWrappers` on the field opt out of individual layers. Fresh
521
- * array per call — downstream memoisation is on the consuming computed.
522
- */
523
- function resolveWrappers(field, defaultWrappers, autoAssociations) {
524
- if (field.wrappers === null) {
525
- return EMPTY_WRAPPERS;
526
- }
527
- const autoWrappers = field.skipAutoWrappers ? EMPTY_WRAPPERS : (autoAssociations.get(field.type) ?? EMPTY_WRAPPERS);
528
- const defaults = field.skipDefaultWrappers ? EMPTY_WRAPPERS : (defaultWrappers ?? EMPTY_WRAPPERS);
529
- const fieldLevel = field.wrappers ?? EMPTY_WRAPPERS;
530
- if (autoWrappers.length === 0 && defaults.length === 0 && fieldLevel.length === 0) {
531
- return EMPTY_WRAPPERS;
532
- }
533
- return [...autoWrappers, ...defaults, ...fieldLevel];
534
- }
535
-
536
502
  /**
537
503
  * Owns the wrapper-chain lifecycle for a field or container: async component
538
504
  * loading, switchMap-based cancellation of stale loads, flicker-tolerant
@@ -723,6 +689,155 @@ function toReadonlyFieldTreeCached(cache, field) {
723
689
  return view;
724
690
  }
725
691
 
692
+ const EMPTY_STATE = Object.freeze({ phase: 'empty' });
693
+ /**
694
+ * Encapsulates the imperative `ViewContainerRef` / `ComponentRef` lifecycle
695
+ * for the innermost field component. Pulled out of `DfFieldOutlet` so the
696
+ * directive itself stays declarative — signals + constructor wiring — while
697
+ * the unavoidable mutable bookkeeping (Angular's view container API is
698
+ * imperative) lives in one place behind a small, focused surface.
699
+ *
700
+ * State is held in a single `signal<FieldSlotState>` discriminated union;
701
+ * every mutation is an explicit `state.set(...)` transition. No nullable
702
+ * instance fields, no `undefined`-initialized refs — each phase carries
703
+ * exactly the data it owns.
704
+ *
705
+ * Responsibilities:
706
+ * - Create the component (or reuse the same-class instance across a rebuild,
707
+ * preserving its DOM state, focus, and caret).
708
+ * - Push mapper inputs via `setInput`, dedup'd per-key so dev-mode NG0303
709
+ * warnings for undeclared mapper keys don't replay on every emission.
710
+ * - Detach cleanly before the outer VCR clears, so same-class reuse can
711
+ * re-insert the preserved `hostView`.
712
+ * - Destroy on directive teardown when the host view is still detached.
713
+ */
714
+ class FieldComponentSlot {
715
+ state = signal(EMPTY_STATE, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
716
+ /**
717
+ * Per-key cache for `pushInputs`. Lives outside the lifecycle state machine on
718
+ * purpose — it's a `setInput`-call optimization that changes on every keystroke,
719
+ * not a phase transition. Keeping it here means the `state` signal only emits
720
+ * on real `empty ⇄ mounted ⇄ detached` transitions (≈ once per
721
+ * mount/destroy cycle), not once per input emission.
722
+ */
723
+ lastInputs = null;
724
+ /** Read-only view of the lifecycle phase. Useful for tests and assertions. */
725
+ phase = () => this.state().phase;
726
+ /** Read-only view of the full state. Exposed so tests can assert ref stability. */
727
+ snapshot = () => this.state();
728
+ /**
729
+ * Mount the field component into `slot`. Reuses the existing `ComponentRef`
730
+ * when its component type matches (preserving DOM/focus/caret across a
731
+ * wrapper-chain rebuild); otherwise discards the old ref and creates a fresh
732
+ * one.
733
+ */
734
+ mountOrReuse(slot, component, fieldInjector, environmentInjector, rawInputs) {
735
+ const current = this.state();
736
+ if (current.phase !== 'empty' && current.ref.componentType === component) {
737
+ slot.insert(current.ref.hostView);
738
+ this.lastInputs = null;
739
+ this.state.set(Object.freeze({ phase: 'mounted', ref: current.ref, slot }));
740
+ this.pushInputs(rawInputs);
741
+ if (current.phase === 'detached')
742
+ this.replayFocus(current.focusSnapshot);
743
+ return;
744
+ }
745
+ // Different component class — destroy the prior ref (if any) and create
746
+ // a fresh one. The `empty` transition before `createComponent` ensures a
747
+ // throw inside the factory doesn't leave a dangling ref the next mount
748
+ // would try to re-insert.
749
+ if (current.phase !== 'empty')
750
+ current.ref.destroy();
751
+ this.lastInputs = null;
752
+ this.state.set(EMPTY_STATE);
753
+ const ref = slot.createComponent(component, {
754
+ environmentInjector,
755
+ injector: createWrapperAwareInjector(fieldInjector, slot.injector),
756
+ });
757
+ this.state.set(Object.freeze({ phase: 'mounted', ref, slot }));
758
+ this.pushInputs(rawInputs);
759
+ }
760
+ /**
761
+ * Push mapper inputs onto the mounted component. Per-key reference compare
762
+ * skips `setInput` for unchanged values — necessary in dev mode where
763
+ * `setInput` on unknown keys logs NG0303 per call (Angular 21 is lenient
764
+ * at runtime but warns); without the cache, every emission would re-warn
765
+ * for every undeclared mapper key.
766
+ *
767
+ * Updates the `lastInputs` cache (a plain field, not part of `state`) so the
768
+ * `state` signal doesn't churn its reference on every keystroke.
769
+ *
770
+ * `reflectComponentType` does NOT include hostDirective-forwarded inputs
771
+ * (angular/angular#49734), so a declared-input filter here would skip every
772
+ * value flowing through `NgForgeFieldShell` / `NgForgeField` /
773
+ * `NgForgeAction`. Mapper-as-contract is enforced by the `*_INPUTS`
774
+ * lockstep type assertions, not at runtime here.
775
+ */
776
+ pushInputs(rawInputs) {
777
+ const current = this.state();
778
+ if (current.phase !== 'mounted')
779
+ return;
780
+ const prev = this.lastInputs;
781
+ if (prev === rawInputs)
782
+ return;
783
+ for (const [key, value] of Object.entries(rawInputs)) {
784
+ if (prev && prev[key] === value)
785
+ continue;
786
+ current.ref.setInput(key, value);
787
+ }
788
+ this.lastInputs = rawInputs;
789
+ }
790
+ /**
791
+ * Detach the host view from its current slot, capturing focus + caret so the
792
+ * next `mountOrReuse()` can replay them after re-inserting into the new
793
+ * slot. A stranded `mounted` state with a bad slot index (re-entrancy during
794
+ * a partial rebuild) is treated like `empty` to avoid leaks.
795
+ */
796
+ detach() {
797
+ const current = this.state();
798
+ if (current.phase !== 'mounted')
799
+ return;
800
+ const focusSnapshot = this.captureFocus(current.ref);
801
+ const idx = current.slot.indexOf(current.ref.hostView);
802
+ if (idx >= 0)
803
+ current.slot.detach(idx);
804
+ this.lastInputs = null;
805
+ this.state.set(Object.freeze({ phase: 'detached', ref: current.ref, focusSnapshot }));
806
+ }
807
+ /**
808
+ * Called on directive teardown. Only destroys when the host view is still
809
+ * detached — otherwise the outer `vcr.clear()` cascade has already destroyed
810
+ * it (calling `.destroy()` again would throw).
811
+ */
812
+ destroyOnTeardown() {
813
+ const current = this.state();
814
+ if (current.phase === 'detached')
815
+ current.ref.destroy();
816
+ this.lastInputs = null;
817
+ this.state.set(EMPTY_STATE);
818
+ }
819
+ captureFocus(ref) {
820
+ const hostEl = ref.location.nativeElement;
821
+ const active = document.activeElement;
822
+ if (!(active instanceof HTMLElement) || !hostEl.contains(active))
823
+ return null;
824
+ const sel = active;
825
+ return {
826
+ element: active,
827
+ selectionStart: typeof sel.selectionStart === 'number' ? sel.selectionStart : null,
828
+ selectionEnd: typeof sel.selectionEnd === 'number' ? sel.selectionEnd : null,
829
+ };
830
+ }
831
+ replayFocus(snap) {
832
+ if (!snap || !snap.element.isConnected)
833
+ return;
834
+ snap.element.focus();
835
+ if (snap.selectionStart !== null && snap.selectionEnd !== null && 'setSelectionRange' in snap.element) {
836
+ snap.element.setSelectionRange(snap.selectionStart, snap.selectionEnd);
837
+ }
838
+ }
839
+ }
840
+
726
841
  /**
727
842
  * Structural directive that renders a `ResolvedField` with its effective
728
843
  * wrapper chain.
@@ -743,12 +858,16 @@ function toReadonlyFieldTreeCached(cache, field) {
743
858
  * includes the field's mapper outputs and a `ReadonlyFieldTree` view of its
744
859
  * form state.
745
860
  *
746
- * Rendering is gated by `field.renderReady()` — the directive waits until
747
- * the mapper produces the required inputs before instantiating the component.
748
- * Once rendered, a transient `renderReady → false` does *not* tear the chain
749
- * down; the controller keeps the mounted components alive and ignores the
750
- * flicker. Only a structural change (wrappers or component class) triggers
751
- * a rebuild.
861
+ * Rendering is gated by `field.renderReady() && !field.hidden()` — the
862
+ * directive waits until the mapper produces the required inputs AND the
863
+ * field isn't hidden before instantiating the component. Once rendered, a
864
+ * transient `renderReady → false` does *not* tear the chain down; the
865
+ * controller keeps the mounted components alive and ignores the flicker.
866
+ * Only a structural change (wrappers or component class) triggers a rebuild.
867
+ *
868
+ * Imperative `ComponentRef` / `ViewContainerRef` lifecycle is encapsulated
869
+ * in `FieldComponentSlot` — the directive itself is signal definitions
870
+ * plus three method calls (`mountOrReuse`, `detach`, `destroyOnTeardown`).
752
871
  *
753
872
  * @example
754
873
  * ```html
@@ -768,25 +887,16 @@ class DfFieldOutlet {
768
887
  wrapperAutoAssociations = inject(WRAPPER_AUTO_ASSOCIATIONS);
769
888
  defaultWrappersSignal = inject(DEFAULT_WRAPPERS, { optional: true });
770
889
  readonlyFieldCache = inject(READONLY_FIELD_TREE_CACHE);
771
- fieldRef;
772
- /**
773
- * VCR the `fieldRef`'s host view is currently inserted into (either the outer
774
- * VCR when there are no wrappers, or the innermost wrapper's `#fieldComponent`
775
- * slot). Tracked so `beforeRebuild` can detach the view before the outer VCR
776
- * cascade-destroys it.
777
- */
778
- fieldSlot;
779
- /** Focus + caret captured during detach, replayed after re-insert. */
780
- focusSnapshot;
890
+ /** Encapsulates Angular's imperative ComponentRef / ViewContainerRef lifecycle. */
891
+ fieldComponent = new FieldComponentSlot();
892
+ componentIdentity = computed(() => this.dfFieldOutlet().component, ...(ngDevMode ? [{ debugName: "componentIdentity" }] : /* istanbul ignore next */ []));
781
893
  /**
782
- * Last rawInputs reference pushed to the innermost field. Used to dedupe the
783
- * rawInputs effect when the same snapshot was already applied as part of the
784
- * initial render — keeps per-keystroke input updates O(changed-keys) instead
785
- * of re-walking the whole input bag.
894
+ * Gate for the wrapper chain controller: true only when required inputs are populated AND the
895
+ * field isn't hidden. Combining the two prevents an initial-mount race where the chain mounts
896
+ * during the brief window between `renderReady` flipping true and `hidden()` settling — that
897
+ * window is exactly where Angular Signal Forms' `[formField]` directive emits NG01916.
786
898
  */
787
- lastPushedInputs;
788
- componentIdentity = computed(() => this.dfFieldOutlet().component, ...(ngDevMode ? [{ debugName: "componentIdentity" }] : /* istanbul ignore next */ []));
789
- renderReady = computed(() => this.dfFieldOutlet().renderReady(), ...(ngDevMode ? [{ debugName: "renderReady" }] : /* istanbul ignore next */ []));
899
+ renderReady = computed(() => this.dfFieldOutlet().renderReady() && !this.dfFieldOutlet().hidden(), ...(ngDevMode ? [{ debugName: "renderReady" }] : /* istanbul ignore next */ []));
790
900
  rawInputs = computed(() => this.dfFieldOutlet().inputs(), ...(ngDevMode ? [{ debugName: "rawInputs" }] : /* istanbul ignore next */ []));
791
901
  /**
792
902
  * Effective wrapper chain. Element-wise identity comparison keeps the signal
@@ -813,109 +923,17 @@ class DfFieldOutlet {
813
923
  rebuildKey: this.componentIdentity,
814
924
  fieldInputs: this.fieldInputs,
815
925
  fieldInjector: this.fieldInjector,
816
- beforeRebuild: () => this.detachFieldRef(),
926
+ beforeRebuild: () => this.fieldComponent.detach(),
817
927
  renderInnermost: (slot) => {
818
928
  const resolved = this.dfFieldOutlet();
819
- if (this.fieldRef && this.fieldRef.componentType === resolved.component) {
820
- // Same component class — re-insert the preserved hostView. Browser
821
- // loses focus on detach, so we replay the snapshot captured in
822
- // detachFieldRef. Input value persists on the DOM node itself.
823
- slot.insert(this.fieldRef.hostView);
824
- this.fieldSlot = slot;
825
- this.pushRawInputs(this.fieldRef, this.rawInputs());
826
- this.restoreFocusSnapshot();
827
- return;
828
- }
829
- // Different component class — discard the old ref and create fresh.
830
- // Merge field + element injectors so the field can inject ancestor
831
- // wrappers AND form-context tokens (ARRAY_CONTEXT etc.).
832
- // Clear state before createComponent — if it throws (bad provider,
833
- // invalid class) we don't want to leak a reference to a destroyed ref
834
- // that the same-component branch would later try to re-insert.
835
- this.focusSnapshot = undefined;
836
- this.lastPushedInputs = undefined;
837
- const previousRef = this.fieldRef;
838
- this.fieldRef = undefined;
839
- this.fieldSlot = undefined;
840
- previousRef?.destroy();
841
- this.fieldRef = slot.createComponent(resolved.component, {
842
- environmentInjector: this.fieldEnvInjector(),
843
- injector: createWrapperAwareInjector(resolved.injector, slot.injector),
844
- });
845
- this.fieldSlot = slot;
846
- // setInput dirties the signal synchronously; the next CD pass reads
847
- // it. Safe outside CD — see wrapper-chain-controller scheduling.
848
- this.pushRawInputs(this.fieldRef, this.rawInputs());
929
+ this.fieldComponent.mountOrReuse(slot, resolved.component, resolved.injector, this.fieldEnvInjector(), this.rawInputs());
849
930
  },
850
931
  });
851
- // Push rawInputs to the innermost field. Safe mid-rebuild — renderInnermost
852
- // re-reads rawInputs synchronously on mount, so skipping here loses nothing.
853
- explicitEffect([this.rawInputs], ([rawInputs]) => {
854
- if (!this.fieldRef)
855
- return;
856
- if (this.lastPushedInputs === rawInputs)
857
- return;
858
- this.pushRawInputs(this.fieldRef, rawInputs);
859
- });
860
- this.destroyRef.onDestroy(() => {
861
- // Only destroy when detached — vcr.clear() cascades otherwise.
862
- if (this.fieldRef && !this.fieldSlot)
863
- this.fieldRef.destroy();
864
- this.fieldRef = undefined;
865
- this.fieldSlot = undefined;
866
- this.lastPushedInputs = undefined;
867
- });
868
- }
869
- detachFieldRef() {
870
- if (!this.fieldRef)
871
- return;
872
- if (!this.fieldSlot) {
873
- // Stranded fieldRef — destroy to prevent a leak.
874
- this.fieldRef.destroy();
875
- this.fieldRef = undefined;
876
- return;
877
- }
878
- this.captureFocusSnapshot();
879
- const idx = this.fieldSlot.indexOf(this.fieldRef.hostView);
880
- if (idx >= 0)
881
- this.fieldSlot.detach(idx);
882
- this.fieldSlot = undefined;
883
- }
884
- /** Capture active element + selection before the hostView is detached from DOM. */
885
- captureFocusSnapshot() {
886
- if (!this.fieldRef)
887
- return;
888
- const hostEl = this.fieldRef.location.nativeElement;
889
- const active = document.activeElement;
890
- if (!(active instanceof HTMLElement) || !hostEl.contains(active))
891
- return;
892
- const sel = active;
893
- this.focusSnapshot = {
894
- element: active,
895
- selectionStart: typeof sel.selectionStart === 'number' ? sel.selectionStart : null,
896
- selectionEnd: typeof sel.selectionEnd === 'number' ? sel.selectionEnd : null,
897
- };
898
- }
899
- /** Re-apply the pre-detach focus + caret after the hostView is back in the DOM. */
900
- restoreFocusSnapshot() {
901
- const snap = this.focusSnapshot;
902
- this.focusSnapshot = undefined;
903
- if (!snap || !snap.element.isConnected)
904
- return;
905
- snap.element.focus();
906
- if (snap.selectionStart !== null && snap.selectionEnd !== null && 'setSelectionRange' in snap.element) {
907
- snap.element.setSelectionRange(snap.selectionStart, snap.selectionEnd);
908
- }
909
- }
910
- pushRawInputs(ref, rawInputs) {
911
- // setInput is lenient in Angular 21 — unknown keys silently drop (not NG0303). reflectComponentType does NOT include hostDirective forwarded inputs (angular/angular#49734), so a declared-input filter here would skip every value flowing through NgForgeFieldShell / NgForgeField / NgForgeAction. Mapper-as-contract is enforced by the *_INPUTS lockstep type assertions, not at runtime here.
912
- const last = this.lastPushedInputs;
913
- for (const [key, value] of Object.entries(rawInputs)) {
914
- if (last && last[key] === value)
915
- continue;
916
- ref.setInput(key, value);
917
- }
918
- this.lastPushedInputs = rawInputs;
932
+ // Push rawInputs to the innermost field on every change. The slot dedupes per-key, so the
933
+ // initial render's synchronous push from `renderInnermost` and the effect's first fire
934
+ // converge cleanly with no double-pushing.
935
+ explicitEffect([this.rawInputs], ([rawInputs]) => this.fieldComponent.pushInputs(rawInputs));
936
+ this.destroyRef.onDestroy(() => this.fieldComponent.destroyOnTeardown());
919
937
  }
920
938
  buildFieldInputs(rawInputs) {
921
939
  const fieldTreeCandidate = rawInputs['field'];
@@ -2748,16 +2766,25 @@ function evaluateCondition(expression, context) {
2748
2766
  case 'javascript':
2749
2767
  return evaluateJavaScriptExpression(expression, context);
2750
2768
  case 'custom': {
2751
- const customFn = context.customFunctions?.[expression.functionName];
2769
+ // Read both keys via cast — the XOR type narrows to `never` when both
2770
+ // would be truthy, but JSON-loaded configs can produce that state at
2771
+ // runtime. We accept it here, warn, and let inline win.
2772
+ const bothSet = expression;
2773
+ const inlineFn = bothSet.fn;
2774
+ const registeredName = bothSet.functionName;
2775
+ if (inlineFn && registeredName) {
2776
+ context.logger.warn('Both "fn" and "functionName" are set on custom condition. Inline "fn" takes precedence; "functionName" is ignored.');
2777
+ }
2778
+ const customFn = inlineFn ?? (registeredName ? context.customFunctions?.[registeredName] : undefined);
2752
2779
  if (!customFn) {
2753
- context.logger.error('Custom function not found:', expression.functionName);
2780
+ context.logger.error('Custom function not found:', registeredName);
2754
2781
  return false;
2755
2782
  }
2756
2783
  try {
2757
2784
  return !!customFn(context);
2758
2785
  }
2759
2786
  catch (error) {
2760
- context.logger.error('Error executing custom function:', expression.functionName, error);
2787
+ context.logger.error('Error executing custom function:', registeredName ?? '<inline>', error);
2761
2788
  return false;
2762
2789
  }
2763
2790
  }
@@ -2766,7 +2793,7 @@ function evaluateCondition(expression, context) {
2766
2793
  case 'or':
2767
2794
  return evaluateOrCondition(expression, context);
2768
2795
  case 'http':
2769
- context.logger.warn('[Dynamic Forms] HTTP conditions are resolved asynchronously via createHttpConditionLogicFunction(). ' +
2796
+ context.logger.warn('HTTP conditions are resolved asynchronously via createHttpConditionLogicFunction(). ' +
2770
2797
  'When used inside and/or composites, the HTTP result is not available synchronously. Returning false.');
2771
2798
  return false;
2772
2799
  case 'async':
@@ -4145,9 +4172,9 @@ class PageOrchestratorComponent {
4145
4172
  }
4146
4173
  }
4147
4174
  }
4148
- `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs').then(m => m.default)]] });
4175
+ `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs').then(m => m.default)]] });
4149
4176
  }
4150
- i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
4177
+ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
4151
4178
  type: Component,
4152
4179
  args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
4153
4180
  @for (pageField of pageFields(); track pageField.key; let i = $index) {
@@ -4579,6 +4606,85 @@ function isValidNonPagedForm(fields) {
4579
4606
  return result.mode === 'non-paged' && result.isValid;
4580
4607
  }
4581
4608
 
4609
+ function createSlot() {
4610
+ let counter = 0;
4611
+ const templates = new Map();
4612
+ const itemOrder = [];
4613
+ const primitiveFieldKey = signal(undefined, ...(ngDevMode ? [{ debugName: "primitiveFieldKey" }] : /* istanbul ignore next */ []));
4614
+ return {
4615
+ templates,
4616
+ itemOrder,
4617
+ primitiveFieldKey,
4618
+ nextId: () => `item-${counter++}`,
4619
+ setTemplate(itemId, t) {
4620
+ templates.set(itemId, t);
4621
+ },
4622
+ getTemplate(itemId) {
4623
+ return templates.get(itemId);
4624
+ },
4625
+ hasTemplate(itemId) {
4626
+ return templates.has(itemId);
4627
+ },
4628
+ setIdAt(index, itemId) {
4629
+ itemOrder[index] = itemId;
4630
+ },
4631
+ removeAt(index) {
4632
+ const [removed] = itemOrder.splice(index, 1);
4633
+ if (removed !== undefined)
4634
+ templates.delete(removed);
4635
+ },
4636
+ moveItem(fromIndex, toIndex) {
4637
+ const [id] = itemOrder.splice(fromIndex, 1);
4638
+ if (id !== undefined)
4639
+ itemOrder.splice(toIndex, 0, id);
4640
+ },
4641
+ truncate(length) {
4642
+ const removed = itemOrder.splice(length);
4643
+ for (const id of removed)
4644
+ templates.delete(id);
4645
+ },
4646
+ clear() {
4647
+ templates.clear();
4648
+ itemOrder.length = 0;
4649
+ counter = 0;
4650
+ primitiveFieldKey.set(undefined);
4651
+ },
4652
+ };
4653
+ }
4654
+ /**
4655
+ * Form-scoped registry of per-array durable state. One slot per array path
4656
+ * (e.g. 'members', 'team.members', 'orders.lineItems'). Survives
4657
+ * `ArrayFieldComponent` lifecycle, dies with the `DynamicForm`. Cleared on
4658
+ * schema swap by the `formSetup` effect in `FormStateManager`.
4659
+ *
4660
+ * `ArrayFieldComponent` injects this service and obtains its slot via
4661
+ * `slotFor(arrayPath)` on mount. Replaces the prior per-component
4662
+ * `ARRAY_TEMPLATE_REGISTRY` + `ARRAY_ITEM_ID_GENERATOR` providers.
4663
+ */
4664
+ class ArrayItemRegistryService {
4665
+ slots = new Map();
4666
+ /** Get-or-create a slot for the array at the given path. */
4667
+ slotFor(arrayPath) {
4668
+ let slot = this.slots.get(arrayPath);
4669
+ if (!slot) {
4670
+ slot = createSlot();
4671
+ this.slots.set(arrayPath, slot);
4672
+ }
4673
+ return slot;
4674
+ }
4675
+ /** Drop all slots — called from the `formSetup` effect on schema change. */
4676
+ clearAll() {
4677
+ for (const slot of this.slots.values())
4678
+ slot.clear();
4679
+ this.slots.clear();
4680
+ }
4681
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ArrayItemRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4682
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ArrayItemRegistryService });
4683
+ }
4684
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ArrayItemRegistryService, decorators: [{
4685
+ type: Injectable
4686
+ }] });
4687
+
4582
4688
  class SchemaRegistryService {
4583
4689
  registeredSchemas = new Map();
4584
4690
  /**
@@ -4628,7 +4734,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
4628
4734
  * Use this guard when you need type-safe access to `FunctionHttpValidatorConfig` properties.
4629
4735
  */
4630
4736
  function isFunctionHttpValidator(config) {
4631
- return 'functionName' in config && !!config.functionName;
4737
+ const c = config;
4738
+ return typeof c.functionName === 'string' || (c.fn !== null && typeof c.fn === 'object');
4632
4739
  }
4633
4740
 
4634
4741
  /**
@@ -4824,8 +4931,6 @@ class HttpConditionFunctionCacheService {
4824
4931
  * );
4825
4932
  * // user.value() keeps the old user data during loading transitions
4826
4933
  * ```
4827
- *
4828
- * @experimental Uses Angular's experimental resource composition APIs.
4829
4934
  */
4830
4935
  function withPreviousValue(input) {
4831
4936
  const derived = linkedSignal({ ...(ngDevMode ? { debugName: "derived" } : /* istanbul ignore next */ {}), source: input.snapshot,
@@ -5047,11 +5152,18 @@ function createAsyncConditionLogicFunction(condition) {
5047
5152
  const cacheService = inject(AsyncConditionFunctionCacheService);
5048
5153
  const pendingValue = condition.pendingValue ?? false;
5049
5154
  const debounceMs = condition.debounceMs ?? 300;
5050
- // Check function cache
5051
- const cacheKey = stableStringify(condition);
5052
- const cached = cacheService.asyncConditionFunctionCache.get(cacheKey);
5053
- if (cached) {
5054
- return cached;
5155
+ if (condition.asyncFn && condition.asyncFunctionName) {
5156
+ logger.warn('Both "asyncFn" and "asyncFunctionName" are set on async condition. Inline "asyncFn" takes precedence; "asyncFunctionName" is ignored.');
5157
+ }
5158
+ // Skip cross-condition caching when an inline function is used: stableStringify
5159
+ // cannot distinguish two different inline functions, which would cause cache hits
5160
+ // to wrongly reuse a LogicFn closing over a different function reference.
5161
+ const cacheKey = condition.asyncFn ? undefined : stableStringify(condition);
5162
+ if (cacheKey !== undefined) {
5163
+ const cached = cacheService.asyncConditionFunctionCache.get(cacheKey);
5164
+ if (cached) {
5165
+ return cached;
5166
+ }
5055
5167
  }
5056
5168
  // Each condition needs its own per-field signal store.
5057
5169
  const perFunctionSignalStore = new Map();
@@ -5076,8 +5188,10 @@ function createAsyncConditionLogicFunction(condition) {
5076
5188
  const asyncResource = rxResource({
5077
5189
  params: () => debouncedTrigger() ?? undefined,
5078
5190
  stream: ({ params: triggerKey }) => {
5079
- // Look up the async function at call time (fresh reference)
5080
- const asyncFn = functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName);
5191
+ // Inline `asyncFn` wins; otherwise look up the registered function at call time
5192
+ // (fresh reference, in case the registry was updated after LogicFn creation).
5193
+ const asyncFn = condition.asyncFn ??
5194
+ (condition.asyncFunctionName ? functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName) : undefined);
5081
5195
  if (!asyncFn) {
5082
5196
  logger.warn(`Async Condition - function '${condition.asyncFunctionName}' not found. ` +
5083
5197
  `Register it in customFnConfig.asyncConditions.`);
@@ -5129,7 +5243,9 @@ function createAsyncConditionLogicFunction(condition) {
5129
5243
  });
5130
5244
  return resultResource.value();
5131
5245
  };
5132
- cacheService.asyncConditionFunctionCache.set(cacheKey, fn);
5246
+ if (cacheKey !== undefined) {
5247
+ cacheService.asyncConditionFunctionCache.set(cacheKey, fn);
5248
+ }
5133
5249
  return fn;
5134
5250
  }
5135
5251
 
@@ -5764,12 +5880,12 @@ function applyCustomValidator(config, fieldPath) {
5764
5880
  if (config.expression) {
5765
5881
  validatorFn = createExpressionValidator(config);
5766
5882
  }
5767
- else if (config.functionName) {
5883
+ else if (config.fn || config.functionName) {
5768
5884
  validatorFn = createFunctionValidator(config);
5769
5885
  }
5770
5886
  else {
5771
5887
  const logger = inject(DynamicFormLogger);
5772
- logger.warn('Custom validator must have either "expression" or "functionName"');
5888
+ logger.warn('Custom validator must have one of "expression", "functionName", or "fn"');
5773
5889
  return;
5774
5890
  }
5775
5891
  const whenLogic = createConditionalLogic(config.when);
@@ -5781,6 +5897,13 @@ function applyCustomValidator(config, fieldPath) {
5781
5897
  }
5782
5898
  function createFunctionValidator(config) {
5783
5899
  const logger = inject(DynamicFormLogger);
5900
+ if (config.fn && config.functionName) {
5901
+ logger.warn('Both "fn" and "functionName" are set on custom validator. Inline "fn" takes precedence; "functionName" is ignored.');
5902
+ }
5903
+ if (config.fn) {
5904
+ const inlineFn = config.fn;
5905
+ return (ctx) => inlineFn(ctx, config.params);
5906
+ }
5784
5907
  const functionName = config.functionName;
5785
5908
  if (!functionName) {
5786
5909
  logger.warn('Custom validator missing functionName');
@@ -5841,10 +5964,24 @@ function createExpressionValidator(config) {
5841
5964
  * - onError: Optional handler for resource errors
5842
5965
  */
5843
5966
  function applyAsyncValidator(config, fieldPath) {
5844
- const registry = inject(FunctionRegistryService);
5845
- const validatorConfig = registry.getAsyncValidator(config.functionName);
5846
- if (!validatorConfig) {
5847
- throw new DynamicFormError(`Async validator "${config.functionName}" not found. Register it with customFnConfig.asyncValidators.`);
5967
+ if (config.fn && config.functionName) {
5968
+ const logger = inject(DynamicFormLogger);
5969
+ logger.warn('Both "fn" and "functionName" are set on async validator. Inline "fn" takes precedence; "functionName" is ignored.');
5970
+ }
5971
+ let validatorConfig;
5972
+ if (config.fn) {
5973
+ validatorConfig = config.fn;
5974
+ }
5975
+ else if (config.functionName) {
5976
+ const registry = inject(FunctionRegistryService);
5977
+ validatorConfig = registry.getAsyncValidator(config.functionName);
5978
+ if (!validatorConfig) {
5979
+ throw new DynamicFormError(`Async validator "${config.functionName}" not found. Register it with customFnConfig.asyncValidators.`);
5980
+ }
5981
+ }
5982
+ else {
5983
+ // XOR type rejects this at compile time; JSON-loaded configs can still reach here at runtime.
5984
+ throw new DynamicFormError('Async validator requires one of "functionName" or "fn".');
5848
5985
  }
5849
5986
  const whenLogic = createConditionalLogic(config.when);
5850
5987
  const asyncOptions = {
@@ -5866,10 +6003,16 @@ function applyAsyncValidator(config, fieldPath) {
5866
6003
  function applyUnifiedHttpValidator(config, fieldPath) {
5867
6004
  if (isFunctionHttpValidator(config)) {
5868
6005
  applyFunctionHttpValidator(config, fieldPath);
6006
+ return;
5869
6007
  }
5870
- else {
5871
- applyDeclarativeHttpValidator(config, fieldPath);
6008
+ // Declarative path requires both `http` and `responseMapping` — reject early
6009
+ // for JSON-loaded configs that have neither side fully populated. The
6010
+ // discriminated union forbids this at compile time.
6011
+ const declarative = config;
6012
+ if (!declarative.http || !declarative.responseMapping) {
6013
+ throw new DynamicFormError('HTTP validator requires one of "functionName" / "fn" (function-based) or "http" + "responseMapping" (declarative).');
5872
6014
  }
6015
+ applyDeclarativeHttpValidator(config, fieldPath);
5873
6016
  }
5874
6017
  /**
5875
6018
  * Apply function-based HTTP validator to field path using Angular's public validateHttp() API.
@@ -5880,10 +6023,24 @@ function applyUnifiedHttpValidator(config, fieldPath) {
5880
6023
  * - onError: Optional handler for HTTP errors
5881
6024
  */
5882
6025
  function applyFunctionHttpValidator(config, fieldPath) {
5883
- const registry = inject(FunctionRegistryService);
5884
- const httpValidatorConfig = registry.getHttpValidator(config.functionName);
5885
- if (!httpValidatorConfig) {
5886
- throw new DynamicFormError(`HTTP validator "${config.functionName}" not found. Register it with customFnConfig.httpValidators.`);
6026
+ if (config.fn && config.functionName) {
6027
+ const logger = inject(DynamicFormLogger);
6028
+ logger.warn('Both "fn" and "functionName" are set on HTTP validator. Inline "fn" takes precedence; "functionName" is ignored.');
6029
+ }
6030
+ let httpValidatorConfig;
6031
+ if (config.fn) {
6032
+ httpValidatorConfig = config.fn;
6033
+ }
6034
+ else if (config.functionName) {
6035
+ const registry = inject(FunctionRegistryService);
6036
+ httpValidatorConfig = registry.getHttpValidator(config.functionName);
6037
+ if (!httpValidatorConfig) {
6038
+ throw new DynamicFormError(`HTTP validator "${config.functionName}" not found. Register it with customFnConfig.httpValidators.`);
6039
+ }
6040
+ }
6041
+ else {
6042
+ // XOR type rejects this at compile time; JSON-loaded configs can still reach here at runtime.
6043
+ throw new DynamicFormError('HTTP validator requires one of "functionName" or "fn".');
5887
6044
  }
5888
6045
  const whenLogic = createConditionalLogic(config.when);
5889
6046
  const httpOptions = {
@@ -8467,22 +8624,31 @@ const PLACEHOLDER_INDEX = '$';
8467
8624
  /**
8468
8625
  * Builds a property override store key for a field.
8469
8626
  *
8470
- * For non-array fields, the key is just the field's key.
8471
- * For array item fields, the key includes the array path and either a concrete
8472
- * index (e.g., `contacts.0.email`) or the `$` placeholder (e.g., `contacts.$.email`).
8627
+ * For non-array fields, the key is the dot-joined group ancestors plus the
8628
+ * field's leaf key (e.g., `address.street` for `street` inside `address`).
8629
+ * For array item fields, the key includes the array path, either a concrete
8630
+ * index or the `$` placeholder, and any group ancestors INSIDE the array
8631
+ * item (e.g., `contacts.0.email`, `contacts.$.email`, or
8632
+ * `contacts.0.profile.email` for a group nested under each contact).
8633
+ *
8634
+ * `groupPath` matches the form-value path of nested groups (collector resets
8635
+ * it at array boundaries — see `property-derivation-collector.ts`), so the
8636
+ * resulting key always mirrors the field's location in `formValue`.
8473
8637
  *
8474
8638
  * @param arrayKey - The array field key (e.g., 'contacts'), or undefined for non-array fields
8475
8639
  * @param index - The array item index, `PLACEHOLDER_INDEX` for the wildcard format, or undefined for non-array fields
8476
8640
  * @param fieldKey - The leaf field key (e.g., 'email')
8477
- * @returns The store key (e.g., 'email', 'contacts.0.email', or 'contacts.$.email')
8641
+ * @param groupPath - Dot-joined group ancestors scoped to the current array item (or top-level), or undefined when not inside any group
8642
+ * @returns The store key (e.g., 'email', 'address.street', 'contacts.0.email', 'contacts.$.profile.email')
8478
8643
  *
8479
8644
  * @public
8480
8645
  */
8481
- function buildPropertyOverrideKey(arrayKey, index, fieldKey) {
8646
+ function buildPropertyOverrideKey(arrayKey, index, fieldKey, groupPath) {
8647
+ const leafScope = groupPath ? `${groupPath}.${fieldKey}` : fieldKey;
8482
8648
  if (arrayKey != null && index != null) {
8483
- return `${arrayKey}.${index}.${fieldKey}`;
8649
+ return `${arrayKey}.${index}.${leafScope}`;
8484
8650
  }
8485
- return fieldKey;
8651
+ return leafScope;
8486
8652
  }
8487
8653
 
8488
8654
  /**
@@ -8563,6 +8729,25 @@ function applyIndexSuffix(inputs, index) {
8563
8729
  key: `${key}_${index}`,
8564
8730
  };
8565
8731
  }
8732
+ /**
8733
+ * Prefixes the key with the underscored group ancestor path so that the same
8734
+ * leaf key inside different groups produces distinct DOM IDs (issue #401).
8735
+ * Form-schema keys remain clean — only the rendered `key` input is rewritten.
8736
+ *
8737
+ * Dots in the group path become underscores, then the original leaf key is
8738
+ * appended: `(groupPath='user.address', inputs.key='street')` →
8739
+ * `inputs.key = 'user_address_street'`.
8740
+ */
8741
+ function applyGroupPrefix(inputs, groupPath) {
8742
+ const key = inputs['key'];
8743
+ if (typeof key !== 'string' || groupPath.length === 0) {
8744
+ return inputs;
8745
+ }
8746
+ return {
8747
+ ...inputs,
8748
+ key: `${groupPath.replace(/\./g, '_')}_${key}`,
8749
+ };
8750
+ }
8566
8751
  /**
8567
8752
  * Main field mapper function that uses the field registry to get the appropriate mapper
8568
8753
  * based on the field's type property.
@@ -8599,6 +8784,10 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
8599
8784
  // Check if we're inside an array item context - if so, we need to suffix keys
8600
8785
  // Optional because ARRAY_CONTEXT is only provided inside array item injectors
8601
8786
  const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
8787
+ // Check if we're inside a group context - if so, prefix the key with the group path
8788
+ // so overlapping leaf keys across groups produce distinct DOM IDs (issue #401).
8789
+ // Optional because GROUP_CONTEXT is only provided by GroupFieldComponent.
8790
+ const groupContext = inject(GROUP_CONTEXT, { optional: true });
8602
8791
  // Inject the property override store for property derivation overrides
8603
8792
  // Always available — provided at the DynamicForm component level via provideDynamicFormDI
8604
8793
  const store = inject(PROPERTY_OVERRIDE_STORE);
@@ -8609,6 +8798,7 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
8609
8798
  // Check that arrayContext exists and has a valid index signal (guards against mock injectors)
8610
8799
  const indexSignal = arrayContext?.index;
8611
8800
  const hasArrayContext = indexSignal !== undefined && isSignal(indexSignal);
8801
+ const hasGroupContext = groupContext != null;
8612
8802
  // Fast-path check for property overrides: only fields whose definition declares
8613
8803
  // a property-derivation enter the computed() wrapper. We inspect the FieldDef
8614
8804
  // directly rather than asking the store, because the store's registration
@@ -8617,7 +8807,7 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
8617
8807
  // collector apply the same inclusion rule, so the answers always agree.
8618
8808
  const hasOverrides = fieldHasPropertyDerivations(fieldDef);
8619
8809
  // Fast path: no transformations needed
8620
- if (!hasPropsForwarding && !hasArrayContext && !hasOverrides) {
8810
+ if (!hasPropsForwarding && !hasArrayContext && !hasGroupContext && !hasOverrides) {
8621
8811
  return mapperResult;
8622
8812
  }
8623
8813
  // Wrap in computed to apply transformations
@@ -8627,18 +8817,34 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
8627
8817
  if (hasPropsForwarding) {
8628
8818
  inputs = mergeForwardedPropsToMeta(inputs, propsToMeta);
8629
8819
  }
8820
+ // Apply group prefix BEFORE array suffix so the resulting key reads as
8821
+ // `{groupPath}_{key}_{index}` (e.g. `address_street_0`) — matching the
8822
+ // form-value path order: group → leaf → array index.
8823
+ if (hasGroupContext) {
8824
+ // Non-null after the hasGroupContext guard.
8825
+ inputs = applyGroupPrefix(inputs, groupContext.groupPath());
8826
+ }
8630
8827
  // Apply index suffix for array items
8631
8828
  if (hasArrayContext) {
8632
8829
  const index = indexSignal();
8633
8830
  inputs = applyIndexSuffix(inputs, index);
8634
8831
  }
8635
- // Apply property overrides from the store (AFTER all static transformations)
8832
+ // Apply property overrides from the store (AFTER all static transformations).
8833
+ //
8834
+ // INVARIANT: the store key MUST be built from `fieldDef.key` + the group/array
8835
+ // ancestry (via buildPropertyOverrideKey) — NEVER from the DOM-scoped
8836
+ // `inputs['key']` produced by applyGroupPrefix/applyIndexSuffix above. The
8837
+ // collector (property-derivation-collector.ts) registers using the same
8838
+ // shape; if these drift, overrides silently miss their target.
8636
8839
  if (hasOverrides) {
8637
- // Build the store key inside computed() so index signal read establishes reactive dependency
8638
- // Safe to access arrayContext/indexSignal directly — hasArrayContext already confirmed they exist
8840
+ // Build inside computed() so the index/group signals establish reactive deps.
8841
+ // GROUP_CONTEXT resets at array boundaries (see create-array-item-injector.ts),
8842
+ // so groupPath() represents ancestors inside the current array item (or
8843
+ // top-level groups when not in an array) — matching the collector.
8844
+ const groupPath = hasGroupContext ? groupContext.groupPath() : undefined;
8639
8845
  const key = hasArrayContext
8640
- ? buildPropertyOverrideKey(arrayContext.arrayKey, indexSignal(), fieldDef.key)
8641
- : fieldDef.key;
8846
+ ? buildPropertyOverrideKey(arrayContext.arrayKey, indexSignal(), fieldDef.key, groupPath || undefined)
8847
+ : buildPropertyOverrideKey(undefined, undefined, fieldDef.key, groupPath || undefined);
8642
8848
  const overrides = store.getOverrides(key)();
8643
8849
  inputs = applyPropertyOverrides(inputs, overrides);
8644
8850
  }
@@ -8657,6 +8863,40 @@ function createRenderReadySignal(inputs, definition) {
8657
8863
  return requiredInputs.every((inputName) => currentInputs[inputName] !== undefined);
8658
8864
  });
8659
8865
  }
8866
+ /**
8867
+ * Reads the current hidden state from mapper inputs. Uniform across all field
8868
+ * types — value-bearing leaves source from the `FieldTree`'s `state.hidden()`
8869
+ * (where Angular Signal Forms cascades `hidden(path, …)`), and non-field outputs
8870
+ * (button/text/containers) source from the mapper's explicit `inputs.hidden`.
8871
+ *
8872
+ * The `@if (!field.hidden())` gate at every `*dfFieldOutlet` consumption site uses
8873
+ * this signal, removing hidden fields from the DOM. That's required to silence
8874
+ * Angular's `NG01916: Field 'X' is hidden but is being rendered` warning — and
8875
+ * is the prescribed pattern from the
8876
+ * [`hidden` API docs](https://angular.dev/api/forms/signals/hidden).
8877
+ *
8878
+ * Container internal state (e.g. an `ArrayFieldComponent`'s items + dynamic
8879
+ * templates + id counter) survives this destroy/recreate via the form-scoped
8880
+ * `ArrayItemRegistryService` keyed by array path.
8881
+ */
8882
+ function createHiddenSignal(inputs) {
8883
+ return computed(() => {
8884
+ const currentInputs = inputs();
8885
+ const fieldCandidate = currentInputs['field'];
8886
+ // Value-bearing leaves expose `field` as a callable `FieldTree` whose invocation returns a
8887
+ // `FieldState` with a `hidden` accessor. `isSignal()` doesn't recognise the Proxy-wrapped
8888
+ // FieldTree (no SIGNAL symbol), so we duck-type the shape and call `state.hidden()` directly.
8889
+ if (typeof fieldCandidate === 'function') {
8890
+ const state = fieldCandidate();
8891
+ if (state && typeof state.hidden === 'function') {
8892
+ return state.hidden() === true;
8893
+ }
8894
+ }
8895
+ // Non-value fields (button, text, containers) carry an explicit `hidden` input set by
8896
+ // `applyHiddenLogic` based on the mapper-side evaluation.
8897
+ return currentInputs['hidden'] === true;
8898
+ });
8899
+ }
8660
8900
  /**
8661
8901
  * Resolves a single field definition to a ResolvedField using RxJS.
8662
8902
  * Loads the component asynchronously and maps inputs in the injection context.
@@ -8693,6 +8933,7 @@ function resolveField(fieldDef, context) {
8693
8933
  injector: context.injector,
8694
8934
  inputs,
8695
8935
  renderReady: createRenderReadySignal(inputs, definition),
8936
+ hidden: createHiddenSignal(inputs),
8696
8937
  };
8697
8938
  }), catchError((error) => {
8698
8939
  // Only call onError if component is not destroyed to avoid accessing cleaned-up state
@@ -8729,6 +8970,7 @@ function resolveFieldSync(fieldDef, context) {
8729
8970
  injector: context.injector,
8730
8971
  inputs,
8731
8972
  renderReady: createRenderReadySignal(inputs, definition),
8973
+ hidden: createHiddenSignal(inputs),
8732
8974
  };
8733
8975
  }
8734
8976
  /**
@@ -8755,13 +8997,14 @@ function reconcileFields(prev, curr) {
8755
8997
  }
8756
8998
  // Same component instance + injector, but the field definition changed.
8757
8999
  // Preserve component identity (so ngComponentOutlet keeps the same instance)
8758
- // while swapping in the freshly-mapped inputs/renderReady signals so prop
8759
- // updates (e.g. select options) reach the component.
9000
+ // while swapping in the freshly-mapped inputs/renderReady/hidden signals
9001
+ // so prop updates (e.g. select options) reach the component.
8760
9002
  return {
8761
9003
  ...existing,
8762
9004
  fieldDef: field.fieldDef,
8763
9005
  inputs: field.inputs,
8764
9006
  renderReady: field.renderReady,
9007
+ hidden: field.hidden,
8765
9008
  };
8766
9009
  }
8767
9010
  // New field, type changed, or context changed (new injector) - use new field
@@ -9079,15 +9322,32 @@ function injectFieldRegistry() {
9079
9322
  * Collects all field keys, types, and validates regex patterns from a field tree
9080
9323
  * by recursively traversing containers (page, row, group, array).
9081
9324
  *
9082
- * @param collectKeys - Whether to add field keys to data.keys for duplicate detection.
9083
- * Set to false when inside an array container: array item fields share template keys
9084
- * across items by design (e.g. every item has 'name', 'email'), so they must not
9085
- * participate in global duplicate-key checking.
9325
+ * Keys are scoped by their group ancestors (e.g. `address.city` instead of `city`),
9326
+ * so the same leaf key can appear inside different groups without conflict. Page and
9327
+ * row containers do not contribute to the path because they flatten their children
9328
+ * into the parent scope. Array items are excluded entirely from key collection
9329
+ * because they share template keys across items by design.
9330
+ *
9331
+ * Fields whose registered `valueHandling` is anything other than `include` (e.g.
9332
+ * buttons with `exclude`, page/row with `flatten`) are not collected: they don't
9333
+ * bind to a form-value path, so duplicate-key uniqueness isn't meaningful for them.
9334
+ *
9335
+ * @param collectKeys - When false, the entire subtree skips key collection (used
9336
+ * when descending into an array container).
9086
9337
  */
9087
- function collectFieldData(fields, data, collectKeys = true) {
9338
+ function collectFieldData(fields, data, registry, pathPrefix, collectKeys = true) {
9088
9339
  for (const field of fields) {
9089
- if (collectKeys && field.key) {
9090
- data.keys.push(field.key);
9340
+ // Defensively skip only known non-include modes — unexpected/missing
9341
+ // valueHandling defaults back to participating in duplicate detection so a
9342
+ // mis-registered field type doesn't silently drop from collision checks.
9343
+ const valueHandling = field.type ? getFieldValueHandling(field.type, registry) : 'include';
9344
+ const participatesInValue = valueHandling !== 'exclude' && valueHandling !== 'flatten';
9345
+ if (collectKeys && field.key && participatesInValue) {
9346
+ const scopedKey = pathPrefix ? `${pathPrefix}.${field.key}` : field.key;
9347
+ data.keys.push(scopedKey);
9348
+ // Project to the DOM-ID format (dots → underscores) so we can detect
9349
+ // collisions like top-level `'foo_bar'` vs group `'foo'` + leaf `'bar'`.
9350
+ data.domIds.push(scopedKey.replace(/\./g, '_'));
9091
9351
  }
9092
9352
  if (field.type) {
9093
9353
  data.types.add(field.type);
@@ -9107,13 +9367,16 @@ function collectFieldData(fields, data, collectKeys = true) {
9107
9367
  // Array item fields share template keys across items by design — stop collecting keys
9108
9368
  // once inside an array. Types and regex patterns are still validated.
9109
9369
  const childCollectKeys = collectKeys && field.type !== 'array';
9370
+ // Only group containers extend the scoping path — page/row flatten into the parent
9371
+ // scope, and array items use dynamic indices that aren't statically knowable.
9372
+ const childPrefix = isGroupField(field) && field.key ? (pathPrefix ? `${pathPrefix}.${field.key}` : field.key) : pathPrefix;
9110
9373
  const children = normalizeFieldsArray(field.fields);
9111
9374
  for (const child of children) {
9112
9375
  if (Array.isArray(child)) {
9113
- collectFieldData(child, data, childCollectKeys);
9376
+ collectFieldData(child, data, registry, childPrefix, childCollectKeys);
9114
9377
  }
9115
9378
  else {
9116
- collectFieldData([child], data, childCollectKeys);
9379
+ collectFieldData([child], data, registry, childPrefix, childCollectKeys);
9117
9380
  }
9118
9381
  }
9119
9382
  }
@@ -9149,7 +9412,38 @@ function validateNoDuplicateKeys(allKeys) {
9149
9412
  const duplicateList = Array.from(duplicates)
9150
9413
  .map((k) => `'${k}'`)
9151
9414
  .join(', ');
9152
- throw new DynamicFormError(`Duplicate field keys detected: ${duplicateList}. Each field key must be unique within a form config.`);
9415
+ throw new DynamicFormError(`Duplicate field keys detected: ${duplicateList}. Field keys must be unique within their group scope (the same key may appear inside different groups).`);
9416
+ }
9417
+ }
9418
+ /**
9419
+ * Validates that the underscore-projected DOM-ID namespace has no collisions.
9420
+ *
9421
+ * DOM IDs are built by underscore-joining the group ancestor path with the
9422
+ * leaf key (e.g. `address_street`). Two scoped paths can land on the same
9423
+ * DOM ID even when their form-value paths differ — e.g. a top-level key
9424
+ * `'foo_bar'` and a leaf `'bar'` inside group `'foo'` both render as
9425
+ * `id="foo_bar"`. The form-value-path check accepts this (paths are
9426
+ * `'foo_bar'` vs `'foo.bar'` — distinct) but the rendered DOM would have
9427
+ * duplicate IDs, breaking aria associations and CSS selectors.
9428
+ *
9429
+ * @throws {DynamicFormError} When two scoped paths project to the same DOM ID
9430
+ */
9431
+ function validateNoDomIdCollisions(domIds) {
9432
+ const seen = new Set();
9433
+ const duplicates = new Set();
9434
+ for (const id of domIds) {
9435
+ if (seen.has(id)) {
9436
+ duplicates.add(id);
9437
+ }
9438
+ seen.add(id);
9439
+ }
9440
+ if (duplicates.size > 0) {
9441
+ const duplicateList = Array.from(duplicates)
9442
+ .map((k) => `'${k}'`)
9443
+ .join(', ');
9444
+ throw new DynamicFormError(`DOM id collision detected for: ${duplicateList}. Group-nested keys are joined with '_' to form DOM ids ` +
9445
+ `(e.g. group 'foo' + leaf 'bar' renders as id='foo_bar'). A separate top-level key like 'foo_bar' or a different ` +
9446
+ `group/leaf pair that underscores to the same string will produce duplicate ids. Rename one of the colliding keys.`);
9153
9447
  }
9154
9448
  }
9155
9449
  /**
@@ -9187,11 +9481,13 @@ function validateFieldTypesRegistered(allTypes, registry, logger) {
9187
9481
  function validateFormConfig(fields, registry, logger) {
9188
9482
  const data = {
9189
9483
  keys: [],
9484
+ domIds: [],
9190
9485
  types: new Set(),
9191
9486
  regexErrors: [],
9192
9487
  };
9193
- collectFieldData(fields, data);
9488
+ collectFieldData(fields, data, registry, '');
9194
9489
  validateNoDuplicateKeys(data.keys);
9490
+ validateNoDomIdCollisions(data.domIds);
9195
9491
  validateFieldTypesRegistered(data.types, registry, logger);
9196
9492
  if (data.regexErrors.length > 0) {
9197
9493
  throw new DynamicFormError(data.regexErrors.length === 1
@@ -9893,6 +10189,12 @@ class FormStateManager {
9893
10189
  eventBus = inject(EventBus);
9894
10190
  functionRegistry = inject(FunctionRegistryService);
9895
10191
  schemaRegistry = inject(SchemaRegistryService);
10192
+ // Optional so unit-test setups that drive FormStateManager directly without provideDynamicFormDI()
10193
+ // (e.g. mocked RootFormRegistryService specs) don't have to manually wire the registry. Real
10194
+ // DynamicForm usage always provides it via coreProviders; if you hit the dev warning below in
10195
+ // your own setup, you're using DynamicForm without provideDynamicFormDI() — fix the providers
10196
+ // chain rather than ignoring it.
10197
+ arrayItemRegistry = inject(ArrayItemRegistryService, { optional: true });
9896
10198
  /** Host component dependencies (config, formOptions, value). */
9897
10199
  deps = (() => {
9898
10200
  const raw = inject(FORM_STATE_DEPS);
@@ -10519,12 +10821,24 @@ class FormStateManager {
10519
10821
  }
10520
10822
  });
10521
10823
  // Schema change resets the saved store + transition tracker so values from a stale schema
10522
- // can't leak into the new one.
10824
+ // can't leak into the new one. Also drops every per-array slot in the registry so a fresh
10825
+ // schema starts with an empty template/itemOrder/id-counter state.
10826
+ let warnedAboutMissingArrayRegistry = false;
10523
10827
  explicitEffect([this.formSetup], () => {
10524
10828
  this.prevFieldStateSnapshot.set({});
10525
10829
  if (Object.keys(this.excludedValueStore()).length > 0) {
10526
10830
  this.excludedValueStore.set({});
10527
10831
  }
10832
+ if (this.arrayItemRegistry) {
10833
+ this.arrayItemRegistry.clearAll();
10834
+ }
10835
+ else if (DEV_MODE && !warnedAboutMissingArrayRegistry) {
10836
+ warnedAboutMissingArrayRegistry = true;
10837
+ this.logger.warn('ArrayItemRegistryService was not provided to FormStateManager. ' +
10838
+ 'Array fields will lose dynamically-added item templates across hide/show cycles. ' +
10839
+ 'This usually means DynamicForm was bootstrapped without provideDynamicFormDI() — ' +
10840
+ 'check your providers chain.');
10841
+ }
10528
10842
  });
10529
10843
  // Outward sync uses boundFormValue (honors only explicit excludeValueIf* opt-ins) so
10530
10844
  // global defaults that target submission output don't reshape the host's bound model.
@@ -11036,15 +11350,18 @@ function computeValueFromEntry(entry, evalContext, options) {
11036
11350
  if (entry.expression) {
11037
11351
  return ExpressionParser.evaluate(entry.expression, evalContext);
11038
11352
  }
11039
- if (entry.functionName) {
11040
- const fn = options.derivationFunctions?.[entry.functionName];
11353
+ if (entry.fn || entry.functionName) {
11354
+ if (entry.fn && entry.functionName) {
11355
+ evalContext.logger.warn(`Both "fn" and "functionName" are set on derivation for '${options.subject}'. Inline "fn" takes precedence; "functionName" is ignored.`);
11356
+ }
11357
+ const fn = entry.fn ?? (entry.functionName ? options.derivationFunctions?.[entry.functionName] : undefined);
11041
11358
  if (!fn) {
11042
11359
  const kind = options.functionKind ?? 'Derivation function';
11043
11360
  throw new DynamicFormError(`${kind} '${entry.functionName}' not found in customFnConfig.derivations`);
11044
11361
  }
11045
11362
  return fn(evalContext);
11046
11363
  }
11047
- throw new DynamicFormError(`Derivation for ${options.subject} has no value source. Specify 'value', 'expression', or 'functionName'.`);
11364
+ throw new DynamicFormError(`Derivation for ${options.subject} has no value source. Specify 'value', 'expression', 'functionName', or 'fn'.`);
11048
11365
  }
11049
11366
 
11050
11367
  /**
@@ -11447,7 +11764,7 @@ function getEntriesForChangedFields$1(entries, changedFields) {
11447
11764
  function tryApplyDerivation(entry, context, chainContext) {
11448
11765
  const { derivationLogger } = context;
11449
11766
  // HTTP and async entries are processed asynchronously in their own streams — skip in sync loop
11450
- if (entry.http || entry.asyncFunctionName) {
11767
+ if (entry.http || entry.asyncFunctionName || entry.asyncFn) {
11451
11768
  return { applied: false, fieldKey: entry.fieldKey };
11452
11769
  }
11453
11770
  // Check if this is an array field derivation (has '$' placeholder)
@@ -11754,9 +12071,11 @@ function formatDerivationError(entry, phase, message) {
11754
12071
  ? `expression: "${entry.expression.substring(0, 50)}${entry.expression.length > 50 ? '...' : ''}"`
11755
12072
  : entry.functionName
11756
12073
  ? `function: "${entry.functionName}"`
11757
- : entry.value !== undefined
11758
- ? 'static value'
11759
- : 'unknown source';
12074
+ : entry.fn
12075
+ ? 'inline fn'
12076
+ : entry.value !== undefined
12077
+ ? 'static value'
12078
+ : 'unknown source';
11760
12079
  return `${ERROR_PREFIX$1} Failed to ${phase} derivation (field: ${entry.fieldKey}, ${source}): ${message}`;
11761
12080
  }
11762
12081
  /**
@@ -11825,7 +12144,7 @@ function shouldSkipForUserOverride(entry, resolvedFieldKey, context, chainContex
11825
12144
  function applyDerivationsForTrigger(collection, trigger, context, changedFields) {
11826
12145
  // Filter entries by trigger type, excluding HTTP and async entries (processed in async streams)
11827
12146
  const filteredEntries = collection.entries.filter((entry) => {
11828
- if (entry.http || entry.asyncFunctionName)
12147
+ if (entry.http || entry.asyncFunctionName || entry.asyncFn)
11829
12148
  return false;
11830
12149
  if (trigger === 'onChange') {
11831
12150
  return !entry.trigger || entry.trigger === 'onChange';
@@ -11994,7 +12313,7 @@ function topologicalSort(entries) {
11994
12313
  * Combines:
11995
12314
  * - Explicit `dependsOn` (when provided, it takes precedence over auto-detection)
11996
12315
  * - Expression-detected dependencies (only used when `dependsOn` is empty)
11997
- * - `'*'` wildcard for `functionName` (only when `dependsOn` is empty)
12316
+ * - `'*'` wildcard for `functionName` / inline `fn` (only when `dependsOn` is empty)
11998
12317
  * - Condition expression dependencies (always included)
11999
12318
  *
12000
12319
  * @internal
@@ -12008,7 +12327,9 @@ function extractDependenciesFromConfig(config) {
12008
12327
  if (config.expression) {
12009
12328
  extractStringDependencies(config.expression).forEach((dep) => deps.add(dep));
12010
12329
  }
12011
- if (config.functionName) {
12330
+ // Inline `fn` mirrors `functionName` semantics: without explicit dependsOn,
12331
+ // assume the function may depend on anything in the form.
12332
+ if (config.functionName || config.fn) {
12012
12333
  deps.add('*');
12013
12334
  }
12014
12335
  }
@@ -12018,6 +12339,32 @@ function extractDependenciesFromConfig(config) {
12018
12339
  return Array.from(deps);
12019
12340
  }
12020
12341
 
12342
+ /**
12343
+ * Reads the optional inline `fn` property off a derivation config.
12344
+ *
12345
+ * `fn` only appears on the function-mode branch of the discriminated union, so
12346
+ * call sites that need to copy it through unconditionally must widen via cast.
12347
+ * This helper centralises the cast and keeps it read-only — no widening, no
12348
+ * mutation. Returns `undefined` for any non-function-mode variant.
12349
+ *
12350
+ * @internal
12351
+ */
12352
+ function extractInlineFn(config) {
12353
+ return config.fn;
12354
+ }
12355
+ /**
12356
+ * Reads the optional inline `asyncFn` property off a derivation config.
12357
+ *
12358
+ * Same rationale as {@link extractInlineFn}: `asyncFn` only appears on the
12359
+ * `source: 'asyncFunction'` branch, so collectors that copy it through need a
12360
+ * cast. Read-only access, no widening.
12361
+ *
12362
+ * @internal
12363
+ */
12364
+ function extractInlineAsyncFn(config) {
12365
+ return config.asyncFn;
12366
+ }
12367
+
12021
12368
  /**
12022
12369
  * Recursively walks a field-definition tree, invoking `visitor` for each field.
12023
12370
  *
@@ -12360,9 +12707,11 @@ function createLogicEntry(fieldKey, config, context) {
12360
12707
  value: config.value,
12361
12708
  expression: config.expression,
12362
12709
  functionName: config.functionName,
12710
+ fn: extractInlineFn(config),
12363
12711
  http: config.http,
12364
12712
  responseExpression: config.responseExpression,
12365
12713
  asyncFunctionName: config.asyncFunctionName,
12714
+ asyncFn: extractInlineAsyncFn(config),
12366
12715
  trigger,
12367
12716
  debounceMs,
12368
12717
  isShorthand: false,
@@ -12911,9 +13260,16 @@ function createAsyncDerivationStream(entry, formValue$, context) {
12911
13260
  `Array async derivations will be supported in a future release.`);
12912
13261
  return EMPTY;
12913
13262
  }
12914
- if (!entry.asyncFunctionName) {
13263
+ if (!entry.asyncFunctionName && !entry.asyncFn) {
13264
+ context.logger.warn(`${LOG_PREFIX} Derivation for '${entry.fieldKey}' has neither "asyncFunctionName" nor "asyncFn". ` +
13265
+ `One of the two is required for source: 'asyncFunction'.`);
12915
13266
  return EMPTY;
12916
13267
  }
13268
+ if (entry.asyncFn && entry.asyncFunctionName) {
13269
+ context.logger.warn(`${LOG_PREFIX} Both "asyncFn" and "asyncFunctionName" are set on derivation for '${entry.fieldKey}'. ` +
13270
+ `Inline "asyncFn" takes precedence; "asyncFunctionName" is ignored.`);
13271
+ }
13272
+ const inlineAsyncFn = entry.asyncFn;
12917
13273
  const asyncFunctionName = entry.asyncFunctionName;
12918
13274
  const debounceMs = entry.debounceMs ?? DEFAULT_ASYNC_DEBOUNCE_MS;
12919
13275
  // startWith(null) → pairwise() causes all fields to appear "changed" on the
@@ -12959,8 +13315,8 @@ function createAsyncDerivationStream(entry, formValue$, context) {
12959
13315
  const customFunctions = context.customFunctions?.();
12960
13316
  const asyncDerivationFunctions = context.asyncDerivationFunctions?.();
12961
13317
  const externalData = context.externalData?.();
12962
- // Look up the async function
12963
- const asyncFn = asyncDerivationFunctions?.[asyncFunctionName];
13318
+ // Inline `asyncFn` wins; otherwise look up the registered function.
13319
+ const asyncFn = inlineAsyncFn ?? (asyncFunctionName ? asyncDerivationFunctions?.[asyncFunctionName] : undefined);
12964
13320
  if (!asyncFn) {
12965
13321
  context.logger.warn(`${LOG_PREFIX} Async derivation function '${asyncFunctionName}' not found for field '${entry.fieldKey}'. ` +
12966
13322
  `Register it in customFnConfig.asyncDerivations.`);
@@ -13388,7 +13744,7 @@ class DerivationOrchestrator {
13388
13744
  toObservable(this.derivationCollection, { injector: this.injector })
13389
13745
  .pipe(takeUntilDestroyed(this.destroyRef))
13390
13746
  .subscribe((collection) => {
13391
- const asyncEntries = collection?.entries.filter((entry) => entry.asyncFunctionName) ?? [];
13747
+ const asyncEntries = collection?.entries.filter((entry) => entry.asyncFunctionName || entry.asyncFn) ?? [];
13392
13748
  if (asyncEntries.length === 0) {
13393
13749
  this.teardownAsyncFunctionStreams();
13394
13750
  this.lastAsyncEntryKeys = null;
@@ -13435,9 +13791,14 @@ class DerivationOrchestrator {
13435
13791
  * Used for smart teardown comparison.
13436
13792
  */
13437
13793
  computeAsyncEntryKeys(entries) {
13794
+ // For inline `asyncFn` entries we tag the key with the field path (the real
13795
+ // identity, since derivations are self-targeting) rather than the array
13796
+ // index. Index-based tagging churned identities whenever topologicalSort
13797
+ // reordered entries, causing unnecessary teardown + re-subscribe.
13438
13798
  return new Set(entries.map((entry) => {
13439
13799
  const config = {
13440
13800
  asyncFunctionName: entry.asyncFunctionName,
13801
+ asyncFnId: entry.asyncFn ? `inline:${entry.fieldKey}` : undefined,
13441
13802
  dependsOn: entry.dependsOn,
13442
13803
  debounceMs: entry.debounceMs,
13443
13804
  stopOnUserOverride: entry.stopOnUserOverride,
@@ -13486,10 +13847,11 @@ function warnAboutWildcardDependencies$1(logger, entries, fieldCount) {
13486
13847
  // HTTP and async entries require explicit dependsOn (validated at collection time)
13487
13848
  const implicitWildcards = wildcardEntries.filter((entry) => !entry.http &&
13488
13849
  !entry.asyncFunctionName &&
13489
- entry.functionName &&
13850
+ !entry.asyncFn &&
13851
+ (entry.functionName || entry.fn) &&
13490
13852
  (!entry.originalConfig?.dependsOn || entry.originalConfig.dependsOn.length === 0));
13491
13853
  if (implicitWildcards.length > 0) {
13492
- const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey} (${e.functionName})`);
13854
+ const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey} (${e.functionName ?? '<inline fn>'})`);
13493
13855
  logger.warn('Derivation: custom functions without explicit dependsOn detected. ' +
13494
13856
  `These run on EVERY form change, which may impact performance (form has ${fieldCount} fields). ` +
13495
13857
  'Consider specifying explicit dependsOn arrays for better performance.', derivationDescs);
@@ -13972,8 +14334,16 @@ function collectPropertyDerivations(fields, logger, tracker) {
13972
14334
  const entries = [];
13973
14335
  const context = { logger, tracker };
13974
14336
  traverseFieldsWithContext(fields, context, (field, ctx) => collectFromField(field, entries, ctx), {
13975
- onArrayChild: (_, field) => ({ arrayPath: field.key }),
13976
- // Group and layout containers do not affect property-derivation paths.
14337
+ // Array boundary: each item is its own model root. Reset groupPath so
14338
+ // ancestor groups OUTSIDE the array don't bleed into per-item keys.
14339
+ onArrayChild: (_, field) => ({ arrayPath: field.key, groupPath: undefined }),
14340
+ onGroupChild: (parent, field) => {
14341
+ // Keyless groups (rare) act like layout containers — pass through.
14342
+ if (!field.key)
14343
+ return {};
14344
+ return { groupPath: parent.groupPath ? `${parent.groupPath}.${field.key}` : field.key };
14345
+ },
14346
+ // Layout containers (page, row, container) leave context unchanged.
13977
14347
  });
13978
14348
  return { entries };
13979
14349
  }
@@ -14005,7 +14375,7 @@ function collectFromField(field, entries, context) {
14005
14375
  * @internal
14006
14376
  */
14007
14377
  function createPropertyDerivationEntryFromDerivation(fieldKey, config, context) {
14008
- const effectiveFieldKey = buildPropertyOverrideKey(context.arrayPath, context.arrayPath ? PLACEHOLDER_INDEX : undefined, fieldKey);
14378
+ const effectiveFieldKey = buildPropertyOverrideKey(context.arrayPath, context.arrayPath ? PLACEHOLDER_INDEX : undefined, fieldKey, context.groupPath);
14009
14379
  const dependsOn = extractDependenciesFromConfig(config);
14010
14380
  const condition = config.condition ?? true;
14011
14381
  const trigger = config.trigger ?? 'onChange';
@@ -14018,6 +14388,7 @@ function createPropertyDerivationEntryFromDerivation(fieldKey, config, context)
14018
14388
  value: config.value,
14019
14389
  expression: config.expression,
14020
14390
  functionName: config.functionName,
14391
+ fn: extractInlineFn(config),
14021
14392
  trigger,
14022
14393
  debounceMs,
14023
14394
  debugName: config.debugName,
@@ -14172,10 +14543,10 @@ const PROPERTY_DERIVATION_ORCHESTRATOR = new InjectionToken('PROPERTY_DERIVATION
14172
14543
  // ─────────────────────────────────────────────────────────────────────────────
14173
14544
  function warnAboutWildcardDependencies(logger, entries, fieldCount) {
14174
14545
  const implicitWildcards = entries.filter((entry) => entry.dependsOn.includes('*') &&
14175
- entry.functionName &&
14546
+ (entry.functionName || entry.fn) &&
14176
14547
  (!entry.originalConfig?.dependsOn || entry.originalConfig.dependsOn.length === 0));
14177
14548
  if (implicitWildcards.length > 0) {
14178
- const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey}.${e.targetProperty} (${e.functionName})`);
14549
+ const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey}.${e.targetProperty} (${e.functionName ?? '<inline fn>'})`);
14179
14550
  logger.warn('PropertyDerivation: custom functions without explicit dependsOn detected. ' +
14180
14551
  `These run on EVERY form change, which may impact performance (form has ${fieldCount} fields). ` +
14181
14552
  'Consider specifying explicit dependsOn arrays for better performance.', derivationDescs);
@@ -14205,6 +14576,7 @@ function coreProviders() {
14205
14576
  deps: [FormStateManager],
14206
14577
  },
14207
14578
  FieldContextRegistryService,
14579
+ ArrayItemRegistryService,
14208
14580
  {
14209
14581
  provide: DEFAULT_PROPS,
14210
14582
  useFactory: (stateManager) => computed(() => stateManager.activeConfig()?.defaultProps),
@@ -14645,7 +15017,13 @@ class DynamicForm {
14645
15017
  }
14646
15018
  @case ('non-paged') {
14647
15019
  @for (field of resolvedFields(); track field.key) {
14648
- <ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
15020
+ <!-- @if + DfFieldOutlet's own renderReady-&-!hidden gate together silence NG01916:
15021
+ the template @if removes the host from the DOM (Angular's prescribed pattern), and
15022
+ the directive's gate stops a same-CD-pass mount before [formField] can warn.
15023
+ Don't dedupe one without the other — see DfFieldOutlet.renderReady. -->
15024
+ @if (!field.hidden()) {
15025
+ <ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
15026
+ }
14649
15027
  }
14650
15028
  }
14651
15029
  @default {
@@ -14665,7 +15043,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
14665
15043
  }
14666
15044
  @case ('non-paged') {
14667
15045
  @for (field of resolvedFields(); track field.key) {
14668
- <ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
15046
+ <!-- @if + DfFieldOutlet's own renderReady-&-!hidden gate together silence NG01916:
15047
+ the template @if removes the host from the DOM (Angular's prescribed pattern), and
15048
+ the directive's gate stops a same-CD-pass mount before [formField] can warn.
15049
+ Don't dedupe one without the other — see DfFieldOutlet.renderReady. -->
15050
+ @if (!field.hidden()) {
15051
+ <ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
15052
+ }
14669
15053
  }
14670
15054
  }
14671
15055
  @default {
@@ -15248,8 +15632,14 @@ function containerFieldMapper(fieldDef) {
15248
15632
  * ```
15249
15633
  */
15250
15634
  /**
15251
- * Base field type definitions for fields that don't use the `field` input.
15252
- * Used by display-only fields like `text` that render immediately without
15635
+ * Base for container field types that consume the mapper-emitted `field` input
15636
+ * (row, group, array, page, container).
15637
+ */
15638
+ const CONTAINER_FIELD_TYPES_BASE = {
15639
+ renderReadyWhen: ['field'],
15640
+ };
15641
+ /**
15642
+ * Base for display-only fields (`text`) that render immediately without
15253
15643
  * waiting for form value integration.
15254
15644
  */
15255
15645
  const DISPLAY_FIELD_TYPES_BASE = {
@@ -15268,31 +15658,35 @@ const BUILT_IN_FIELDS = [
15268
15658
  // `row` is a virtual field type: it maps to ContainerFieldComponent, and
15269
15659
  // the rowFieldMapper injects a `{ type: 'row' }` wrapper so the container
15270
15660
  // renders the flex/grid layout via RowWrapperComponent.
15271
- loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs'),
15661
+ loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs'),
15272
15662
  mapper: rowFieldMapper,
15273
15663
  valueHandling: 'flatten',
15664
+ ...CONTAINER_FIELD_TYPES_BASE,
15274
15665
  },
15275
15666
  {
15276
15667
  name: 'group',
15277
- loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs'),
15668
+ loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs'),
15278
15669
  mapper: groupFieldMapper,
15279
15670
  valueHandling: 'include',
15671
+ ...CONTAINER_FIELD_TYPES_BASE,
15280
15672
  },
15281
15673
  {
15282
15674
  name: 'array',
15283
- loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs'),
15675
+ loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs'),
15284
15676
  mapper: arrayFieldMapper,
15285
15677
  valueHandling: 'include',
15678
+ ...CONTAINER_FIELD_TYPES_BASE,
15286
15679
  },
15287
15680
  {
15288
15681
  name: 'page',
15289
- loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs'),
15682
+ loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs'),
15290
15683
  mapper: pageFieldMapper,
15291
15684
  valueHandling: 'flatten',
15685
+ ...CONTAINER_FIELD_TYPES_BASE,
15292
15686
  },
15293
15687
  {
15294
15688
  name: 'text',
15295
- loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs'),
15689
+ loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs'),
15296
15690
  mapper: textFieldMapper,
15297
15691
  valueHandling: 'exclude',
15298
15692
  ...DISPLAY_FIELD_TYPES_BASE,
@@ -15304,15 +15698,16 @@ const BUILT_IN_FIELDS = [
15304
15698
  },
15305
15699
  {
15306
15700
  name: 'container',
15307
- loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs'),
15701
+ loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs'),
15308
15702
  mapper: containerFieldMapper,
15309
15703
  valueHandling: 'flatten',
15704
+ ...CONTAINER_FIELD_TYPES_BASE,
15310
15705
  },
15311
15706
  ];
15312
15707
  const BUILT_IN_WRAPPERS = [
15313
15708
  {
15314
15709
  wrapperName: 'css',
15315
- loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs'),
15710
+ loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs'),
15316
15711
  },
15317
15712
  {
15318
15713
  wrapperName: 'row',
@@ -16386,5 +16781,5 @@ function extractErrorParams(error) {
16386
16781
  * Generated bundle index. Do not edit.
16387
16782
  */
16388
16783
 
16389
- export { SubmitEvent as $, ARRAY_CONTEXT as A, BUILT_IN_FIELDS as B, CONTAINER_FIELD_PROCESSORS as C, DynamicFormLogger as D, EventBus as E, FIELD_SIGNAL_CONTEXT as F, DEFAULT_VALIDATION_MESSAGES as G, DynamicForm as H, DynamicFormError as I, EventDispatcher as J, FIELD_REGISTRY as K, FORM_OPTIONS as L, FormClearEvent as M, FormResetEvent as N, INITIALIZATION_TIMEOUT_MS as O, InsertArrayItemEvent as P, MoveArrayItemEvent as Q, RootFormRegistryService as R, NextPageEvent as S, NoopLogger as T, PageChangeEvent as U, PopArrayItemEvent as V, WRAPPER_AUTO_ASSOCIATIONS as W, PrependArrayItemEvent as X, PreviousPageEvent as Y, RemoveAtIndexEvent as Z, ShiftArrayItemEvent as _, DEFAULT_WRAPPERS as a, WRAPPER_REGISTRY as a0, applyMetaToElement as a1, applyValidator as a2, applyValidators as a3, arrayEvent as a4, arrayFieldMapper as a5, baseFieldMapper as a6, buildBaseInputs as a7, containerFieldMapper as a8, createField as a9, resolveNextButtonDisabled as aA, resolveNonFieldDisabled as aB, resolveNonFieldHidden as aC, resolveSubmitButtonDisabled as aD, resolveTokens as aE, rowFieldMapper as aF, textFieldMapper as aG, toReadonlyFieldTree as aH, withEventFormValue as aI, withLoggerConfig as aJ, withPreviousValue as aK, withValidationExecutionDefaults as aL, withValueExclusionDefaults as aM, wrapperProps as aN, createWrappers as aa, evaluateNonFieldDisabled as ab, evaluateNonFieldHidden as ac, field as ad, formConfig as ae, getArrayLength as af, groupFieldMapper as ag, hasFormValue as ah, interpolateParams as ai, isArrayField as aj, isCheckedField as ak, isContainerField as al, isContainerTypedField as am, isDisplayOnlyField as an, isFormStateCondition as ao, isLeafField as ap, isPageField as aq, isRowField as ar, isSimplifiedArrayField as as, isValueBearingField as at, isValueField as au, isWrapperTypeDefinition as av, isWrappersBundle as aw, omit as ax, pageFieldMapper as ay, provideDynamicForm as az, isSameWrapperChain as b, createFieldResolutionPipe as c, derivedFromDeferred as d, createWrapperChainController as e, DfFieldOutlet as f, isEqual as g, createSchemaFromFields as h, injectFieldRegistry as i, createRenderReadySignal as j, ARRAY_TEMPLATE_REGISTRY as k, ARRAY_ITEM_ID_GENERATOR as l, mapFieldToInputs as m, getNormalizedArrayMetadata as n, getFieldValueHandling as o, getFieldDefaultValue as p, isGroupField as q, resolveWrappers as r, createArrayItemIdGenerator as s, ComponentInitializedEvent as t, DynamicTextPipe as u, validatePageNesting as v, dynamicTextToObservable as w, AppendArrayItemEvent as x, ConsoleLogger as y, DEFAULT_PROPS as z };
16390
- //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs.map
16784
+ export { SubmitEvent as $, ARRAY_CONTEXT as A, BUILT_IN_FIELDS as B, CONTAINER_FIELD_PROCESSORS as C, DynamicFormLogger as D, EventBus as E, FIELD_SIGNAL_CONTEXT as F, GROUP_CONTEXT as G, DynamicForm as H, DynamicFormError as I, EventDispatcher as J, FIELD_REGISTRY as K, FORM_OPTIONS as L, FormClearEvent as M, FormResetEvent as N, INITIALIZATION_TIMEOUT_MS as O, InsertArrayItemEvent as P, MoveArrayItemEvent as Q, RootFormRegistryService as R, NextPageEvent as S, NoopLogger as T, PageChangeEvent as U, PopArrayItemEvent as V, WRAPPER_AUTO_ASSOCIATIONS as W, PrependArrayItemEvent as X, PreviousPageEvent as Y, RemoveAtIndexEvent as Z, ShiftArrayItemEvent as _, DEFAULT_WRAPPERS as a, WRAPPER_REGISTRY as a0, applyMetaToElement as a1, applyValidator as a2, applyValidators as a3, arrayEvent as a4, arrayFieldMapper as a5, baseFieldMapper as a6, buildBaseInputs as a7, containerFieldMapper as a8, createField as a9, resolveNextButtonDisabled as aA, resolveNonFieldDisabled as aB, resolveNonFieldHidden as aC, resolveSubmitButtonDisabled as aD, resolveTokens as aE, rowFieldMapper as aF, textFieldMapper as aG, toReadonlyFieldTree as aH, withEventFormValue as aI, withLoggerConfig as aJ, withPreviousValue as aK, withValidationExecutionDefaults as aL, withValueExclusionDefaults as aM, wrapperProps as aN, createWrappers as aa, evaluateNonFieldDisabled as ab, evaluateNonFieldHidden as ac, field as ad, formConfig as ae, getArrayLength as af, groupFieldMapper as ag, hasFormValue as ah, interpolateParams as ai, isArrayField as aj, isCheckedField as ak, isContainerField as al, isContainerTypedField as am, isDisplayOnlyField as an, isFormStateCondition as ao, isLeafField as ap, isPageField as aq, isRowField as ar, isSimplifiedArrayField as as, isValueBearingField as at, isValueField as au, isWrapperTypeDefinition as av, isWrappersBundle as aw, omit as ax, pageFieldMapper as ay, provideDynamicForm as az, isSameWrapperChain as b, createFieldResolutionPipe as c, derivedFromDeferred as d, createWrapperChainController as e, DfFieldOutlet as f, isEqual as g, createSchemaFromFields as h, injectFieldRegistry as i, createHiddenSignal as j, createRenderReadySignal as k, ArrayItemRegistryService as l, mapFieldToInputs as m, getNormalizedArrayMetadata as n, getFieldValueHandling as o, getFieldDefaultValue as p, isGroupField as q, resolveWrappers as r, ComponentInitializedEvent as s, DynamicTextPipe as t, dynamicTextToObservable as u, validatePageNesting as v, AppendArrayItemEvent as w, ConsoleLogger as x, DEFAULT_PROPS as y, DEFAULT_VALIDATION_MESSAGES as z };
16785
+ //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs.map