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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-GiaVHO9N.mjs → ng-forge-dynamic-forms-array-field.component-D6GEsH72.mjs} +99 -476
  2. package/fesm2022/ng-forge-dynamic-forms-array-field.component-D6GEsH72.mjs.map +1 -0
  3. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-C554hOxN.mjs → ng-forge-dynamic-forms-container-field.component-BluTT44A.mjs} +3 -14
  4. package/fesm2022/ng-forge-dynamic-forms-container-field.component-BluTT44A.mjs.map +1 -0
  5. package/fesm2022/ng-forge-dynamic-forms-container-utils-diYAWMa_.mjs +23 -0
  6. package/fesm2022/ng-forge-dynamic-forms-container-utils-diYAWMa_.mjs.map +1 -0
  7. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-CnJgFWLB.mjs → ng-forge-dynamic-forms-css-wrapper.component-CgMQOaG7.mjs} +2 -22
  8. package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-CgMQOaG7.mjs.map +1 -0
  9. package/fesm2022/ng-forge-dynamic-forms-emit-initialization-CB86IMsQ.mjs +17 -0
  10. package/fesm2022/ng-forge-dynamic-forms-emit-initialization-CB86IMsQ.mjs.map +1 -0
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BJ8j7Sh0.mjs → ng-forge-dynamic-forms-group-field.component-2bWHql4u.mjs} +15 -63
  12. package/fesm2022/ng-forge-dynamic-forms-group-field.component-2bWHql4u.mjs.map +1 -0
  13. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +72 -844
  14. package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
  15. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CPkVjU_N.mjs +10876 -0
  16. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CPkVjU_N.mjs.map +1 -0
  17. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs → ng-forge-dynamic-forms-page-field.component-zF-vEv5V.mjs} +10 -38
  18. package/fesm2022/ng-forge-dynamic-forms-page-field.component-zF-vEv5V.mjs.map +1 -0
  19. package/fesm2022/{ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs → ng-forge-dynamic-forms-row-wrapper.component-CwkYn5yl.mjs} +2 -18
  20. package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CwkYn5yl.mjs.map +1 -0
  21. package/fesm2022/ng-forge-dynamic-forms-schema.mjs +0 -73
  22. package/fesm2022/ng-forge-dynamic-forms-schema.mjs.map +1 -1
  23. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BGw6T-4h.mjs → ng-forge-dynamic-forms-text-field.component-_8_vVeH0.mjs} +2 -2
  24. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BGw6T-4h.mjs.map → ng-forge-dynamic-forms-text-field.component-_8_vVeH0.mjs.map} +1 -1
  25. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  26. package/package.json +1 -1
  27. package/types/ng-forge-dynamic-forms-integration.d.ts +27 -462
  28. package/types/ng-forge-dynamic-forms-schema.d.ts +2 -117
  29. package/types/ng-forge-dynamic-forms.d.ts +286 -4650
  30. package/fesm2022/ng-forge-dynamic-forms-array-field.component-GiaVHO9N.mjs.map +0 -1
  31. package/fesm2022/ng-forge-dynamic-forms-container-field.component-C554hOxN.mjs.map +0 -1
  32. package/fesm2022/ng-forge-dynamic-forms-container-utils-BS4d0cCk.mjs +0 -48
  33. package/fesm2022/ng-forge-dynamic-forms-container-utils-BS4d0cCk.mjs.map +0 -1
  34. package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-CnJgFWLB.mjs.map +0 -1
  35. package/fesm2022/ng-forge-dynamic-forms-emit-initialization-3PyXVwsn.mjs +0 -29
  36. package/fesm2022/ng-forge-dynamic-forms-emit-initialization-3PyXVwsn.mjs.map +0 -1
  37. package/fesm2022/ng-forge-dynamic-forms-group-field.component-BJ8j7Sh0.mjs.map +0 -1
  38. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms--PuL0AKc.mjs +0 -18773
  39. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms--PuL0AKc.mjs.map +0 -1
  40. package/fesm2022/ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs.map +0 -1
  41. package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs.map +0 -1
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { signal, computed, runInInjectionContext, Injector, linkedSignal, inject, DestroyRef, EnvironmentInjector, input, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { O as RootFormRegistryService, ax as isEqual, aL as mapFieldToInputs, v as FIELD_SIGNAL_CONTEXT, e as ARRAY_CONTEXT, G as GROUP_CONTEXT, a5 as createHiddenSignal, a6 as createRenderReadySignal, s as DynamicFormLogger, ao as injectFieldRegistry, ap as injectFieldSignalContext, E as EventBus, g as ArrayItemRegistryService, ak as getNormalizedArrayMetadata, aj as getFieldValueHandling, ai as getFieldDefaultValue, aA as isGroupField, o as DfFieldOutlet } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms--PuL0AKc.mjs';
3
+ import { O as RootFormRegistryService, ax as isEqual, aL as mapFieldToInputs, v as FIELD_SIGNAL_CONTEXT, e as ARRAY_CONTEXT, G as GROUP_CONTEXT, a5 as createHiddenSignal, a6 as createRenderReadySignal, s as DynamicFormLogger, ao as injectFieldRegistry, ap as injectFieldSignalContext, E as EventBus, g as ArrayItemRegistryService, ak as getNormalizedArrayMetadata, aj as getFieldValueHandling, ai as getFieldDefaultValue, aA as isGroupField, o as DfFieldOutlet } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms-CPkVjU_N.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-3PyXVwsn.mjs';
7
+ import { e as emitComponentInitialized } from './ng-forge-dynamic-forms-emit-initialization-CB86IMsQ.mjs';
8
8
 
9
9
  class ArrayFieldStateMachine {
10
10
  _state = signal('idle', ...(ngDevMode ? [{ debugName: "_state" }] : /* istanbul ignore next */ []));
@@ -13,8 +13,7 @@ class ArrayFieldStateMachine {
13
13
  _hasEverEmitted = false;
14
14
  state = this._state.asReadonly();
15
15
  runId = this._runId.asReadonly();
16
- /** True after the first `componentInitialized` pulse has fired for this component. */
17
- get hasEverEmitted() {
16
+ get hasEverEmitted() {
18
17
  return this._hasEverEmitted;
19
18
  }
20
19
  dispatch(op) {
@@ -27,9 +26,7 @@ class ArrayFieldStateMachine {
27
26
  this._state.set('pending');
28
27
  break;
29
28
  case 'append':
30
- // append doesn't reset state — mirrors today's behavior where
31
- // appendItems leaves the init-cycle tracking alone.
32
- break;
29
+ break;
33
30
  }
34
31
  return {
35
32
  id: nextId,
@@ -41,12 +38,7 @@ class ArrayFieldStateMachine {
41
38
  },
42
39
  };
43
40
  }
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) {
41
+ shouldEmit(allReady) {
50
42
  if (this._state() !== 'settled' || !allReady)
51
43
  return false;
52
44
  if (this.lastEmittedRunId === this._runId())
@@ -58,29 +50,10 @@ class ArrayFieldStateMachine {
58
50
  }
59
51
  }
60
52
 
61
- /**
62
- * Safely extracts an array value from a model object.
63
- * Returns empty array if the key doesn't exist or value isn't an array.
64
- */
65
53
  function getArrayValue(value, key) {
66
54
  const arrayValue = value?.[key];
67
55
  return Array.isArray(arrayValue) ? arrayValue : [];
68
56
  }
69
- /**
70
- * Determines the optimal differential update operation based on current and new state.
71
- *
72
- * Operations:
73
- * - Clear all (empty array)
74
- * - Initial render (no existing items)
75
- * - Append only (items added at end)
76
- * - Recreate (items removed - we can't know which items, so recreate all)
77
- * - None (same length - items update via linkedSignal)
78
- *
79
- * Note: We always use 'recreate' for removals because we can't determine which
80
- * specific items were removed. Operations like shift, removeAtIndex remove from
81
- * the middle, not just the end. Each array item has its own local form that
82
- * doesn't reactively track parent changes, so we must recreate to get correct values.
83
- */
84
57
  function determineDifferentialOperation(currentItems, newLength) {
85
58
  const currentLength = currentItems.length;
86
59
  if (newLength === 0) {
@@ -89,53 +62,25 @@ function determineDifferentialOperation(currentItems, newLength) {
89
62
  if (currentLength === 0) {
90
63
  return { type: 'initial', fieldTreesLength: newLength };
91
64
  }
92
- // For all length changes, we optimize by assuming existing items stay in place.
93
- // Items handle their own value updates via linkedSignal - no need to compare snapshots.
94
- // Snapshot comparison was causing false "recreate" when existing items had been edited.
95
- if (newLength > currentLength) {
96
- // Items added - append new ones, existing items stay
97
- return { type: 'append', startIndex: currentLength, endIndex: newLength };
65
+ if (newLength > currentLength) {
66
+ return { type: 'append', startIndex: currentLength, endIndex: newLength };
98
67
  }
99
68
  if (newLength < currentLength) {
100
- // Items removed - must recreate because we don't know which items were removed.
101
- // shift() removes from index 0, removeAtIndex() removes from any position.
102
- // Each item's local form is initialized with its value at creation time and
103
- // doesn't reactively track parent array changes.
104
- return { type: 'recreate' };
69
+ return { type: 'recreate' };
105
70
  }
106
- // Same length - no structural change, items update via linkedSignal
107
- return { type: 'none' };
71
+ return { type: 'none' };
108
72
  }
109
73
 
110
- /**
111
- * Creates an injector and inputs for an array item.
112
- *
113
- * Uses direct root form binding - components bind directly to the root form's
114
- * FieldTree for array items (rootForm['arrayKey'][index]). This architecture:
115
- *
116
- * - Eliminates the need for local forms and bidirectional sync
117
- * - Allows Zod/StandardSchema validation errors to flow naturally to components
118
- * - Reduces complexity by having a single source of truth (the root form)
119
- *
120
- * The FieldSignalContext.form property uses a getter that evaluates a computed
121
- * signal, making form access reactive to index changes (when items reorder,
122
- * the component points to the correct array position).
123
- */
124
74
  function createArrayItemInjectorAndInputs(options) {
125
75
  const { template, indexSignal, parentFieldSignalContext, parentInjector, registry, arrayField, primitiveFieldKey } = options;
126
- // Get root form registry - it's guaranteed to be available since root form
127
- // is registered before resolvedFields computes (dependency chain ensures this)
128
- const rootFormRegistry = parentInjector.get(RootFormRegistryService);
129
- // Create a computed that derives the array item's FieldTree from root form.
130
- // Uses isEqual to prevent unnecessary re-computation when index changes but value is same.
131
- const itemFormAccessor = computed(() => {
76
+ const rootFormRegistry = parentInjector.get(RootFormRegistryService);
77
+ const itemFormAccessor = computed(() => {
132
78
  const rootForm = rootFormRegistry.rootForm();
133
79
  if (!rootForm) {
134
80
  return undefined;
135
81
  }
136
82
  const index = indexSignal();
137
- // Navigate: rootForm['arrayKey'][index]
138
- const arrayFieldTree = rootForm[arrayField.key];
83
+ const arrayFieldTree = rootForm[arrayField.key];
139
84
  if (!arrayFieldTree) {
140
85
  return undefined;
141
86
  }
@@ -149,30 +94,14 @@ function createArrayItemInjectorAndInputs(options) {
149
94
  arrayField,
150
95
  primitiveFieldKey,
151
96
  });
152
- // mapFieldToInputs automatically reads ARRAY_CONTEXT from the injector
153
- // and applies the index suffix to keys for unique DOM IDs
154
- const inputs = runInInjectionContext(injector, () => {
97
+ const inputs = runInInjectionContext(injector, () => {
155
98
  return mapFieldToInputs(template, registry);
156
99
  });
157
100
  return { injector, inputs };
158
101
  }
159
- /**
160
- * Creates a scoped injector for an array item.
161
- * Provides both FIELD_SIGNAL_CONTEXT (for form access) and ARRAY_CONTEXT (for position awareness).
162
- *
163
- * The FIELD_SIGNAL_CONTEXT.form uses a getter that evaluates itemFormAccessor() on access.
164
- * This makes form access reactive - when mappers access context.form['fieldKey'], the getter runs,
165
- * evaluating the computed which tracks indexSignal as a dependency. This ensures components
166
- * automatically update when array items reorder.
167
- *
168
- * Uses `useFactory` with `deps: [Injector]` to provide FIELD_SIGNAL_CONTEXT. Angular resolves
169
- * the `Injector` dep as the child injector being created, eliminating any temporal gap.
170
- */
171
102
  function createItemInjector(options) {
172
103
  const { itemFormAccessor, indexSignal, parentFieldSignalContext, parentInjector, arrayField, primitiveFieldKey } = options;
173
- // Use getter for formValue to ensure it's always current, not a stale snapshot.
174
- // Components accessing arrayContext.formValue will get the current value.
175
- const arrayContext = {
104
+ const arrayContext = {
176
105
  arrayKey: arrayField.key,
177
106
  index: indexSignal,
178
107
  get formValue() {
@@ -180,40 +109,24 @@ function createItemInjector(options) {
180
109
  },
181
110
  field: arrayField,
182
111
  };
183
- // Cache for the primitive wrapper object — avoids creating a new { [primitiveFieldKey]: raw }
184
- // on every form getter access, which would defeat reference-equality checks downstream.
185
- let cachedPrimitiveRaw;
112
+ let cachedPrimitiveRaw;
186
113
  let cachedPrimitiveWrapped;
187
114
  return Injector.create({
188
115
  parent: parentInjector,
189
116
  providers: [
190
117
  {
191
118
  provide: FIELD_SIGNAL_CONTEXT,
192
- // Angular resolves `Injector` in deps as the child injector being created,
193
- // so no temporal gap exists — the injector reference is immediately available.
194
- useFactory: (injector) => ({
119
+ useFactory: (injector) => ({
195
120
  injector,
196
121
  value: parentFieldSignalContext.value,
197
- // Array items don't propagate parent default values because each item's defaults
198
- // are determined by its template fields at creation time (via getFieldDefaultValue).
199
- // The parent's defaultValues contain top-level form defaults, not per-item defaults.
200
- defaultValues: () => ({}),
122
+ defaultValues: () => ({}),
201
123
  get form() {
202
124
  const raw = itemFormAccessor();
203
125
  if (!raw) {
204
- // During initialization or transitions, the FieldTree may not be available yet.
205
- // This occurs in the window between item creation and Angular's form tree construction.
206
- // Return an empty object so that downstream getFieldTree(key) calls return undefined
207
- // rather than causing a runtime error on a truly undefined value.
208
- return {};
126
+ return {};
209
127
  }
210
- // For primitive array items, the FieldTree is a FormControl (the item IS the control).
211
- // Wrap it in an object so getFieldTree(primitiveFieldKey) returns the FormControl itself.
212
- // Without this, getFieldTree('value') would access FormControl['value'] (the WritableSignal),
213
- // not a child FieldTree, causing NG0950 errors.
214
- if (primitiveFieldKey) {
215
- // Cache the wrapper to preserve reference identity when raw hasn't changed
216
- if (raw !== cachedPrimitiveRaw) {
128
+ if (primitiveFieldKey) {
129
+ if (raw !== cachedPrimitiveRaw) {
217
130
  cachedPrimitiveRaw = raw;
218
131
  cachedPrimitiveWrapped = { [primitiveFieldKey]: raw };
219
132
  }
@@ -225,51 +138,27 @@ function createItemInjector(options) {
225
138
  deps: [Injector],
226
139
  },
227
140
  { provide: ARRAY_CONTEXT, useValue: arrayContext },
228
- // Reset GROUP_CONTEXT at the array boundary: inner groups inside each
229
- // item start their path fresh, matching how value-derivation and
230
- // property-derivation collectors key entries
231
- // (`{arrayKey}.$.{innerGroupPath}.{fieldKey}`). Without this, an outer
232
- // group around the array would leak into descendants' DOM IDs and
233
- // override-store keys, drifting from the collector.
234
- { provide: GROUP_CONTEXT, useValue: EMPTY_GROUP_CONTEXT },
141
+ { provide: GROUP_CONTEXT, useValue: EMPTY_GROUP_CONTEXT },
235
142
  ],
236
143
  });
237
144
  }
238
- /**
239
- * Sentinel `GroupContext` provided at array item boundaries. Static `signal('')`
240
- * means `applyGroupPrefix` no-ops (empty path) and any inner group composes its
241
- * own path from this empty base.
242
- */
243
145
  const EMPTY_GROUP_CONTEXT = { groupPath: signal('') };
244
146
 
245
- /**
246
- * Resolves a single array item with all its fields for declarative rendering.
247
- *
248
- * Uses linkedSignal for the index, which automatically updates when itemOrderSignal changes.
249
- * This enables position-aware updates without recreating components when items are reordered.
250
- * Supports multiple sibling templates (e.g., name + email without a wrapper).
251
- */
252
147
  function resolveArrayItem(options) {
253
148
  const { index, templates, arrayField, itemPositionMap, parentFieldSignalContext, parentInjector, registry, destroyRef, loadTypeComponent, generateItemId, existingItemId, primitiveFieldKey, } = options;
254
149
  if (templates.length === 0) {
255
150
  return of(undefined);
256
151
  }
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();
261
- // O(1) position lookup via Map instead of O(n) indexOf()
262
- const indexSignal = linkedSignal(() => {
152
+ const itemId = existingItemId ?? generateItemId();
153
+ const indexSignal = linkedSignal(() => {
263
154
  const positionMap = itemPositionMap();
264
155
  return positionMap.get(itemId) ?? index;
265
156
  }, ...(ngDevMode ? [{ debugName: "indexSignal" }] : /* istanbul ignore next */ []));
266
- // Resolve all templates in parallel
267
- const fieldObservables = templates.map((template) => from(loadTypeComponent(template.type)).pipe(map((component) => {
157
+ const fieldObservables = templates.map((template) => from(loadTypeComponent(template.type)).pipe(map((component) => {
268
158
  if (destroyRef.destroyed) {
269
159
  return undefined;
270
160
  }
271
- // Componentless fields (e.g., hidden) return undefined - nothing to render
272
- if (!component) {
161
+ if (!component) {
273
162
  return undefined;
274
163
  }
275
164
  const { injector, inputs } = createArrayItemInjectorAndInputs({
@@ -281,8 +170,7 @@ function resolveArrayItem(options) {
281
170
  arrayField,
282
171
  primitiveFieldKey,
283
172
  });
284
- // Array item templates should always have inputs (componentless fields are handled above)
285
- if (!inputs) {
173
+ if (!inputs) {
286
174
  return undefined;
287
175
  }
288
176
  return {
@@ -304,8 +192,7 @@ function resolveArrayItem(options) {
304
192
  })));
305
193
  return forkJoin(fieldObservables).pipe(map((fields) => {
306
194
  const validFields = fields.filter((f) => f !== undefined);
307
- // If no fields resolved, return undefined
308
- if (validFields.length === 0) {
195
+ if (validFields.length === 0) {
309
196
  return undefined;
310
197
  }
311
198
  return {
@@ -315,9 +202,6 @@ function resolveArrayItem(options) {
315
202
  }));
316
203
  }
317
204
 
318
- /**
319
- * All array event type discriminants.
320
- */
321
205
  const ARRAY_EVENT_TYPES = [
322
206
  'append-array-item',
323
207
  'prepend-array-item',
@@ -327,10 +211,6 @@ const ARRAY_EVENT_TYPES = [
327
211
  'shift-array-item',
328
212
  'remove-at-index',
329
213
  ];
330
- /**
331
- * Converts an array event to a normalized action.
332
- * Uses a switch for full type safety with discriminated union narrowing.
333
- */
334
214
  function toArrayAction(event) {
335
215
  switch (event.type) {
336
216
  case 'append-array-item':
@@ -349,43 +229,12 @@ function toArrayAction(event) {
349
229
  return { action: 'remove', index: event.index };
350
230
  }
351
231
  }
352
- /**
353
- * Creates an observable stream of normalized array actions for a specific array key.
354
- *
355
- * @param eventBus - The event bus to subscribe to
356
- * @param arrayKey - Function returning the array key to filter events for
357
- * @returns Observable of normalized ArrayAction objects
358
- *
359
- * @example
360
- * ```typescript
361
- * observeArrayActions(this.eventBus, () => this.key())
362
- * .pipe(takeUntilDestroyed())
363
- * .subscribe(action => {
364
- * if (action.action === 'add') {
365
- * this.addItem(action.template, action.index);
366
- * } else {
367
- * this.removeItem(action.index);
368
- * }
369
- * });
370
- * ```
371
- */
372
232
  function observeArrayActions(eventBus, arrayKey) {
373
233
  return eventBus.on(ARRAY_EVENT_TYPES).pipe(filter((event) => event.arrayKey === arrayKey()), map(toArrayAction));
374
234
  }
375
235
 
376
- /**
377
- * Container component for rendering dynamic arrays of fields.
378
- *
379
- * Supports add/remove/move operations via the arrayEvent() builder API.
380
- * Uses differential updates to optimize rendering - only recreates items when necessary.
381
- * Each item gets a scoped injector with ARRAY_CONTEXT for position-aware operations.
382
- * Supports multiple sibling fields per array item (e.g., name + email without a wrapper).
383
- */
384
236
  class ArrayFieldComponent {
385
- // ─────────────────────────────────────────────────────────────────────────────
386
- // Dependencies
387
- // ─────────────────────────────────────────────────────────────────────────────
388
- destroyRef = inject(DestroyRef);
237
+ destroyRef = inject(DestroyRef);
389
238
  fieldRegistry = injectFieldRegistry();
390
239
  parentFieldSignalContext = injectFieldSignalContext();
391
240
  parentInjector = inject(Injector);
@@ -393,112 +242,47 @@ class ArrayFieldComponent {
393
242
  eventBus = inject(EventBus);
394
243
  logger = inject(DynamicFormLogger);
395
244
  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();
405
- // ─────────────────────────────────────────────────────────────────────────────
406
- // Inputs
407
- // ─────────────────────────────────────────────────────────────────────────────
408
- field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
245
+ slot = computed(() => this.arrayItemRegistry.slotFor(this.field().key), ...(ngDevMode ? [{ debugName: "slot" }] : /* istanbul ignore next */ []));
246
+ generateItemId = () => this.slot().nextId();
247
+ field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
409
248
  key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
410
249
  className = input(...(ngDevMode ? [undefined, { debugName: "className" }] : /* istanbul ignore next */ []));
411
250
  hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
412
- // ─────────────────────────────────────────────────────────────────────────────
413
- // Computed Signals
414
- // ─────────────────────────────────────────────────────────────────────────────
415
- hostClasses = computed(() => {
251
+ hostClasses = computed(() => {
416
252
  const base = 'df-field df-array';
417
253
  const custom = this.className();
418
254
  return custom ? `${base} ${custom}` : base;
419
255
  }, ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
420
256
  rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : /* istanbul ignore next */ []));
421
- /**
422
- * Gets the auto-remove button FieldDef from normalization metadata.
423
- * Set by simplified array normalization for primitive arrays with remove buttons.
424
- * The button is rendered alongside each item without wrapping in a row,
425
- * preserving flat primitive form values.
426
- */
427
- autoRemoveButton = computed(() => {
257
+ autoRemoveButton = computed(() => {
428
258
  const metadata = getNormalizedArrayMetadata(this.field());
429
259
  return metadata?.autoRemoveButton;
430
260
  }, ...(ngDevMode ? [{ debugName: "autoRemoveButton" }] : /* istanbul ignore next */ []));
431
- /**
432
- * For primitive array items, the key of the value field template (e.g., 'value').
433
- * Used to wrap the FormControl in the item context so getFieldTree(key) works.
434
- * Returns undefined for object arrays (FormGroup items have natural child navigation).
435
- *
436
- * Detection order:
437
- * 1. Normalization metadata (for simplified arrays — always available at normalization time)
438
- * 2. Existing item definitions (non-empty full-API arrays)
439
- * 3. Dynamically discovered key from handleAddFromEvent (empty full-API arrays)
440
- *
441
- * Uses linkedSignal: recomputes when field() changes, supports manual set()
442
- * for the dynamic discovery case.
443
- */
444
- primitiveFieldKey = linkedSignal(() => {
445
- // Priority 1: Normalization metadata (simplified arrays always have this)
446
- const metadata = getNormalizedArrayMetadata(this.field());
261
+ primitiveFieldKey = linkedSignal(() => {
262
+ const metadata = getNormalizedArrayMetadata(this.field());
447
263
  if (metadata?.primitiveFieldKey) {
448
264
  return metadata.primitiveFieldKey;
449
265
  }
450
- // Priority 2: Existing item definitions (full-API arrays with initial items)
451
- const definitions = this.field().fields || [];
266
+ const definitions = this.field().fields || [];
452
267
  if (definitions.length > 0 && !Array.isArray(definitions[0])) {
453
268
  return definitions[0].key;
454
269
  }
455
270
  return undefined;
456
271
  }, ...(ngDevMode ? [{ debugName: "primitiveFieldKey" }] : /* istanbul ignore next */ []));
457
- /**
458
- * Normalized item templates WITHOUT auto-remove button appended.
459
- * Each element is normalized to an array: single FieldDef → [FieldDef], array stays as-is.
460
- *
461
- * Used by moveItem() to stash raw templates into the templateRegistry, preserving
462
- * the invariant that registry entries are pre-synthesis (withAutoRemove() adds the
463
- * button during resolution).
464
- */
465
- rawItemTemplates = computed(() => {
272
+ rawItemTemplates = computed(() => {
466
273
  const arrayField = this.field();
467
274
  const definitions = arrayField.fields || [];
468
275
  return definitions.map((def) => {
469
276
  return (Array.isArray(def) ? def : [def]);
470
277
  });
471
278
  }, ...(ngDevMode ? [{ debugName: "rawItemTemplates" }] : /* istanbul ignore next */ []));
472
- /**
473
- * Resolves the effective fallback template for this array — used when the form
474
- * value contains an item with no registered template (i.e., items that were
475
- * neither added via event handlers nor covered by a positional entry in `fields`).
476
- *
477
- * Populated from `SimplifiedArrayField.template` via normalization metadata, so every
478
- * simplified array gets an automatic default. Returns undefined for full-API arrays;
479
- * `createResolveItemObservable` then falls back to warn-and-drop.
480
- *
481
- * Homogeneous arrays only — all fallback items receive the same template.
482
- */
483
- fallbackTemplate = computed(() => {
279
+ fallbackTemplate = computed(() => {
484
280
  const raw = getNormalizedArrayMetadata(this.field())?.template;
485
281
  if (!raw)
486
282
  return undefined;
487
283
  return (Array.isArray(raw) ? [...raw] : [raw]);
488
284
  }, ...(ngDevMode ? [{ debugName: "fallbackTemplate" }] : /* istanbul ignore next */ []));
489
- /**
490
- * Gets the item templates (field definitions) for the array.
491
- * Each element can be either:
492
- * - A single FieldDef (primitive item) - normalized to [FieldDef]
493
- * - An array of FieldDefs (object item) - used as-is
494
- *
495
- * When auto-remove is configured, the remove button is appended to each item's
496
- * template list for rendering. This is purely visual — the form schema uses the
497
- * original primitive item definition (single FieldDef → FormControl → flat value).
498
- *
499
- * Returns normalized templates where all items are arrays for consistent handling.
500
- */
501
- itemTemplates = computed(() => {
285
+ itemTemplates = computed(() => {
502
286
  const raw = this.rawItemTemplates();
503
287
  const removeButton = this.autoRemoveButton();
504
288
  if (!removeButton)
@@ -516,38 +300,22 @@ class ArrayFieldComponent {
516
300
  const arrayFieldTree = parentForm[arrayKey];
517
301
  if (!arrayFieldTree)
518
302
  return arrayValue.map(() => null);
519
- // Access array items via bracket notation - Angular Signal Forms arrays support this
520
- const items = [];
303
+ const items = [];
521
304
  for (let i = 0; i < arrayValue.length; i++) {
522
- // Access item FieldTree directly via numeric indexing (ArrayFieldTree supports this)
523
- const itemFieldTree = arrayFieldTree[i];
305
+ const itemFieldTree = arrayFieldTree[i];
524
306
  items.push(itemFieldTree ?? null);
525
307
  }
526
308
  return items;
527
309
  }, ...(ngDevMode ? [{ debugName: "arrayFieldTrees" }] : /* istanbul ignore next */ []));
528
- // ─────────────────────────────────────────────────────────────────────────────
529
- // State Signals
530
- // ─────────────────────────────────────────────────────────────────────────────
531
- resolvedItemsSignal = signal([], ...(ngDevMode ? [{ debugName: "resolvedItemsSignal" }] : /* istanbul ignore next */ []));
532
- // Explicit state machine for async resolution + componentInitialized emission.
533
- // Replaces the prior updateVersion + pendingInitializationCycle + settledInitializationCycle trio.
534
- fsm = new ArrayFieldStateMachine();
535
- /**
536
- * Map of item IDs to their current positions. O(1) lookup vs O(n) indexOf().
537
- * Used by child linkedSignals to reactively track their position in the array.
538
- */
539
- itemPositionMap = computed(() => {
310
+ resolvedItemsSignal = signal([], ...(ngDevMode ? [{ debugName: "resolvedItemsSignal" }] : /* istanbul ignore next */ []));
311
+ fsm = new ArrayFieldStateMachine();
312
+ itemPositionMap = computed(() => {
540
313
  const items = this.resolvedItemsSignal();
541
314
  return new Map(items.map((item, idx) => [item.id, idx]));
542
315
  }, ...(ngDevMode ? [{ debugName: "itemPositionMap" }] : /* istanbul ignore next */ []));
543
- /** Read-only view of resolved items for template consumption. */
544
- resolvedItems = computed(() => this.resolvedItemsSignal(), ...(ngDevMode ? [{ debugName: "resolvedItems" }] : /* istanbul ignore next */ []));
316
+ resolvedItems = computed(() => this.resolvedItemsSignal(), ...(ngDevMode ? [{ debugName: "resolvedItems" }] : /* istanbul ignore next */ []));
545
317
  allResolvedFieldsRenderReady = computed(() => this.resolvedItems().every((item) => item.fields.every((field) => field.renderReady())), ...(ngDevMode ? [{ debugName: "allResolvedFieldsRenderReady" }] : /* istanbul ignore next */ []));
546
- /**
547
- * Whether the array has reached its configured maxLength.
548
- * Exposed as a public signal so add-button components can bind [disabled]="atMaxLength()".
549
- */
550
- atMaxLength = computed(() => {
318
+ atMaxLength = computed(() => {
551
319
  const maxLength = this.field().maxLength;
552
320
  if (maxLength === undefined)
553
321
  return false;
@@ -555,30 +323,13 @@ class ArrayFieldComponent {
555
323
  const currentArray = getArrayValue(this.parentFieldSignalContext.value(), arrayKey);
556
324
  return currentArray.length >= maxLength;
557
325
  }, ...(ngDevMode ? [{ debugName: "atMaxLength" }] : /* istanbul ignore next */ []));
558
- // ─────────────────────────────────────────────────────────────────────────────
559
- // Auto-remove Cache
560
- // ─────────────────────────────────────────────────────────────────────────────
561
- /**
562
- * Caches the result of appending the auto-remove button to template arrays.
563
- * Keyed by template array reference — cache hits occur during recreate/resolution
564
- * operations where stored template references are reused. Add operations via
565
- * handleAddFromEvent always create a fresh `[...template]` copy (line ~277),
566
- * so each add is a cache miss by design (the spread is needed for mutability).
567
- */
568
- autoRemoveCache = new WeakMap();
569
- // ─────────────────────────────────────────────────────────────────────────────
570
- // Constructor
571
- // ─────────────────────────────────────────────────────────────────────────────
572
- constructor() {
326
+ autoRemoveCache = new WeakMap();
327
+ constructor() {
573
328
  this.setupEffects();
574
329
  this.setupEventHandlers();
575
330
  }
576
- // ─────────────────────────────────────────────────────────────────────────────
577
- // Private Methods
578
- // ─────────────────────────────────────────────────────────────────────────────
579
- setupEffects() {
580
- // Sync field components when array data changes
581
- explicitEffect([this.arrayFieldTrees], ([fieldTrees]) => {
331
+ setupEffects() {
332
+ explicitEffect([this.arrayFieldTrees], ([fieldTrees]) => {
582
333
  this.performDifferentialUpdate(fieldTrees);
583
334
  });
584
335
  explicitEffect([this.fsm.state, this.allResolvedFieldsRenderReady], ([, allReady]) => {
@@ -592,8 +343,7 @@ class ArrayFieldComponent {
592
343
  .pipe(takeUntilDestroyed())
593
344
  .subscribe((action) => {
594
345
  if (action.action === 'add') {
595
- // Template can be single FieldDef (primitive) or FieldDef[] (object)
596
- const templates = Array.isArray(action.template) ? action.template : action.template ? [action.template] : [];
346
+ const templates = Array.isArray(action.template) ? action.template : action.template ? [action.template] : [];
597
347
  if (templates.length === 0) {
598
348
  this.logger.error(`Cannot add item to array '${this.key()}': template is REQUIRED. ` +
599
349
  'Buttons must specify an explicit template property. ' +
@@ -610,25 +360,11 @@ class ArrayFieldComponent {
610
360
  }
611
361
  });
612
362
  }
613
- /**
614
- * Handles add operations from events (append, prepend, insert).
615
- * Creates resolved items FIRST, then updates form value.
616
- * This ensures prepend/insert work correctly - differential update sees "none"
617
- * because resolved items count already matches the new array length.
618
- *
619
- * Supports both primitive (single FieldDef) and object (FieldDef[]) templates.
620
- */
621
- async handleAddFromEvent(template, index) {
622
- // Normalize template to mutable array for consistent handling.
623
- // A single FieldDef with valueHandling: 'flatten' (container, row) is an object item
624
- // whose children should be flattened — NOT a primitive item. Only true leaf fields
625
- // (input, checkbox, etc.) are primitive when passed as a single FieldDef.
626
- const templates = Array.isArray(template) ? [...template] : [template];
363
+ async handleAddFromEvent(template, index) {
364
+ const templates = Array.isArray(template) ? [...template] : [template];
627
365
  const isSingleField = !Array.isArray(template);
628
366
  const isPrimitiveItem = isSingleField && getFieldValueHandling(templates[0].type, this.rawFieldRegistry()) !== 'flatten';
629
- // Track primitive field key for full-API arrays that start empty.
630
- // Simplified arrays already have this info from normalization metadata.
631
- if (isPrimitiveItem && templates[0].key && !this.primitiveFieldKey()) {
367
+ if (isPrimitiveItem && templates[0].key && !this.primitiveFieldKey()) {
632
368
  this.primitiveFieldKey.set(templates[0].key);
633
369
  }
634
370
  if (templates.length === 0) {
@@ -641,27 +377,22 @@ class ArrayFieldComponent {
641
377
  const currentValue = parentForm().value();
642
378
  const currentArray = getArrayValue(currentValue, arrayKey);
643
379
  const insertIndex = index !== undefined ? Math.min(index, currentArray.length) : currentArray.length;
644
- // Compute default value
645
- let value;
380
+ let value;
646
381
  if (isPrimitiveItem) {
647
- // Primitive item: single field's value is extracted directly
648
- value = getFieldDefaultValue(templates[0], this.rawFieldRegistry());
382
+ value = getFieldDefaultValue(templates[0], this.rawFieldRegistry());
649
383
  }
650
384
  else {
651
- // Object item: merge all template defaults into an object
652
- value = {};
385
+ value = {};
653
386
  for (const templateField of templates) {
654
387
  const rawValue = getFieldDefaultValue(templateField, this.rawFieldRegistry());
655
388
  const valueHandling = getFieldValueHandling(templateField.type, this.rawFieldRegistry());
656
389
  const isContainer = templateField.type === 'group' || templateField.type === 'row' || templateField.type === 'container';
657
390
  if (isContainer) {
658
391
  if (isGroupField(templateField)) {
659
- // Groups wrap their fields under the group key
660
- value = { ...value, [templateField.key]: rawValue };
392
+ value = { ...value, [templateField.key]: rawValue };
661
393
  }
662
394
  else {
663
- // Rows and containers flatten their fields directly
664
- value = { ...value, ...rawValue };
395
+ value = { ...value, ...rawValue };
665
396
  }
666
397
  }
667
398
  else if (valueHandling === 'include' && templateField.key) {
@@ -669,13 +400,9 @@ class ArrayFieldComponent {
669
400
  }
670
401
  }
671
402
  }
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' });
675
- // Append auto-remove button to resolution templates (for rendering only)
676
- const resolveTemplates = this.withAutoRemove(templates);
677
- // Resolve the new item
678
- const resolvedItem = await firstValueFrom(resolveArrayItem({
403
+ const run = this.fsm.dispatch({ kind: 'append' });
404
+ const resolveTemplates = this.withAutoRemove(templates);
405
+ const resolvedItem = await firstValueFrom(resolveArrayItem({
679
406
  index: insertIndex,
680
407
  templates: resolveTemplates,
681
408
  arrayField: this.field(),
@@ -694,52 +421,35 @@ class ArrayFieldComponent {
694
421
  if (run.isStale() || !resolvedItem) {
695
422
  return;
696
423
  }
697
- // Store the template used for this item so it can be re-used during recreate operations
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);
701
- // Insert resolved item at correct position
702
- this.resolvedItemsSignal.update((current) => {
424
+ this.slot().templates.set(resolvedItem.id, templates);
425
+ this.slot().itemOrder.splice(insertIndex, 0, resolvedItem.id);
426
+ this.resolvedItemsSignal.update((current) => {
703
427
  const newItems = [...current];
704
428
  newItems.splice(insertIndex, 0, resolvedItem);
705
429
  return newItems;
706
430
  });
707
- // Update form value - differential update sees "none" (lengths match)
708
- const newArray = [...currentArray];
431
+ const newArray = [...currentArray];
709
432
  newArray.splice(insertIndex, 0, value);
710
- // The `as any` is required because Angular Signal Forms uses complex conditional types
711
- // that cannot infer the correct type when setting a dynamically-keyed property
712
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
713
- parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
433
+ parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
714
434
  }
715
435
  performDifferentialUpdate(fieldTrees) {
716
436
  const resolvedItems = this.resolvedItemsSignal();
717
437
  const operation = determineDifferentialOperation(resolvedItems, fieldTrees.length);
718
- // "none" operations (value-only changes) don't kick the state machine — they shouldn't
719
- // interfere with in-flight async resolutions.
720
- if (operation.type === 'none') {
438
+ if (operation.type === 'none') {
721
439
  return;
722
440
  }
723
441
  switch (operation.type) {
724
442
  case 'clear': {
725
- // Clean up template registry when all items are removed
726
- this.slot().templates.clear();
443
+ this.slot().templates.clear();
727
444
  this.resolvedItemsSignal.set([]);
728
445
  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) {
446
+ if (resolvedItems.length === 0 && !this.fsm.hasEverEmitted) {
732
447
  run.resolve();
733
448
  }
734
449
  break;
735
450
  }
736
451
  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();
452
+ const slot = this.slot();
743
453
  const useSurvival = slot.itemOrder.length === fieldTrees.length && fieldTrees.length > 0;
744
454
  const positionalTemplates = useSurvival ? slot.itemOrder.map((id) => slot.templates.get(id)) : undefined;
745
455
  const existingItemIds = useSurvival ? slot.itemOrder.slice() : undefined;
@@ -753,13 +463,8 @@ class ArrayFieldComponent {
753
463
  break;
754
464
  }
755
465
  case 'recreate': {
756
- // Capture templates by item ID so each item is recreated with its original template,
757
- // even after move operations that change positions. Registry entries (from dynamic adds
758
- // or moves) take priority over positional itemTemplates lookup.
759
- const positionalTemplates = resolvedItems.map((item) => {
760
- // Registry entries (from dynamic adds or moves) take priority;
761
- // unmoved initial items return undefined → falls through to itemTemplates[idx] during resolve.
762
- return this.slot().templates.get(item.id);
466
+ const positionalTemplates = resolvedItems.map((item) => {
467
+ return this.slot().templates.get(item.id);
763
468
  });
764
469
  this.resolvedItemsSignal.set([]);
765
470
  const run = this.fsm.dispatch({ kind: 'recreate' });
@@ -773,18 +478,15 @@ class ArrayFieldComponent {
773
478
  this.resolvedItemsSignal.set([]);
774
479
  return;
775
480
  }
776
- // Wrap each item observable to catch individual errors
777
- const safeItemObservables = fieldTrees.map((_, i) => this.createResolveItemObservable(i, positionalTemplates?.[i], existingItemIds?.[i]).pipe(catchError((error) => {
481
+ const safeItemObservables = fieldTrees.map((_, i) => this.createResolveItemObservable(i, positionalTemplates?.[i], existingItemIds?.[i]).pipe(catchError((error) => {
778
482
  this.logger.error(`Failed to resolve array item at index ${i}:`, error);
779
483
  return of(undefined);
780
484
  })));
781
485
  try {
782
486
  const items = await firstValueFrom(forkJoin(safeItemObservables).pipe(map((items) => items.filter((item) => item !== undefined))));
783
487
  if (!run.isStale()) {
784
- // Update template registry with new item IDs for dynamically added items
785
- if (positionalTemplates) {
786
- // Clean up old entries and add new ones
787
- const newItemIds = new Set(items.map((item) => item.id));
488
+ if (positionalTemplates) {
489
+ const newItemIds = new Set(items.map((item) => item.id));
788
490
  for (const existingId of this.slot().templates.keys()) {
789
491
  if (!newItemIds.has(existingId)) {
790
492
  this.slot().templates.delete(existingId);
@@ -797,15 +499,12 @@ class ArrayFieldComponent {
797
499
  }
798
500
  });
799
501
  }
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();
502
+ const slot = this.slot();
803
503
  slot.itemOrder.length = 0;
804
504
  for (const item of items)
805
505
  slot.itemOrder.push(item.id);
806
506
  this.resolvedItemsSignal.set(items);
807
- // Mark this run settled so the emit-effect can fire componentInitialized once allReady is true.
808
- run.resolve();
507
+ run.resolve();
809
508
  }
810
509
  }
811
510
  catch (err) {
@@ -817,8 +516,7 @@ class ArrayFieldComponent {
817
516
  const itemsToResolve = fieldTrees.slice(startIndex, endIndex);
818
517
  if (itemsToResolve.length === 0)
819
518
  return;
820
- // Wrap each item observable to catch individual errors
821
- const safeItemObservables = itemsToResolve.map((_, i) => {
519
+ const safeItemObservables = itemsToResolve.map((_, i) => {
822
520
  const index = startIndex + i;
823
521
  return this.createResolveItemObservable(index).pipe(catchError((error) => {
824
522
  this.logger.error(`Failed to resolve array item at index ${index}:`, error);
@@ -838,24 +536,10 @@ class ArrayFieldComponent {
838
536
  this.logger.error('Failed to append array items:', err);
839
537
  }
840
538
  }
841
- /**
842
- * Creates an observable that resolves a single array item.
843
- *
844
- * Template resolution order:
845
- * 1. Use overrideTemplate if provided (from recreate with stored templates)
846
- * 2. Use itemTemplates[index] if within defined templates range
847
- * 3. Use the simplified-array fallback template (from normalization metadata) for
848
- * untracked items present in the form value (e.g., external `value.set`, parent
849
- * two-way binding, initial values beyond what was declared via simplified `value`).
850
- * Registers the resolved item in templateRegistry so subsequent recreates use
851
- * Priority 1.
852
- * 4. Return undefined (item cannot be resolved without a template)
853
- */
854
- createResolveItemObservable(index, overrideTemplate, existingItemId) {
539
+ createResolveItemObservable(index, overrideTemplate, existingItemId) {
855
540
  const itemTemplates = this.itemTemplates();
856
541
  const primitiveKey = this.primitiveFieldKey();
857
- // Priority 1: Use override template (from recreate with stored templates)
858
- if (overrideTemplate && overrideTemplate.length > 0) {
542
+ if (overrideTemplate && overrideTemplate.length > 0) {
859
543
  return resolveArrayItem({
860
544
  index,
861
545
  templates: this.withAutoRemove(overrideTemplate),
@@ -871,8 +555,7 @@ class ArrayFieldComponent {
871
555
  primitiveFieldKey: primitiveKey,
872
556
  });
873
557
  }
874
- // Priority 2: Use defined template at this index
875
- const templates = itemTemplates[index];
558
+ const templates = itemTemplates[index];
876
559
  if (templates && templates.length > 0) {
877
560
  return resolveArrayItem({
878
561
  index,
@@ -889,9 +572,7 @@ class ArrayFieldComponent {
889
572
  primitiveFieldKey: primitiveKey,
890
573
  });
891
574
  }
892
- // Priority 3: Use the metadata fallback template for untracked items in the form value.
893
- // Homogeneous only — every fallback item receives the same template regardless of value shape.
894
- const fallback = this.fallbackTemplate();
575
+ const fallback = this.fallbackTemplate();
895
576
  if (fallback && fallback.length > 0) {
896
577
  return resolveArrayItem({
897
578
  index,
@@ -907,32 +588,14 @@ class ArrayFieldComponent {
907
588
  existingItemId,
908
589
  primitiveFieldKey: primitiveKey,
909
590
  }).pipe(tap((item) => {
910
- // Register the fallback template against the generated item id so subsequent
911
- // recreates hit Priority 1 instead of re-resolving via this branch.
912
- if (item)
591
+ if (item)
913
592
  this.slot().templates.set(item.id, fallback);
914
593
  }));
915
594
  }
916
- // Priority 4: no template available. Full-API arrays are positional by design —
917
- // `fields` declares one template per item, so values extending past `fields.length`
918
- // (e.g., from external `value.set`, parent two-way binding, or initial values on a
919
- // `fields: []` array) have no template to render. Use the simplified array API
920
- // (`template` + `value`) when you need homogeneous arrays with value-driven items.
921
- this.logger.warn(`No template found for array item at index ${index}. This likely occured for a Full-API array element that was created from the DynamicForm's value being set directly, which is currently not supported. Consider using the simplified array API.`);
595
+ this.logger.warn(`No template found for array item at index ${index}. This likely occured for a Full-API array element that was created from the DynamicForm's value being set directly, which is currently not supported. Consider using the simplified array API.`);
922
596
  return of(undefined);
923
597
  }
924
- /**
925
- * Appends the auto-remove button to a templates array for rendering.
926
- * The button is added for visual rendering only — it doesn't affect the form schema.
927
- * Original templates are stored in templateRegistry WITHOUT the remove button,
928
- * so this method is called during resolution to add it dynamically.
929
- *
930
- * Uses a WeakMap cache keyed by template array reference. Cache hits occur
931
- * during recreate/resolution paths where stored templates are reused.
932
- * Add operations always pass a fresh `[...template]` copy, so they miss
933
- * the cache intentionally (the copy is needed for mutable item construction).
934
- */
935
- withAutoRemove(templates) {
598
+ withAutoRemove(templates) {
936
599
  const removeButton = this.autoRemoveButton();
937
600
  if (!removeButton)
938
601
  return templates;
@@ -943,15 +606,7 @@ class ArrayFieldComponent {
943
606
  this.autoRemoveCache.set(templates, cached);
944
607
  return cached;
945
608
  }
946
- /**
947
- * Handles move operations — reorders an existing item without destroying it.
948
- * Updates resolvedItems and form value atomically. Since the array length
949
- * doesn't change, `determineDifferentialOperation` returns 'none' and no
950
- * recreate is triggered. The `@for` loop tracks by `item.id`, so Angular
951
- * moves the DOM node instead of destroying/recreating. The `itemPositionMap`
952
- * computed auto-recomputes, propagating new indices to child linkedSignals.
953
- */
954
- moveItem(fromIndex, toIndex) {
609
+ moveItem(fromIndex, toIndex) {
955
610
  const arrayKey = this.field().key;
956
611
  const parentForm = this.parentFieldSignalContext.form;
957
612
  const currentValue = parentForm().value();
@@ -967,14 +622,7 @@ class ArrayFieldComponent {
967
622
  }
968
623
  if (fromIndex === toIndex)
969
624
  return;
970
- // Register raw (pre-auto-remove) templates for all items whose position will change.
971
- // Initial items use itemTemplates[currentIndex] during recreate, but after
972
- // a move their position no longer matches their original template. Stashing
973
- // the template by item ID lets the recreate path resolve the correct one.
974
- // We use rawItemTemplates (without auto-remove button) because the recreate path
975
- // calls withAutoRemove() during resolution — storing the synthesized version would
976
- // cause duplicate remove buttons.
977
- const rawTemplates = this.rawItemTemplates();
625
+ const rawTemplates = this.rawItemTemplates();
978
626
  const currentItems = this.resolvedItemsSignal();
979
627
  const lo = Math.min(fromIndex, toIndex);
980
628
  const hi = Math.max(fromIndex, toIndex);
@@ -984,37 +632,26 @@ class ArrayFieldComponent {
984
632
  this.slot().templates.set(item.id, rawTemplates[i]);
985
633
  }
986
634
  }
987
- // Reorder resolvedItems — splice preserves object identity (no destroy/recreate)
988
- this.resolvedItemsSignal.update((current) => {
635
+ this.resolvedItemsSignal.update((current) => {
989
636
  const newItems = [...current];
990
637
  const [moved] = newItems.splice(fromIndex, 1);
991
638
  newItems.splice(toIndex, 0, moved);
992
639
  return newItems;
993
640
  });
994
- // Keep slot.itemOrder in sync so the post-recreate survival path sees the moved order.
995
- this.slot().moveItem(fromIndex, toIndex);
996
- // Reorder form value array the same way
997
- const newArray = [...currentArray];
641
+ this.slot().moveItem(fromIndex, toIndex);
642
+ const newArray = [...currentArray];
998
643
  const [movedValue] = newArray.splice(fromIndex, 1);
999
644
  newArray.splice(toIndex, 0, movedValue);
1000
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1001
- parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
645
+ parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
1002
646
  }
1003
- /**
1004
- * Handles remove operations from events (pop, shift, removeAt).
1005
- * Updates resolvedItems FIRST, then form value - this ensures differential
1006
- * update sees "none" (lengths match) and avoids unnecessary recreates.
1007
- * Remaining items' linkedSignal indices auto-update via itemPositionMap.
1008
- */
1009
- removeItem(index) {
647
+ removeItem(index) {
1010
648
  const arrayKey = this.field().key;
1011
649
  const parentForm = this.parentFieldSignalContext.form;
1012
650
  const currentValue = parentForm().value();
1013
651
  const currentArray = getArrayValue(currentValue, arrayKey);
1014
652
  if (currentArray.length === 0)
1015
653
  return;
1016
- // When index is undefined or -1, remove the last item.
1017
- let removeIndex;
654
+ let removeIndex;
1018
655
  if (index === undefined || index === -1) {
1019
656
  removeIndex = currentArray.length - 1;
1020
657
  }
@@ -1025,33 +662,19 @@ class ArrayFieldComponent {
1025
662
  else {
1026
663
  removeIndex = index;
1027
664
  }
1028
- // Update resolvedItems FIRST - remove the item at the specified index.
1029
- // This ensures differential update sees "none" (lengths already match).
1030
- // Remaining items' linkedSignal indices auto-update via itemPositionMap.
1031
- //
1032
- // When the item is removed from resolvedItemsSignal, the @for loop removes the DOM element
1033
- // and NgComponentOutlet destroys the component view, triggering DestroyRef callbacks.
1034
- // Async validators use Angular's resource API tied to the form-level schema path — when the
1035
- // array value is updated below, the resource's params re-evaluate. If the removed item's path
1036
- // no longer exists, params returns undefined, cancelling the pending validation automatically.
1037
- const removedItem = this.resolvedItemsSignal()[removeIndex];
665
+ const removedItem = this.resolvedItemsSignal()[removeIndex];
1038
666
  if (removedItem) {
1039
667
  this.slot().templates.delete(removedItem.id);
1040
668
  }
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);
669
+ this.slot().removeAt(removeIndex);
1044
670
  this.resolvedItemsSignal.update((current) => {
1045
671
  const newItems = [...current];
1046
672
  newItems.splice(removeIndex, 1);
1047
673
  return newItems;
1048
674
  });
1049
- // Update the parent form with the new array value
1050
- const newArray = [...currentArray];
675
+ const newArray = [...currentArray];
1051
676
  newArray.splice(removeIndex, 1);
1052
- // The `as any` is required due to Angular Signal Forms' complex conditional types
1053
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1054
- parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
677
+ parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
1055
678
  }
1056
679
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1057
680
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", 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: `
@@ -1100,4 +723,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1100
723
  }], 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 }] }] } });
1101
724
 
1102
725
  export { ArrayFieldComponent, ArrayFieldComponent as default };
1103
- //# sourceMappingURL=ng-forge-dynamic-forms-array-field.component-GiaVHO9N.mjs.map
726
+ //# sourceMappingURL=ng-forge-dynamic-forms-array-field.component-D6GEsH72.mjs.map