@ng-forge/dynamic-forms 0.9.0-next.11 → 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-DJ-LiLm_.mjs → ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs} +154 -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-Pcxg5PQu.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-a4RstR2Z.mjs → ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs} +2 -2
  6. package/fesm2022/{ng-forge-dynamic-forms-container-utils-a4RstR2Z.mjs.map → ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs.map} +1 -1
  7. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-CJACCIEK.mjs → ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs} +2 -2
  8. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-CJACCIEK.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs.map} +1 -1
  9. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-Bjf_I0D_.mjs → ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs} +2 -2
  10. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-Bjf_I0D_.mjs.map → ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs.map} +1 -1
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-fzKwPrQr.mjs → ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs} +9 -5
  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-o3rJX4vG.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs} +396 -248
  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-BX4R9GGa.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-B9SK8Vun.mjs → ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs} +2 -2
  18. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-B9SK8Vun.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 +488 -468
  22. package/fesm2022/ng-forge-dynamic-forms-array-field.component-DJ-LiLm_.mjs.map +0 -1
  23. package/fesm2022/ng-forge-dynamic-forms-container-field.component-Pcxg5PQu.mjs.map +0 -1
  24. package/fesm2022/ng-forge-dynamic-forms-group-field.component-fzKwPrQr.mjs.map +0 -1
  25. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-o3rJX4vG.mjs.map +0 -1
  26. package/fesm2022/ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs.map +0 -1
@@ -1,10 +1,62 @@
1
1
  import * as i0 from '@angular/core';
2
- import { computed, runInInjectionContext, Injector, signal, linkedSignal, inject, DestroyRef, EnvironmentInjector, input, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { R as RootFormRegistryService, g as isEqual, m as mapFieldToInputs, F as FIELD_SIGNAL_CONTEXT, A as ARRAY_CONTEXT, G as GROUP_CONTEXT, j as createRenderReadySignal, D as DynamicFormLogger, i as injectFieldRegistry, E as EventBus, k as ARRAY_TEMPLATE_REGISTRY, l as ARRAY_ITEM_ID_GENERATOR, n as getNormalizedArrayMetadata, o as getFieldValueHandling, p as getFieldDefaultValue, q as isGroupField, f as DfFieldOutlet, s as createArrayItemIdGenerator } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms-o3rJX4vG.mjs';
2
+ import { signal, computed, runInInjectionContext, Injector, linkedSignal, inject, DestroyRef, EnvironmentInjector, input, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { R as RootFormRegistryService, g as isEqual, m as mapFieldToInputs, F as FIELD_SIGNAL_CONTEXT, A as ARRAY_CONTEXT, G as GROUP_CONTEXT, j as createHiddenSignal, k as createRenderReadySignal, D as DynamicFormLogger, i as injectFieldRegistry, E as EventBus, l as ArrayItemRegistryService, n as getNormalizedArrayMetadata, o as getFieldValueHandling, p as getFieldDefaultValue, q as isGroupField, f as DfFieldOutlet } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs';
4
4
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
5
  import { of, from, map, catchError, forkJoin, filter, firstValueFrom, tap } from 'rxjs';
6
6
  import { explicitEffect } from 'ngxtension/explicit-effect';
7
- import { e as emitComponentInitialized } from './ng-forge-dynamic-forms-emit-initialization-Bjf_I0D_.mjs';
7
+ import { e as emitComponentInitialized } from './ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs';
8
+
9
+ class ArrayFieldStateMachine {
10
+ _state = signal('idle', ...(ngDevMode ? [{ debugName: "_state" }] : /* istanbul ignore next */ []));
11
+ _runId = signal(0, ...(ngDevMode ? [{ debugName: "_runId" }] : /* istanbul ignore next */ []));
12
+ lastEmittedRunId = -1;
13
+ _hasEverEmitted = false;
14
+ state = this._state.asReadonly();
15
+ runId = this._runId.asReadonly();
16
+ /** True after the first `componentInitialized` pulse has fired for this component. */
17
+ get hasEverEmitted() {
18
+ return this._hasEverEmitted;
19
+ }
20
+ dispatch(op) {
21
+ const nextId = this._runId() + 1;
22
+ this._runId.set(nextId);
23
+ switch (op.kind) {
24
+ case 'clear':
25
+ case 'initial':
26
+ case 'recreate':
27
+ this._state.set('pending');
28
+ break;
29
+ case 'append':
30
+ // append doesn't reset state — mirrors today's behavior where
31
+ // appendItems leaves the init-cycle tracking alone.
32
+ break;
33
+ }
34
+ return {
35
+ id: nextId,
36
+ isStale: () => this._runId() !== nextId,
37
+ resolve: () => {
38
+ if (this._runId() === nextId && this._state() === 'pending') {
39
+ this._state.set('settled');
40
+ }
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * Edge-trigger for the emit-effect. Returns true exactly once per `settled` run
46
+ * when `allReady` is true. Side-effect: records the run as emitted and transitions
47
+ * the state back to `idle`. Subsequent calls without a fresh dispatch return false.
48
+ */
49
+ shouldEmit(allReady) {
50
+ if (this._state() !== 'settled' || !allReady)
51
+ return false;
52
+ if (this.lastEmittedRunId === this._runId())
53
+ return false;
54
+ this.lastEmittedRunId = this._runId();
55
+ this._hasEverEmitted = true;
56
+ this._state.set('idle');
57
+ return true;
58
+ }
59
+ }
8
60
 
9
61
  /**
10
62
  * Safely extracts an array value from a model object.
@@ -198,12 +250,14 @@ const EMPTY_GROUP_CONTEXT = { groupPath: signal('') };
198
250
  * Supports multiple sibling templates (e.g., name + email without a wrapper).
199
251
  */
200
252
  function resolveArrayItem(options) {
201
- const { index, templates, arrayField, itemPositionMap, parentFieldSignalContext, parentInjector, registry, destroyRef, loadTypeComponent, generateItemId, primitiveFieldKey, } = options;
253
+ const { index, templates, arrayField, itemPositionMap, parentFieldSignalContext, parentInjector, registry, destroyRef, loadTypeComponent, generateItemId, existingItemId, primitiveFieldKey, } = options;
202
254
  if (templates.length === 0) {
203
255
  return of(undefined);
204
256
  }
205
- // Generate ONE id for this array item (shared by all fields for tracking)
206
- const itemId = generateItemId();
257
+ // Generate ONE id for this array item (shared by all fields for tracking), unless
258
+ // the caller is rehydrating an existing item — in which case reuse its id so the
259
+ // form-scoped template registry continues to map cleanly.
260
+ const itemId = existingItemId ?? generateItemId();
207
261
  // O(1) position lookup via Map instead of O(n) indexOf()
208
262
  const indexSignal = linkedSignal(() => {
209
263
  const positionMap = itemPositionMap();
@@ -238,6 +292,7 @@ function resolveArrayItem(options) {
238
292
  injector,
239
293
  inputs,
240
294
  renderReady: createRenderReadySignal(inputs, registry.get(template.type)),
295
+ hidden: createHiddenSignal(inputs),
241
296
  };
242
297
  }), catchError((error) => {
243
298
  if (!destroyRef.destroyed) {
@@ -337,8 +392,16 @@ class ArrayFieldComponent {
337
392
  environmentInjector = inject(EnvironmentInjector);
338
393
  eventBus = inject(EventBus);
339
394
  logger = inject(DynamicFormLogger);
340
- templateRegistry = inject(ARRAY_TEMPLATE_REGISTRY);
341
- generateItemId = inject(ARRAY_ITEM_ID_GENERATOR);
395
+ arrayItemRegistry = inject(ArrayItemRegistryService);
396
+ // Form-scoped slot keyed by the array's path. Survives this component's lifecycle so that
397
+ // `@if`-gated arrays can be destroyed and recreated without losing dynamically-added items.
398
+ // NOTE: keyed by `field().key` for now — nested arrays with the same key in different scopes
399
+ // would currently collide. Today's per-component-provider model has the same constraint
400
+ // (no nested-array hide/show tests exist). Path-aware keying is a follow-up.
401
+ // `field()` is `input.required`, so the slot reference is resolved lazily.
402
+ slot = computed(() => this.arrayItemRegistry.slotFor(this.field().key), ...(ngDevMode ? [{ debugName: "slot" }] : /* istanbul ignore next */ []));
403
+ /** Bound id-generator reference threaded to `resolveArrayItem` so callsites stay terse. */
404
+ generateItemId = () => this.slot().nextId();
342
405
  // ─────────────────────────────────────────────────────────────────────────────
343
406
  // Inputs
344
407
  // ─────────────────────────────────────────────────────────────────────────────
@@ -466,9 +529,9 @@ class ArrayFieldComponent {
466
529
  // State Signals
467
530
  // ─────────────────────────────────────────────────────────────────────────────
468
531
  resolvedItemsSignal = signal([], ...(ngDevMode ? [{ debugName: "resolvedItemsSignal" }] : /* istanbul ignore next */ []));
469
- updateVersion = signal(0, ...(ngDevMode ? [{ debugName: "updateVersion" }] : /* istanbul ignore next */ []));
470
- pendingInitializationCycle = signal(null, ...(ngDevMode ? [{ debugName: "pendingInitializationCycle" }] : /* istanbul ignore next */ []));
471
- settledInitializationCycle = signal(null, ...(ngDevMode ? [{ debugName: "settledInitializationCycle" }] : /* istanbul ignore next */ []));
532
+ // Explicit state machine for async resolution + componentInitialized emission.
533
+ // Replaces the prior updateVersion + pendingInitializationCycle + settledInitializationCycle trio.
534
+ fsm = new ArrayFieldStateMachine();
472
535
  /**
473
536
  * Map of item IDs to their current positions. O(1) lookup vs O(n) indexOf().
474
537
  * Used by child linkedSignals to reactively track their position in the array.
@@ -518,10 +581,9 @@ class ArrayFieldComponent {
518
581
  explicitEffect([this.arrayFieldTrees], ([fieldTrees]) => {
519
582
  this.performDifferentialUpdate(fieldTrees);
520
583
  });
521
- explicitEffect([this.pendingInitializationCycle, this.settledInitializationCycle, this.allResolvedFieldsRenderReady], ([pending, settled, allReady]) => {
522
- if (pending !== null && settled === pending && allReady) {
584
+ explicitEffect([this.fsm.state, this.allResolvedFieldsRenderReady], ([, allReady]) => {
585
+ if (this.fsm.shouldEmit(allReady)) {
523
586
  emitComponentInitialized(this.eventBus, 'array', this.field().key, this.parentInjector);
524
- this.pendingInitializationCycle.set(null);
525
587
  }
526
588
  });
527
589
  }
@@ -607,9 +669,9 @@ class ArrayFieldComponent {
607
669
  }
608
670
  }
609
671
  }
610
- // Increment version and create resolved item BEFORE updating value
611
- this.updateVersion.update((v) => v + 1);
612
- const currentVersion = this.updateVersion();
672
+ // Dispatch an append run — bumps runId so any in-flight differential update
673
+ // becomes stale, but state stays in its current phase.
674
+ const run = this.fsm.dispatch({ kind: 'append' });
613
675
  // Append auto-remove button to resolution templates (for rendering only)
614
676
  const resolveTemplates = this.withAutoRemove(templates);
615
677
  // Resolve the new item
@@ -629,11 +691,13 @@ class ArrayFieldComponent {
629
691
  this.logger.error(`Failed to resolve array item at index ${insertIndex}:`, error);
630
692
  return of(undefined);
631
693
  })));
632
- if (currentVersion !== this.updateVersion() || !resolvedItem) {
694
+ if (run.isStale() || !resolvedItem) {
633
695
  return;
634
696
  }
635
697
  // Store the template used for this item so it can be re-used during recreate operations
636
- this.templateRegistry.set(resolvedItem.id, templates);
698
+ this.slot().templates.set(resolvedItem.id, templates);
699
+ // Mirror the insert into slot.itemOrder so survival across hide/show stays consistent.
700
+ this.slot().itemOrder.splice(insertIndex, 0, resolvedItem.id);
637
701
  // Insert resolved item at correct position
638
702
  this.resolvedItemsSignal.update((current) => {
639
703
  const newItems = [...current];
@@ -651,31 +715,43 @@ class ArrayFieldComponent {
651
715
  performDifferentialUpdate(fieldTrees) {
652
716
  const resolvedItems = this.resolvedItemsSignal();
653
717
  const operation = determineDifferentialOperation(resolvedItems, fieldTrees.length);
654
- // Only increment version for operations that do actual work.
655
- // "none" operations (value-only changes) should not interfere with async add operations.
718
+ // "none" operations (value-only changes) don't kick the state machine — they shouldn't
719
+ // interfere with in-flight async resolutions.
656
720
  if (operation.type === 'none') {
657
721
  return;
658
722
  }
659
- this.updateVersion.update((v) => v + 1);
660
- const currentVersion = this.updateVersion();
661
723
  switch (operation.type) {
662
- case 'clear':
724
+ case 'clear': {
663
725
  // Clean up template registry when all items are removed
664
- this.templateRegistry.clear();
726
+ this.slot().templates.clear();
665
727
  this.resolvedItemsSignal.set([]);
666
- if (resolvedItems.length === 0 && this.pendingInitializationCycle() === null && this.settledInitializationCycle() === null) {
667
- this.pendingInitializationCycle.set(currentVersion);
668
- this.settledInitializationCycle.set(currentVersion);
728
+ const run = this.fsm.dispatch({ kind: 'clear' });
729
+ // First-ever clear with no items: synthesize a resolve so the emit-effect fires
730
+ // the init pulse (matches the form-starts-empty case from the prior implementation).
731
+ if (resolvedItems.length === 0 && !this.fsm.hasEverEmitted) {
732
+ run.resolve();
669
733
  }
670
734
  break;
671
- case 'initial':
672
- this.pendingInitializationCycle.set(currentVersion);
673
- this.settledInitializationCycle.set(null);
674
- void this.resolveAllItems(fieldTrees, currentVersion);
735
+ }
736
+ case 'initial': {
737
+ // If the slot has surviving state from a prior ArrayFieldComponent instance (same
738
+ // form, same array path, just a different render cycle — e.g. an `@if`-gated array
739
+ // that was hidden then re-shown), reuse the stored templates AND ids so dynamically-
740
+ // added items rebuild correctly. When the slot is empty this collapses to a normal
741
+ // initial resolve via Priority 2/3.
742
+ const slot = this.slot();
743
+ const useSurvival = slot.itemOrder.length === fieldTrees.length && fieldTrees.length > 0;
744
+ const positionalTemplates = useSurvival ? slot.itemOrder.map((id) => slot.templates.get(id)) : undefined;
745
+ const existingItemIds = useSurvival ? slot.itemOrder.slice() : undefined;
746
+ const run = this.fsm.dispatch({ kind: 'initial' });
747
+ void this.resolveAllItems(fieldTrees, run, positionalTemplates, existingItemIds);
675
748
  break;
676
- case 'append':
677
- void this.appendItems(fieldTrees, operation.startIndex, operation.endIndex, currentVersion);
749
+ }
750
+ case 'append': {
751
+ const run = this.fsm.dispatch({ kind: 'append' });
752
+ void this.appendItems(fieldTrees, operation.startIndex, operation.endIndex, run);
678
753
  break;
754
+ }
679
755
  case 'recreate': {
680
756
  // Capture templates by item ID so each item is recreated with its original template,
681
757
  // even after move operations that change positions. Registry entries (from dynamic adds
@@ -683,49 +759,53 @@ class ArrayFieldComponent {
683
759
  const positionalTemplates = resolvedItems.map((item) => {
684
760
  // Registry entries (from dynamic adds or moves) take priority;
685
761
  // unmoved initial items return undefined → falls through to itemTemplates[idx] during resolve.
686
- return this.templateRegistry.get(item.id);
762
+ return this.slot().templates.get(item.id);
687
763
  });
688
764
  this.resolvedItemsSignal.set([]);
689
- this.pendingInitializationCycle.set(currentVersion);
690
- this.settledInitializationCycle.set(null);
691
- void this.resolveAllItems(fieldTrees, currentVersion, positionalTemplates);
765
+ const run = this.fsm.dispatch({ kind: 'recreate' });
766
+ void this.resolveAllItems(fieldTrees, run, positionalTemplates);
692
767
  break;
693
768
  }
694
769
  }
695
770
  }
696
- async resolveAllItems(fieldTrees, updateId, positionalTemplates) {
771
+ async resolveAllItems(fieldTrees, run, positionalTemplates, existingItemIds) {
697
772
  if (fieldTrees.length === 0) {
698
773
  this.resolvedItemsSignal.set([]);
699
774
  return;
700
775
  }
701
776
  // Wrap each item observable to catch individual errors
702
- const safeItemObservables = fieldTrees.map((_, i) => this.createResolveItemObservable(i, positionalTemplates?.[i]).pipe(catchError((error) => {
777
+ const safeItemObservables = fieldTrees.map((_, i) => this.createResolveItemObservable(i, positionalTemplates?.[i], existingItemIds?.[i]).pipe(catchError((error) => {
703
778
  this.logger.error(`Failed to resolve array item at index ${i}:`, error);
704
779
  return of(undefined);
705
780
  })));
706
781
  try {
707
782
  const items = await firstValueFrom(forkJoin(safeItemObservables).pipe(map((items) => items.filter((item) => item !== undefined))));
708
- if (updateId === this.updateVersion()) {
783
+ if (!run.isStale()) {
709
784
  // Update template registry with new item IDs for dynamically added items
710
785
  if (positionalTemplates) {
711
786
  // Clean up old entries and add new ones
712
787
  const newItemIds = new Set(items.map((item) => item.id));
713
- for (const existingId of this.templateRegistry.keys()) {
788
+ for (const existingId of this.slot().templates.keys()) {
714
789
  if (!newItemIds.has(existingId)) {
715
- this.templateRegistry.delete(existingId);
790
+ this.slot().templates.delete(existingId);
716
791
  }
717
792
  }
718
793
  items.forEach((item, idx) => {
719
794
  const template = positionalTemplates[idx];
720
795
  if (template) {
721
- this.templateRegistry.set(item.id, template);
796
+ this.slot().templates.set(item.id, template);
722
797
  }
723
798
  });
724
799
  }
800
+ // Mirror the resolved item ids into the slot's itemOrder so a future component
801
+ // recreate (e.g. after an @if hide cycle) can rehydrate via the survival path.
802
+ const slot = this.slot();
803
+ slot.itemOrder.length = 0;
804
+ for (const item of items)
805
+ slot.itemOrder.push(item.id);
725
806
  this.resolvedItemsSignal.set(items);
726
- if (this.pendingInitializationCycle() === updateId) {
727
- this.settledInitializationCycle.set(updateId);
728
- }
807
+ // Mark this run settled so the emit-effect can fire componentInitialized once allReady is true.
808
+ run.resolve();
729
809
  }
730
810
  }
731
811
  catch (err) {
@@ -733,7 +813,7 @@ class ArrayFieldComponent {
733
813
  this.resolvedItemsSignal.set([]);
734
814
  }
735
815
  }
736
- async appendItems(fieldTrees, startIndex, endIndex, updateId) {
816
+ async appendItems(fieldTrees, startIndex, endIndex, run) {
737
817
  const itemsToResolve = fieldTrees.slice(startIndex, endIndex);
738
818
  if (itemsToResolve.length === 0)
739
819
  return;
@@ -747,7 +827,10 @@ class ArrayFieldComponent {
747
827
  });
748
828
  try {
749
829
  const newItems = await firstValueFrom(forkJoin(safeItemObservables).pipe(map((items) => items.filter((item) => item !== undefined))));
750
- if (updateId === this.updateVersion()) {
830
+ if (!run.isStale()) {
831
+ const slot = this.slot();
832
+ for (const item of newItems)
833
+ slot.itemOrder.push(item.id);
751
834
  this.resolvedItemsSignal.update((current) => [...current, ...newItems]);
752
835
  }
753
836
  }
@@ -768,7 +851,7 @@ class ArrayFieldComponent {
768
851
  * Priority 1.
769
852
  * 4. Return undefined (item cannot be resolved without a template)
770
853
  */
771
- createResolveItemObservable(index, overrideTemplate) {
854
+ createResolveItemObservable(index, overrideTemplate, existingItemId) {
772
855
  const itemTemplates = this.itemTemplates();
773
856
  const primitiveKey = this.primitiveFieldKey();
774
857
  // Priority 1: Use override template (from recreate with stored templates)
@@ -784,6 +867,7 @@ class ArrayFieldComponent {
784
867
  destroyRef: this.destroyRef,
785
868
  loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
786
869
  generateItemId: this.generateItemId,
870
+ existingItemId,
787
871
  primitiveFieldKey: primitiveKey,
788
872
  });
789
873
  }
@@ -801,6 +885,7 @@ class ArrayFieldComponent {
801
885
  destroyRef: this.destroyRef,
802
886
  loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
803
887
  generateItemId: this.generateItemId,
888
+ existingItemId,
804
889
  primitiveFieldKey: primitiveKey,
805
890
  });
806
891
  }
@@ -819,12 +904,13 @@ class ArrayFieldComponent {
819
904
  destroyRef: this.destroyRef,
820
905
  loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
821
906
  generateItemId: this.generateItemId,
907
+ existingItemId,
822
908
  primitiveFieldKey: primitiveKey,
823
909
  }).pipe(tap((item) => {
824
910
  // Register the fallback template against the generated item id so subsequent
825
911
  // recreates hit Priority 1 instead of re-resolving via this branch.
826
912
  if (item)
827
- this.templateRegistry.set(item.id, fallback);
913
+ this.slot().templates.set(item.id, fallback);
828
914
  }));
829
915
  }
830
916
  // Priority 4: no template available. Full-API arrays are positional by design —
@@ -894,8 +980,8 @@ class ArrayFieldComponent {
894
980
  const hi = Math.max(fromIndex, toIndex);
895
981
  for (let i = lo; i <= hi; i++) {
896
982
  const item = currentItems[i];
897
- if (item && !this.templateRegistry.has(item.id) && i < rawTemplates.length) {
898
- this.templateRegistry.set(item.id, rawTemplates[i]);
983
+ if (item && !this.slot().templates.has(item.id) && i < rawTemplates.length) {
984
+ this.slot().templates.set(item.id, rawTemplates[i]);
899
985
  }
900
986
  }
901
987
  // Reorder resolvedItems — splice preserves object identity (no destroy/recreate)
@@ -905,6 +991,8 @@ class ArrayFieldComponent {
905
991
  newItems.splice(toIndex, 0, moved);
906
992
  return newItems;
907
993
  });
994
+ // Keep slot.itemOrder in sync so the post-recreate survival path sees the moved order.
995
+ this.slot().moveItem(fromIndex, toIndex);
908
996
  // Reorder form value array the same way
909
997
  const newArray = [...currentArray];
910
998
  const [movedValue] = newArray.splice(fromIndex, 1);
@@ -948,8 +1036,11 @@ class ArrayFieldComponent {
948
1036
  // no longer exists, params returns undefined, cancelling the pending validation automatically.
949
1037
  const removedItem = this.resolvedItemsSignal()[removeIndex];
950
1038
  if (removedItem) {
951
- this.templateRegistry.delete(removedItem.id);
1039
+ this.slot().templates.delete(removedItem.id);
952
1040
  }
1041
+ // Keep slot.itemOrder in sync so the post-recreate survival path doesn't try to
1042
+ // restore an id that's no longer in the form value.
1043
+ this.slot().removeAt(removeIndex);
953
1044
  this.resolvedItemsSignal.update((current) => {
954
1045
  const newItems = [...current];
955
1046
  newItems.splice(removeIndex, 1);
@@ -963,12 +1054,7 @@ class ArrayFieldComponent {
963
1054
  parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
964
1055
  }
965
1056
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
966
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ArrayFieldComponent, isStandalone: true, selector: "array-field", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, providers: [
967
- // Each array gets its own template registry to track templates used for dynamically added items
968
- { provide: ARRAY_TEMPLATE_REGISTRY, useFactory: () => new Map() },
969
- // Each array gets its own ID generator for SSR hydration compatibility
970
- { provide: ARRAY_ITEM_ID_GENERATOR, useFactory: createArrayItemIdGenerator },
971
- ], ngImport: i0, template: `
1057
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ArrayFieldComponent, isStandalone: true, selector: "array-field", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "class.df-container-hidden": "hidden()", "attr.aria-hidden": "hidden() || null", "id": "`${key()}`", "attr.data-testid": "key()" } }, ngImport: i0, template: `
972
1058
  @for (item of resolvedItems(); track item.id; let i = $index) {
973
1059
  <div
974
1060
  class="df-array-item"
@@ -978,7 +1064,9 @@ class ArrayFieldComponent {
978
1064
  [attr.data-array-item-index]="i"
979
1065
  >
980
1066
  @for (field of item.fields; track $index) {
981
- <ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
1067
+ @if (!field.hidden()) {
1068
+ <ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
1069
+ }
982
1070
  }
983
1071
  </div>
984
1072
  }
@@ -996,7 +1084,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
996
1084
  [attr.data-array-item-index]="i"
997
1085
  >
998
1086
  @for (field of item.fields; track $index) {
999
- <ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
1087
+ @if (!field.hidden()) {
1088
+ <ng-container *dfFieldOutlet="field; environmentInjector: environmentInjector" />
1089
+ }
1000
1090
  }
1001
1091
  </div>
1002
1092
  }
@@ -1006,13 +1096,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
1006
1096
  '[attr.aria-hidden]': 'hidden() || null',
1007
1097
  '[id]': '`${key()}`',
1008
1098
  '[attr.data-testid]': 'key()',
1009
- }, providers: [
1010
- // Each array gets its own template registry to track templates used for dynamically added items
1011
- { provide: ARRAY_TEMPLATE_REGISTRY, useFactory: () => new Map() },
1012
- // Each array gets its own ID generator for SSR hydration compatibility
1013
- { provide: ARRAY_ITEM_ID_GENERATOR, useFactory: createArrayItemIdGenerator },
1014
- ], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:grid;grid-template-columns:1fr;gap:var(--df-array-item-gap, var(--df-grid-row-gap, .5rem));width:100%}:host>*{grid-column:1/-1}:host.df-container-hidden{display:none}.df-array-item{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}.df-array-item>*{grid-column:1/-1}\n"] }]
1099
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-array-item-gap: var(--df-grid-row-gap);--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:grid;grid-template-columns:1fr;gap:var(--df-array-item-gap, var(--df-grid-row-gap, .5rem));width:100%}:host>*{grid-column:1/-1}:host.df-container-hidden{display:none}.df-array-item{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}.df-array-item>*{grid-column:1/-1}\n"] }]
1015
1100
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }] } });
1016
1101
 
1017
1102
  export { ArrayFieldComponent, ArrayFieldComponent as default };
1018
- //# sourceMappingURL=ng-forge-dynamic-forms-array-field.component-DJ-LiLm_.mjs.map
1103
+ //# sourceMappingURL=ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs.map