@ng-forge/dynamic-forms 1.0.0-rc.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -34
  3. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-D5qCzJsL.mjs → ng-forge-dynamic-forms-array-field.component-CIheZh11.mjs} +26 -20
  4. package/fesm2022/ng-forge-dynamic-forms-array-field.component-CIheZh11.mjs.map +1 -0
  5. package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-D6ntLtW9.mjs → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C9aVachF.mjs} +39 -14
  6. package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C9aVachF.mjs.map +1 -0
  7. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-nB6KFqVs.mjs → ng-forge-dynamic-forms-container-field.component-DNafCSO8.mjs} +5 -5
  8. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-nB6KFqVs.mjs.map → ng-forge-dynamic-forms-container-field.component-DNafCSO8.mjs.map} +1 -1
  9. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-AScQAJTQ.mjs → ng-forge-dynamic-forms-css-wrapper.component-BmHGlmES.mjs} +4 -4
  10. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-AScQAJTQ.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-BmHGlmES.mjs.map} +1 -1
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-Bq6Sgd6Z.mjs → ng-forge-dynamic-forms-group-field.component-CKVIw2K5.mjs} +5 -5
  12. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-Bq6Sgd6Z.mjs.map → ng-forge-dynamic-forms-group-field.component-CKVIw2K5.mjs.map} +1 -1
  13. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +34 -34
  14. package/fesm2022/ng-forge-dynamic-forms-internal.mjs +1397 -1401
  15. package/fesm2022/ng-forge-dynamic-forms-internal.mjs.map +1 -1
  16. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-C59pvc26.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.mjs} +70 -57
  17. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.mjs.map +1 -0
  18. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-DRaxbiqA.mjs → ng-forge-dynamic-forms-page-field.component-BRixkgyS.mjs} +5 -5
  19. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-DRaxbiqA.mjs.map → ng-forge-dynamic-forms-page-field.component-BRixkgyS.mjs.map} +1 -1
  20. package/fesm2022/{ng-forge-dynamic-forms-row-wrapper.component-BlVa801t.mjs → ng-forge-dynamic-forms-row-wrapper.component-BBWmqQA_.mjs} +4 -4
  21. package/fesm2022/{ng-forge-dynamic-forms-row-wrapper.component-BlVa801t.mjs.map → ng-forge-dynamic-forms-row-wrapper.component-BBWmqQA_.mjs.map} +1 -1
  22. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CNCpmf5_.mjs → ng-forge-dynamic-forms-text-field.component-C-f80sbk.mjs} +4 -4
  23. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CNCpmf5_.mjs.map → ng-forge-dynamic-forms-text-field.component-C-f80sbk.mjs.map} +1 -1
  24. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  25. package/package.json +6 -6
  26. package/types/ng-forge-dynamic-forms-integration.d.ts +1 -1
  27. package/types/ng-forge-dynamic-forms-internal.d.ts +21 -200
  28. package/fesm2022/ng-forge-dynamic-forms-array-field.component-D5qCzJsL.mjs.map +0 -1
  29. package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-D6ntLtW9.mjs.map +0 -1
  30. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-C59pvc26.mjs.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ng-forge
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -9,41 +9,34 @@ Core library for building type-safe, dynamic Angular forms with signal forms int
9
9
  [![CI](https://img.shields.io/github/actions/workflow/status/ng-forge/ng-forge/ci.yml?branch=main)](https://github.com/ng-forge/ng-forge/actions/workflows/ci.yml)
10
10
  [![npm version](https://img.shields.io/npm/v/@ng-forge/dynamic-forms.svg)](https://www.npmjs.com/package/@ng-forge/dynamic-forms)
11
11
  [![npm downloads](https://img.shields.io/npm/dm/@ng-forge/dynamic-forms.svg)](https://www.npmjs.com/package/@ng-forge/dynamic-forms)
12
- [![Angular](https://img.shields.io/badge/Angular-21+-DD0031.svg)](https://angular.dev)
12
+ [![Angular](https://img.shields.io/badge/Angular-22+-DD0031.svg)](https://angular.dev)
13
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
14
14
  [![Discord](https://img.shields.io/discord/1494269650555371582?color=7289da&label=Discord&logo=discord&logoColor=white)](https://discord.gg/qpzzvFagj3)
15
15
 
16
- > **Experimental API Notice:** This library uses Angular's experimental Signal Forms API.
17
- > Angular may introduce breaking changes in patch releases. Check the [compatibility matrix](#compatibility) below.
16
+ > **Stability:** This library is built on Angular Signal Forms, which is stable as of Angular v22.
17
+ > Check the [compatibility matrix](#compatibility) below for supported Angular versions.
18
18
 
19
19
  ## Compatibility
20
20
 
21
- | @ng-forge/dynamic-forms | Angular |
22
- | ----------------------- | ------------- |
23
- | 0.9.x | >=21.2.0 |
24
- | 0.8.x | >=21.2.0 |
25
- | 0.7.x | >=21.2.0 |
26
- | 0.6.x | >=21.1.0 |
27
- | 0.5.x | >=21.1.0 |
28
- | 0.4.x | >=21.1.0 |
29
- | 0.3.x | 21.0.7-21.0.x |
30
- | 0.2.x | 21.0.6 |
31
- | 0.1.1+ | 21.0.2-21.0.5 |
32
- | 0.1.0 | 21.0.0-21.0.1 |
21
+ | Angular | @ng-forge/dynamic-forms |
22
+ | ------- | ----------------------- |
23
+ | 22.x | 1.x |
24
+ | 21.x | 0.x (experimental) |
25
+
26
+ Signal Forms are stable as of Angular 22. The `0.x` line targets Angular 21, where Signal Forms were still experimental and could change in patch releases. Each release pins its Angular requirement via `peerDependencies`; npm warns on a mismatch.
33
27
 
34
28
  ## Supported entrypoints
35
29
 
36
- | Entrypoint | Audience | Stability |
37
- | ------------------------------------- | ------------------- | -------------------------------- |
38
- | `@ng-forge/dynamic-forms` | Form consumers | Public API, governed by semver |
39
- | `@ng-forge/dynamic-forms/schema` | Form consumers | Public API, governed by semver |
40
- | `@ng-forge/dynamic-forms/integration` | UI adapter authors | Public API, governed by semver |
41
- | `@ng-forge/dynamic-forms/internal` | None (build-time) | Unsupported, no semver guarantee |
42
- | `@ng-forge/dynamic-forms/testing` | None (test helpers) | Unsupported, no semver guarantee |
30
+ | Entrypoint | Audience | Stability |
31
+ | ------------------------------------- | ------------------ | -------------------------------- |
32
+ | `@ng-forge/dynamic-forms` | Form consumers | Public API, governed by semver |
33
+ | `@ng-forge/dynamic-forms/schema` | Form consumers | Public API, governed by semver |
34
+ | `@ng-forge/dynamic-forms/integration` | UI adapter authors | Public API, governed by semver |
35
+ | `@ng-forge/dynamic-forms/internal` | None (build-time) | Unsupported, no semver guarantee |
43
36
 
44
37
  The `/schema` entrypoint is a supported, semver-governed surface for using [Standard Schema](https://standardschema.dev) validation libraries (Zod, Valibot, ArkType) with dynamic forms via `standardSchema()`.
45
38
 
46
- The `/internal` and `/testing` entrypoints are unsupported build and test surfaces with no semver guarantee. They are published only because shared DI tokens, services, and config types must keep a single compiled identity across the public bundles. Their contents may change or be removed in any release, including patch releases.
39
+ The `/internal` entrypoint is an unsupported build surface with no semver guarantee. It is published only because shared DI tokens, services, and config types must keep a single compiled identity across the public bundles. Its contents may change or be removed in any release, including patch releases.
47
40
 
48
41
  Import only from `@ng-forge/dynamic-forms` and `@ng-forge/dynamic-forms/schema` (form consumers) or `@ng-forge/dynamic-forms/integration` (UI adapter authors).
49
42
 
@@ -90,7 +83,7 @@ export class UserFormComponent {
90
83
 
91
84
  ## Features
92
85
 
93
- - **Signal Forms** - Native Angular 21+ signal forms integration
86
+ - **Signal Forms** - Native Angular Signal Forms integration
94
87
  - **Type-Safe** - Full TypeScript inference with `InferFormValue`
95
88
  - **UI Agnostic** - Bring your own UI or use official integrations
96
89
  - **Validation** - Shorthand validators (`required`, `email`, `minLength`) and custom validators
@@ -111,19 +104,20 @@ This core library requires a UI integration. Choose one:
111
104
  | [@ng-forge/dynamic-forms-primeng](https://www.npmjs.com/package/@ng-forge/dynamic-forms-primeng) | PrimeNG |
112
105
  | [@ng-forge/dynamic-forms-ionic](https://www.npmjs.com/package/@ng-forge/dynamic-forms-ionic) | Ionic |
113
106
 
114
- Or [create your own](https://ng-forge.com/dynamic-forms/advanced/custom-integrations).
107
+ Or [create your own](https://ng-forge.com/material/building-an-adapter).
115
108
 
116
109
  ## Documentation
117
110
 
118
- - [Installation](https://ng-forge.com/dynamic-forms/installation)
119
- - [Field Types](https://ng-forge.com/dynamic-forms/field-types)
120
- - [Validation](https://ng-forge.com/dynamic-forms/validation/basics)
121
- - [Conditional Logic](https://ng-forge.com/dynamic-forms/dynamic-behavior/overview)
122
- - [Value Derivation](https://ng-forge.com/dynamic-forms/dynamic-behavior/derivation)
123
- - [Type Safety](https://ng-forge.com/dynamic-forms/advanced/basics)
124
- - [Events](https://ng-forge.com/dynamic-forms/advanced/events)
125
- - [i18n](https://ng-forge.com/dynamic-forms/dynamic-behavior/i18n)
126
- - [Custom Integrations](https://ng-forge.com/dynamic-forms/advanced/custom-integrations)
111
+ - [Feature overview](https://ng-forge.com/material/feature-overview)
112
+ - [Installation](https://ng-forge.com/material/getting-started)
113
+ - [Field Types](https://ng-forge.com/material/field-types/text-inputs)
114
+ - [Validation](https://ng-forge.com/material/validation/basics)
115
+ - [Conditional Logic](https://ng-forge.com/material/dynamic-behavior/conditional-logic)
116
+ - [Value Derivation](https://ng-forge.com/material/dynamic-behavior/derivation/values)
117
+ - [Type Safety](https://ng-forge.com/material/recipes/type-safety)
118
+ - [Events](https://ng-forge.com/material/recipes/events)
119
+ - [i18n](https://ng-forge.com/material/dynamic-behavior/i18n)
120
+ - [Custom Integrations](https://ng-forge.com/material/building-an-adapter)
127
121
 
128
122
  ## Changelog
129
123
 
@@ -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 { m as mapFieldToInputs, e as createHiddenSignal, f as createRenderReadySignal, i as injectFieldRegistry, A as ArrayItemRegistryService, g as getNormalizedArrayMetadata, h as getFieldDefaultValue, D as DfFieldOutlet } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms-C59pvc26.mjs';
3
+ import { m as mapFieldToInputs, e as createHiddenSignal, f as createRenderReadySignal, i as injectFieldRegistry, A as ArrayItemRegistryService, g as getNormalizedArrayMetadata, h as getFieldDefaultValue, D as DfFieldOutlet } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.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 { RootFormRegistryService, isEqual, FIELD_SIGNAL_CONTEXT, ARRAY_CONTEXT, GROUP_CONTEXT, DynamicFormLogger, injectFieldSignalContext, EventBus, getFieldValueHandling, isGroupField } from '@ng-forge/dynamic-forms/internal';
7
+ import { isEqual, FIELD_SIGNAL_CONTEXT, ARRAY_CONTEXT, GROUP_CONTEXT, DynamicFormLogger, injectFieldSignalContext, EventBus, getFieldValueHandling, isGroupField } from '@ng-forge/dynamic-forms/internal';
8
8
  import { e as emitComponentInitialized } from './ng-forge-dynamic-forms-emit-initialization-DhHwU6FJ.mjs';
9
9
 
10
10
  class ArrayFieldStateMachine {
@@ -76,14 +76,10 @@ function determineDifferentialOperation(currentItems, newLength) {
76
76
 
77
77
  function createArrayItemInjectorAndInputs(options) {
78
78
  const { template, indexSignal, parentFieldSignalContext, parentInjector, registry, arrayField, primitiveFieldKey } = options;
79
- const rootFormRegistry = parentInjector.get(RootFormRegistryService);
80
- const itemFormAccessor = computed(() => {
81
- const rootForm = rootFormRegistry.rootForm();
82
- if (!rootForm) {
83
- return undefined;
84
- }
79
+ const itemFormAccessor = computed(() => {
80
+ const parentForm = parentFieldSignalContext.form;
85
81
  const index = indexSignal();
86
- const arrayFieldTree = rootForm[arrayField.key];
82
+ const arrayFieldTree = parentForm[arrayField.key];
87
83
  if (!arrayFieldTree) {
88
84
  return undefined;
89
85
  }
@@ -307,13 +303,25 @@ class ArrayFieldComponent {
307
303
  });
308
304
  }, /* @ts-ignore */
309
305
  ...(ngDevMode ? [{ debugName: "itemTemplates" }] : /* istanbul ignore next */ []));
310
- arrayFieldTrees = computed(() => {
311
- const arrayKey = this.field().key;
306
+ arrayFieldTree = computed(() => {
312
307
  const parentForm = this.parentFieldSignalContext.form;
313
- const arrayValue = getArrayValue(this.parentFieldSignalContext.value(), arrayKey);
308
+ return parentForm[this.field().key];
309
+ }, /* @ts-ignore */
310
+ ...(ngDevMode ? [{ debugName: "arrayFieldTree" }] : /* istanbul ignore next */ []));
311
+ currentArrayValue = computed(() => {
312
+ const tree = this.arrayFieldTree();
313
+ if (tree) {
314
+ const value = tree().value();
315
+ return Array.isArray(value) ? value : [];
316
+ }
317
+ return getArrayValue(this.parentFieldSignalContext.value(), this.field().key);
318
+ }, /* @ts-ignore */
319
+ ...(ngDevMode ? [{ debugName: "currentArrayValue" }] : /* istanbul ignore next */ []));
320
+ arrayFieldTrees = computed(() => {
321
+ const arrayValue = this.currentArrayValue();
314
322
  if (arrayValue.length === 0)
315
323
  return [];
316
- const arrayFieldTree = parentForm[arrayKey];
324
+ const arrayFieldTree = this.arrayFieldTree();
317
325
  if (!arrayFieldTree)
318
326
  return arrayValue.map(() => null);
319
327
  const items = [];
@@ -340,9 +348,7 @@ class ArrayFieldComponent {
340
348
  const maxLength = this.field().maxLength;
341
349
  if (maxLength === undefined)
342
350
  return false;
343
- const arrayKey = this.field().key;
344
- const currentArray = getArrayValue(this.parentFieldSignalContext.value(), arrayKey);
345
- return currentArray.length >= maxLength;
351
+ return this.currentArrayValue().length >= maxLength;
346
352
  }, /* @ts-ignore */
347
353
  ...(ngDevMode ? [{ debugName: "atMaxLength" }] : /* istanbul ignore next */ []));
348
354
  autoRemoveCache = new WeakMap();
@@ -698,8 +704,8 @@ class ArrayFieldComponent {
698
704
  newArray.splice(removeIndex, 1);
699
705
  parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
700
706
  }
701
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
702
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", 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: `
707
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
708
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", 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: `
703
709
  @for (item of resolvedItems(); track item.id; let i = $index) {
704
710
  <div
705
711
  class="df-array-item"
@@ -717,7 +723,7 @@ class ArrayFieldComponent {
717
723
  }
718
724
  `, isInline: true, styles: [".df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);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, .5rem);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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-sm, .5rem)}.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, 12),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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-md, .5rem)}.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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-lg, .5rem)}.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, .5rem);--df-grid-row-gap: var(--df-grid-row-gap-xl, .5rem)}.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"], dependencies: [{ kind: "directive", type: DfFieldOutlet, selector: "[dfFieldOutlet]", inputs: ["dfFieldOutlet", "dfFieldOutletEnvironmentInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
719
725
  }
720
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ArrayFieldComponent, decorators: [{
726
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ArrayFieldComponent, decorators: [{
721
727
  type: Component,
722
728
  args: [{ selector: 'array-field', imports: [DfFieldOutlet], template: `
723
729
  @for (item of resolvedItems(); track item.id; let i = $index) {
@@ -745,4 +751,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImpor
745
751
  }], 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 }] }] } });
746
752
 
747
753
  export { ArrayFieldComponent, ArrayFieldComponent as default };
748
- //# sourceMappingURL=ng-forge-dynamic-forms-array-field.component-D5qCzJsL.mjs.map
754
+ //# sourceMappingURL=ng-forge-dynamic-forms-array-field.component-CIheZh11.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ng-forge-dynamic-forms-array-field.component-CIheZh11.mjs","mappings":";;;;;;;;;;;;;;;;AAqCW;AACA;IAGT;;;AAIA;;AAEE;AAEA;AACE;AACA;AACA;AACE;;AAEF;;;;AAOA;;;AAGE;AACE;;;;;AAMR,IAKA;;AACgD;AAC9C;AAA6C;AAC7C;AACA;AACA;AACA;;AAEH;;AC3CK;AACJ;AACA;AACF;AAcM;AACJ;AAEA;AACE;;AAGF;;;YAQA;QAEE;;AAGF;gCAKE;;IAIF;AACF;;AChDM;AAGJ;oBAOA;AAEI;AACA;;;AAIE;;AAGF;AACF;;;;;;;;AAWD;QAID;AACE;AACF;AAEA;AACF;AAeA;AACE;QAIA;;AAEE;AACA;AACE;;AAEF;;QAKF;AACA;;AAGE;AACA;AACE;AACE;gCAGA;;;4DAME;AACA;AACE;;gHAME;;;4BASA;;;;AAIA;;AAGF;;;;AAIL;AACD;wEAOA;AACD;AACF;AACH;AAOA;;AC1HM;;AAkBJ;AACE;;YAMF;IAGA;AACE;;;;IAKF;AAGM;AACE;;;AAKA;;AAGF;;;;;;;;AAQC;;AAIC;;;;AAKA;;;;AAIA;AACA;;AAEJ;AAEE;;;AAIM;;AAIN;;AAKN;AAEI;QAGA;AACE;;;AAIA;AACA;;;AAIR;;ACnIA;;;;;;;;;AAyBA;AACE;AACE;;AAEA;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;;AAEA;;;AAGJ;AASM;AACJ;AAIF;;ACXc;YAKK;;;AAGA;AACE;AACF;AACA;AACA;wBAOA;;;;;;;AAUjB;;;;YAOS;;AAEP;AACA;;;;;AAKF,IAMiB;;;;;AAKjB,IAKiB;;AAGf;;;;AAMA;AACE;;AAGF;;;AAGF,IAIiB;AACf;AACA;AAEA;AACE;AACF;;;AAGF,IAKiB;;AAEf;AAAU;;;;AAIZ,IAMiB;AACf;AACA;AAEA;AAAmB;AAEnB;AACE;AACF;;;IAIe;AACf;;;;AAIF,IAMiB;AACf;;AAEE;AACA;;AAEF;;;AAGe;AACf;AAEA;AAA6B;AAE7B;AACA;;;AAIA;YAEE;AACA;;AAGF;;;;;QAUe;AAEjB,IAIiB;AACf;;;;;;AAOe;;AAIjB,IAIS;;;AAEsB;;;;YAQ/B,IAOiB;YAMjB;;;;;QAWE;AACE;AACF;AAEA;;AAEI;;AAEJ;;;AAIA;;AAEG;AACC;gBAEE;AACA;;;AAIM;;;AAIN;;AACK;;;;AAGL;;AAEJ;;AAGJ,IAMQ;;;;gBAWN;AACE;;AAGF;AACE;AAEI;;;;AAMN;AACA;;;QAKA;;YAIE;;;;AAIA;;AAEE;AACA;;AAGE;wBAEE;;;;;;;AAMF;;;;gBAON;;QAMA;AAEI;AACA;AACA;;;;AAIA;;AAEA;;AAEA;;;AAIE;;;;;QAUN;QAEA;;AAIE;;AAEA;AACF;QAGA;;wBAKA;;AAGM;AACN;;gBAKA;;;AAIA;;;AAII;AACA;gCAGA;;;;;;gFAWA;AACA;AACA;AACA;AACA;AACA;;;;AAIA;AACA;;;;;wCAUE;AACF;AAEA;AACA;;;;;;;AAaJ;AACE;;;QAKF;;AAIM;;AAKN;AACE;AAIA;;oBAII;AACA;;;;;;AAME;;AAEE;;AAEJ;;gCAIF;AACA;;;AAEA;;;;;;AAMF;;;;;AAWF;;;AAIE;AACA;;AAGI;;AAGN;AAEA;AACE;AAIA;AACE;;;AAEA;;;;;;;IAQE;AAKN;AACA;;AAIE;;AAEE;AACA;;;;AAIA;;AAEA;;;AAGA;AACD;;QAIH;;AAEE;;AAEE;AACA;;;;AAIA;;AAEA;;;AAGA;AACD;;gBAKH;;AAEE;;AAEE;AACA;;;;AAIA;;AAEA;;;AAGA;;gCAKE;AAAU;;;;AAahB;;AAGF,IAMQ;AACN;AACA;AAAmB;;AAGnB;AAAY;AAEZ;;AAEA;;AAGF;;AAUE;AACA;;AAEA;;AAGE;;;;AAOA;;;;;wDAeF;AACA;;;AAGA;AACE;;AAEE;;;;AAMF;AACA;;AAEA;AACF;;QAKA;AACA;;QAIA;;AAGF,IAMQ;;AAEN;AACA;;AAGA;;QAGA;;AAEE;;;AAEA;;;;;;;;AAmBA;;;;AAMA;AACA;AACA;AACF;QAGA;AACA;gBAIA;;;AA7uBiB;;;;;;;;;;;;;;;;AAXlB;;;;AAlBS;;;;;;;;;;;;;;;;;AAqBR;AACA;AACA;AACA;AACA","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms/src/lib/fields/array/array-field-state-machine.ts","../../../../packages/dynamic-forms/src/lib/utils/array-field/array-field.types.ts","../../../../packages/dynamic-forms/src/lib/utils/array-field/create-array-item-injector.ts","../../../../packages/dynamic-forms/src/lib/utils/array-field/resolve-array-item.ts","../../../../packages/dynamic-forms/src/lib/utils/array-field/array-event-handler.ts","../../../../packages/dynamic-forms/src/lib/fields/array/array-field.component.ts"],"sourcesContent":["import { signal, Signal } from '@angular/core';\n\n/**\n * Explicit state for `ArrayFieldComponent`'s async-resolution lifecycle. Replaces\n * three ad-hoc signals (`updateVersion`, `pendingInitializationCycle`,\n * `settledInitializationCycle`) plus scattered `currentVersion !== updateVersion`\n * cancellation guards with one machine.\n *\n * @internal\n */\nexport type ArrayMachineKind = 'idle' | 'pending' | 'settled';\n\nexport type ArrayOperation =\n /** Form value emptied to length 0. Use `run.resolve()` if this clear should emit the init pulse. */\n | { kind: 'clear' }\n /** First resolution from an idle state (empty → N items). */\n | { kind: 'initial' }\n /** Dynamic add via event — bumps `runId` for cancellation but stays in the current state. */\n | { kind: 'append' }\n /** Full re-resolve (e.g. items removed mid-array — positions need full rebuild). */\n | { kind: 'recreate' };\n\nexport interface RunHandle {\n /** Unique id of this dispatch. Compared against `fsm.runId()` to detect staleness. */\n readonly id: number;\n /** True when a newer `dispatch()` has happened since this handle was created. */\n isStale(): boolean;\n /** Transition this run from `pending` → `settled`. No-op if stale or already settled. */\n resolve(): void;\n}\n\nexport class ArrayFieldStateMachine {\n private readonly _state = signal<ArrayMachineKind>('idle');\n private readonly _runId = signal(0);\n private lastEmittedRunId = -1;\n private _hasEverEmitted = false;\n\n readonly state: Signal<ArrayMachineKind> = this._state.asReadonly();\n readonly runId: Signal<number> = this._runId.asReadonly();\n\n /** True after the first `componentInitialized` pulse has fired for this component. */\n get hasEverEmitted(): boolean {\n return this._hasEverEmitted;\n }\n\n dispatch(op: ArrayOperation): RunHandle {\n const nextId = this._runId() + 1;\n this._runId.set(nextId);\n\n switch (op.kind) {\n case 'clear':\n case 'initial':\n case 'recreate':\n this._state.set('pending');\n break;\n case 'append':\n // append doesn't reset state — mirrors today's behavior where\n // appendItems leaves the init-cycle tracking alone.\n break;\n }\n\n return {\n id: nextId,\n isStale: () => this._runId() !== nextId,\n resolve: () => {\n if (this._runId() === nextId && this._state() === 'pending') {\n this._state.set('settled');\n }\n },\n };\n }\n\n /**\n * Edge-trigger for the emit-effect. Returns true exactly once per `settled` run\n * when `allReady` is true. Side-effect: records the run as emitted and transitions\n * the state back to `idle`. Subsequent calls without a fresh dispatch return false.\n */\n shouldEmit(allReady: boolean): boolean {\n if (this._state() !== 'settled' || !allReady) return false;\n if (this.lastEmittedRunId === this._runId()) return false;\n this.lastEmittedRunId = this._runId();\n this._hasEverEmitted = true;\n this._state.set('idle');\n return true;\n }\n}\n","import { Injector, Signal, type Type } from '@angular/core';\nimport type { FieldDef } from '@ng-forge/dynamic-forms/internal';\n\n/** A single field within a resolved array item. */\nexport interface ResolvedArrayItemField {\n /** Field key (used for tracking and test IDs). */\n key: string;\n /** Original field definition — used by DfFieldOutlet to resolve wrappers. */\n fieldDef: FieldDef<unknown>;\n /** The loaded component type. */\n component: Type<unknown>;\n /** Injector providing ARRAY_CONTEXT and FIELD_SIGNAL_CONTEXT. */\n injector: Injector;\n /** Inputs signal for DfFieldOutlet — evaluated in template for reactivity. */\n inputs: Signal<Record<string, unknown>>;\n /** Whether required mapped inputs are available for safe component instantiation. */\n renderReady: Signal<boolean>;\n /**\n * Whether the array-item field is currently hidden — mirrors `ResolvedField.hidden`\n * so the same `@if (!field.hidden())` template gate works for top-level and\n * array-item fields.\n */\n hidden: Signal<boolean>;\n}\n\n/**\n * Resolved array item ready for declarative rendering with ngComponentOutlet.\n * Each item has a stable unique ID and a linkedSignal-based index that\n * automatically updates when items are added/removed.\n * Supports multiple fields per array item (e.g., name + email without a wrapper).\n */\nexport interface ResolvedArrayItem {\n /** Unique identifier for this item (stable across position changes). */\n id: string;\n /** All fields to render for this array item. */\n fields: ResolvedArrayItemField[];\n}\n\n/**\n * Safely extracts an array value from a model object.\n * Returns empty array if the key doesn't exist or value isn't an array.\n */\nexport function getArrayValue<TModel>(value: Partial<TModel> | undefined, key: string): unknown[] {\n const arrayValue = (value as Record<string, unknown>)?.[key];\n return Array.isArray(arrayValue) ? arrayValue : [];\n}\n\n/**\n * Discriminated union of possible differential update operations.\n * Enables optimized array updates without full recreation when possible.\n */\nexport type DifferentialUpdateOperation =\n | { type: 'clear' }\n | { type: 'initial'; fieldTreesLength: number }\n | { type: 'append'; startIndex: number; endIndex: number }\n | { type: 'recreate' }\n | { type: 'none' };\n\n/** Determines the optimal differential update operation based on current and new state. */\nexport function determineDifferentialOperation(currentItems: ResolvedArrayItem[], newLength: number): DifferentialUpdateOperation {\n const currentLength = currentItems.length;\n\n if (newLength === 0) {\n return { type: 'clear' };\n }\n\n if (currentLength === 0) {\n return { type: 'initial', fieldTreesLength: newLength };\n }\n\n // For all length changes, we optimize by assuming existing items stay in place.\n // Items handle their own value updates via linkedSignal - no need to compare snapshots.\n // Snapshot comparison was causing false \"recreate\" when existing items had been edited.\n\n if (newLength > currentLength) {\n // Items added - append new ones, existing items stay\n return { type: 'append', startIndex: currentLength, endIndex: newLength };\n }\n\n if (newLength < currentLength) {\n // Items removed - must recreate because we don't know which items were removed.\n // shift() removes from index 0, removeAtIndex() removes from any position.\n // Each item's local form is initialized with its value at creation time and\n // doesn't reactively track parent array changes.\n return { type: 'recreate' };\n }\n\n // Same length - no structural change, items update via linkedSignal\n return { type: 'none' };\n}\n","import { computed, Injector, runInInjectionContext, signal, Signal } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\nimport { ArrayField } from '@ng-forge/dynamic-forms/internal';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayContext, FieldSignalContext, GroupContext } from '@ng-forge/dynamic-forms/internal';\nimport { ARRAY_CONTEXT, FIELD_SIGNAL_CONTEXT, GROUP_CONTEXT } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { mapFieldToInputs } from '../field-mapper/field-mapper';\nimport { isEqual } from '@ng-forge/dynamic-forms/internal';\n\n/** Options for creating an array item injector. */\nexport interface CreateArrayItemInjectorOptions<TModel extends Record<string, unknown>> {\n /** The field template defining the array item structure. */\n template: FieldDef<unknown>;\n /** Signal containing the current index (uses linkedSignal for auto-updates). */\n indexSignal: Signal<number>;\n /** Parent context for accessing form state and values. */\n parentFieldSignalContext: FieldSignalContext<TModel>;\n /** Parent injector for creating scoped child injector. */\n parentInjector: Injector;\n /** Field registry for looking up field type definitions. */\n registry: Map<string, FieldTypeDefinition>;\n /** The array field definition containing this item. */\n arrayField: ArrayField;\n /**\n * For primitive array items, the key of the value field in the template (e.g., 'value').\n * When set, the form getter wraps the raw FormControl in `{ [primitiveFieldKey]: FormControl }`\n * so that `getFieldTree(key)` can navigate to the control by key.\n */\n primitiveFieldKey?: string;\n}\n\n/** Result of creating an array item injector. */\nexport interface ArrayItemInjectorResult {\n /** Scoped injector providing ARRAY_CONTEXT and FIELD_SIGNAL_CONTEXT. */\n injector: Injector;\n /** Inputs signal for ngComponentOutlet, mapped from the field template. Undefined for componentless fields. */\n inputs: Signal<Record<string, unknown>> | undefined;\n}\n\n/** Creates an injector and inputs for an array item. */\nexport function createArrayItemInjectorAndInputs<TModel extends Record<string, unknown>>(\n options: CreateArrayItemInjectorOptions<TModel>,\n): ArrayItemInjectorResult {\n const { template, indexSignal, parentFieldSignalContext, parentInjector, registry, arrayField, primitiveFieldKey } = options;\n\n // Create a computed that derives the array item's FieldTree from the parent\n // context's form. The parent form is group-scoped for arrays nested in groups\n // (group > row > array) and the root form otherwise, so navigating by the bare\n // array key is correct in both scopes.\n // Uses isEqual to prevent unnecessary re-computation when index changes but value is same.\n const itemFormAccessor = computed(\n () => {\n const parentForm = parentFieldSignalContext.form as Record<string, unknown>;\n const index = indexSignal();\n // Navigate: parentForm['arrayKey'][index]\n const arrayFieldTree = parentForm[arrayField.key];\n if (!arrayFieldTree) {\n return undefined;\n }\n\n return (arrayFieldTree as unknown[])[index] as FieldTree<unknown> | undefined;\n },\n { equal: isEqual },\n );\n\n const injector = createItemInjector({\n itemFormAccessor,\n indexSignal,\n parentFieldSignalContext,\n parentInjector,\n arrayField,\n primitiveFieldKey,\n });\n\n // mapFieldToInputs automatically reads ARRAY_CONTEXT from the injector\n // and applies the index suffix to keys for unique DOM IDs\n const inputs = runInInjectionContext(injector, () => {\n return mapFieldToInputs(template, registry);\n });\n\n return { injector, inputs };\n}\n\ninterface CreateItemInjectorOptions<TModel extends Record<string, unknown>> {\n itemFormAccessor: Signal<FieldTree<unknown> | undefined>;\n indexSignal: Signal<number>;\n parentFieldSignalContext: FieldSignalContext<TModel>;\n parentInjector: Injector;\n arrayField: ArrayField;\n primitiveFieldKey?: string;\n}\n\n/**\n * Creates a scoped injector for an array item.\n * Provides both FIELD_SIGNAL_CONTEXT (for form access) and ARRAY_CONTEXT (for position awareness).\n */\nfunction createItemInjector<TModel extends Record<string, unknown>>(options: CreateItemInjectorOptions<TModel>): Injector {\n const { itemFormAccessor, indexSignal, parentFieldSignalContext, parentInjector, arrayField, primitiveFieldKey } = options;\n\n // Use getter for formValue to ensure it's always current, not a stale snapshot.\n // Components accessing arrayContext.formValue will get the current value.\n const arrayContext: ArrayContext = {\n arrayKey: arrayField.key,\n index: indexSignal,\n get formValue() {\n return parentFieldSignalContext.value();\n },\n field: arrayField,\n };\n\n // Cache for the primitive wrapper object — avoids creating a new { [primitiveFieldKey]: raw }\n // on every form getter access, which would defeat reference-equality checks downstream.\n let cachedPrimitiveRaw: FieldTree<unknown> | undefined;\n let cachedPrimitiveWrapped: FieldTree<Record<string, unknown>> | undefined;\n\n return Injector.create({\n parent: parentInjector,\n providers: [\n {\n provide: FIELD_SIGNAL_CONTEXT,\n // Angular resolves `Injector` in deps as the child injector being created,\n // so no temporal gap exists — the injector reference is immediately available.\n useFactory: (injector: Injector): FieldSignalContext<Record<string, unknown>> => ({\n injector,\n value: parentFieldSignalContext.value,\n // Array items don't propagate parent default values because each item's defaults\n // are determined by its template fields at creation time (via getFieldDefaultValue).\n // The parent's defaultValues contain top-level form defaults, not per-item defaults.\n defaultValues: () => ({}),\n get form(): FieldTree<Record<string, unknown>> {\n const raw = itemFormAccessor();\n if (!raw) {\n // During initialization or transitions, the FieldTree may not be available yet.\n // This occurs in the window between item creation and Angular's form tree construction.\n // Return an empty object so that downstream getFieldTree(key) calls return undefined\n // rather than causing a runtime error on a truly undefined value.\n return {} as FieldTree<Record<string, unknown>>;\n }\n\n // For primitive array items, the FieldTree is a FormControl (the item IS the control).\n // Wrap it in an object so getFieldTree(primitiveFieldKey) returns the FormControl itself.\n // Without this, getFieldTree('value') would access FormControl['value'] (the WritableSignal),\n // not a child FieldTree, causing NG0950 errors.\n if (primitiveFieldKey) {\n // Cache the wrapper to preserve reference identity when raw hasn't changed\n if (raw !== cachedPrimitiveRaw) {\n cachedPrimitiveRaw = raw;\n cachedPrimitiveWrapped = { [primitiveFieldKey]: raw } as FieldTree<Record<string, unknown>>;\n }\n return cachedPrimitiveWrapped!;\n }\n\n return raw as FieldTree<Record<string, unknown>>;\n },\n }),\n deps: [Injector],\n },\n { provide: ARRAY_CONTEXT, useValue: arrayContext },\n // Reset GROUP_CONTEXT at the array boundary: inner groups inside each\n // item start their path fresh, matching how value-derivation and\n // property-derivation collectors key entries\n // (`{arrayKey}.$.{innerGroupPath}.{fieldKey}`). Without this, an outer\n // group around the array would leak into descendants' DOM IDs and\n // override-store keys, drifting from the collector.\n { provide: GROUP_CONTEXT, useValue: EMPTY_GROUP_CONTEXT },\n ],\n });\n}\n\n/**\n * Sentinel `GroupContext` provided at array item boundaries. Static `signal('')`\n * means `applyGroupPrefix` no-ops (empty path) and any inner group composes its\n * own path from this empty base.\n */\nconst EMPTY_GROUP_CONTEXT: GroupContext = { groupPath: signal('') };\n","import { DestroyRef, Injector, linkedSignal, Signal, Type } from '@angular/core';\nimport { catchError, forkJoin, from, map, Observable, of } from 'rxjs';\nimport { ArrayField } from '@ng-forge/dynamic-forms/internal';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FieldSignalContext } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { ResolvedArrayItem, ResolvedArrayItemField } from './array-field.types';\nimport { createArrayItemInjectorAndInputs } from './create-array-item-injector';\nimport { createHiddenSignal, createRenderReadySignal } from '../resolve-field/resolve-field';\n\n/** Options for resolving an array item. */\nexport interface ResolveArrayItemOptions<TModel extends Record<string, unknown>> {\n /** The initial index of this item in the array. */\n index: number;\n /** The field templates defining the array item structure (supports multiple sibling fields). */\n templates: FieldDef<unknown>[];\n /** The array field definition containing this item. */\n arrayField: ArrayField;\n /** Map signal for O(1) position lookup. Keys are item IDs, values are indices. */\n itemPositionMap: Signal<Map<string, number>>;\n /** Parent context for accessing form state and values. */\n parentFieldSignalContext: FieldSignalContext<TModel>;\n /** Parent injector for creating scoped child injector. */\n parentInjector: Injector;\n /** Field registry for looking up field type definitions. */\n registry: Map<string, FieldTypeDefinition>;\n /** DestroyRef to abort resolution if component is destroyed. */\n destroyRef: DestroyRef;\n /**\n * Function to async load the component for the field type.\n * Returns undefined for componentless fields (e.g., hidden fields).\n */\n loadTypeComponent: (type: string) => Promise<Type<unknown> | undefined>;\n /**\n * Function to generate unique item IDs. Scoped to the array component instance\n * to ensure SSR hydration compatibility (server and client generate same IDs).\n */\n generateItemId: () => string;\n /**\n * Optional pre-existing id to reuse for this item instead of calling `generateItemId`.\n * Used by the recovery path on component recreate so that templates stored under the\n * surviving id in `ArrayItemRegistryService` continue to map cleanly.\n */\n existingItemId?: string;\n /**\n * For primitive array items, the key of the value field in the template.\n * Passed through to createArrayItemInjectorAndInputs for form context wrapping.\n */\n primitiveFieldKey?: string;\n}\n\n/** Resolves a single array item with all its fields for declarative rendering. */\nexport function resolveArrayItem<TModel extends Record<string, unknown>>(\n options: ResolveArrayItemOptions<TModel>,\n): Observable<ResolvedArrayItem | undefined> {\n const {\n index,\n templates,\n arrayField,\n itemPositionMap,\n parentFieldSignalContext,\n parentInjector,\n registry,\n destroyRef,\n loadTypeComponent,\n generateItemId,\n existingItemId,\n primitiveFieldKey,\n } = options;\n\n if (templates.length === 0) {\n return of(undefined);\n }\n\n // Generate ONE id for this array item (shared by all fields for tracking), unless\n // the caller is rehydrating an existing item — in which case reuse its id so the\n // form-scoped template registry continues to map cleanly.\n const itemId = existingItemId ?? generateItemId();\n\n // O(1) position lookup via Map instead of O(n) indexOf()\n const indexSignal = linkedSignal(() => {\n const positionMap = itemPositionMap();\n return positionMap.get(itemId) ?? index;\n });\n\n // Resolve all templates in parallel\n const fieldObservables = templates.map((template) =>\n from(loadTypeComponent(template.type)).pipe(\n map((component): ResolvedArrayItemField | undefined => {\n if (destroyRef.destroyed) {\n return undefined;\n }\n\n // Componentless fields (e.g., hidden) return undefined - nothing to render\n if (!component) {\n return undefined;\n }\n\n const { injector, inputs } = createArrayItemInjectorAndInputs({\n template,\n indexSignal,\n parentFieldSignalContext,\n parentInjector,\n registry,\n arrayField,\n primitiveFieldKey,\n });\n\n // Array item templates should always have inputs (componentless fields are handled above)\n if (!inputs) {\n return undefined;\n }\n\n return {\n key: template.key,\n fieldDef: template,\n component,\n injector,\n inputs,\n renderReady: createRenderReadySignal(inputs, registry.get(template.type)),\n hidden: createHiddenSignal(inputs),\n };\n }),\n catchError((error) => {\n if (!destroyRef.destroyed) {\n const logger = parentInjector.get(DynamicFormLogger);\n logger.error(\n `Failed to load component for field type '${template.type}' at index ${index} ` +\n `within array '${arrayField.key}'. Ensure the field type is registered in your field registry.`,\n error,\n );\n }\n return of(undefined);\n }),\n ),\n );\n\n return forkJoin(fieldObservables).pipe(\n map((fields) => {\n const validFields = fields.filter((f): f is ResolvedArrayItemField => f !== undefined);\n\n // If no fields resolved, return undefined\n if (validFields.length === 0) {\n return undefined;\n }\n\n return {\n id: itemId,\n fields: validFields,\n };\n }),\n );\n}\n","import { filter, map, Observable } from 'rxjs';\nimport { AppendArrayItemEvent } from '../../events/constants/append-array-item.event';\nimport { PrependArrayItemEvent } from '../../events/constants/prepend-array-item.event';\nimport { InsertArrayItemEvent } from '../../events/constants/insert-array-item.event';\nimport { MoveArrayItemEvent } from '../../events/constants/move-array-item.event';\nimport { PopArrayItemEvent } from '../../events/constants/pop-array-item.event';\nimport { ShiftArrayItemEvent } from '../../events/constants/shift-array-item.event';\nimport { RemoveAtIndexEvent } from '../../events/constants/remove-at-index.event';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\n\n/** Union type of all array manipulation events. */\nexport type ArrayEvent =\n | AppendArrayItemEvent\n | PrependArrayItemEvent\n | InsertArrayItemEvent\n | MoveArrayItemEvent\n | PopArrayItemEvent\n | ShiftArrayItemEvent\n | RemoveAtIndexEvent;\n\n/** All array event type discriminants. */\nconst ARRAY_EVENT_TYPES: ArrayEvent['type'][] = [\n 'append-array-item',\n 'prepend-array-item',\n 'insert-array-item',\n 'move-array-item',\n 'pop-array-item',\n 'shift-array-item',\n 'remove-at-index',\n];\n\n/**\n * Normalized action representing an array manipulation.\n * Template can be:\n * - Single FieldDef for primitive items (field's value is extracted directly)\n * - Array of FieldDefs for object items (fields merged into object)\n */\nexport type ArrayAction =\n | { action: 'add'; template: FieldDef<unknown> | readonly FieldDef<unknown>[]; index?: number }\n | { action: 'remove'; index?: number }\n | { action: 'move'; fromIndex: number; toIndex: number };\n\n/**\n * Converts an array event to a normalized action.\n * Uses a switch for full type safety with discriminated union narrowing.\n */\nfunction toArrayAction(event: ArrayEvent): ArrayAction {\n switch (event.type) {\n case 'append-array-item':\n return { action: 'add', template: event.template };\n case 'prepend-array-item':\n return { action: 'add', template: event.template, index: 0 };\n case 'insert-array-item':\n return { action: 'add', template: event.template, index: event.index };\n case 'move-array-item':\n return { action: 'move', fromIndex: event.fromIndex, toIndex: event.toIndex };\n case 'pop-array-item':\n return { action: 'remove' };\n case 'shift-array-item':\n return { action: 'remove', index: 0 };\n case 'remove-at-index':\n return { action: 'remove', index: event.index };\n }\n}\n\n/**\n * Creates an observable stream of normalized array actions for a specific array key.\n *\n * @param eventBus - The event bus to subscribe to\n * @param arrayKey - Function returning the array key to filter events for\n * @returns Observable of normalized ArrayAction objects\n */\nexport function observeArrayActions(eventBus: EventBus, arrayKey: () => string): Observable<ArrayAction> {\n return eventBus.on<ArrayEvent>(ARRAY_EVENT_TYPES).pipe(\n filter((event) => event.arrayKey === arrayKey()),\n map(toArrayAction),\n );\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n computed,\n DestroyRef,\n EnvironmentInjector,\n inject,\n Injector,\n input,\n linkedSignal,\n Signal,\n signal,\n} from '@angular/core';\nimport { DfFieldOutlet } from '../../directives/df-field-outlet/df-field-outlet.directive';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { catchError, firstValueFrom, forkJoin, map, Observable, of, tap } from 'rxjs';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { ArrayField, ArrayItemDefinition, ArrayItemTemplate } from '@ng-forge/dynamic-forms/internal';\nimport { isGroupField } from '@ng-forge/dynamic-forms/internal';\nimport { injectFieldRegistry } from '../../utils/inject-field-registry/inject-field-registry';\nimport { FieldTree } from '@angular/forms/signals';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { getFieldDefaultValue } from '../../utils/default-value/default-value';\nimport { getFieldValueHandling } from '@ng-forge/dynamic-forms/internal';\nimport { emitComponentInitialized } from '../../utils/emit-initialization/emit-initialization';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { injectFieldSignalContext } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayItemRegistryService } from '../../core/registry/array-item-registry.service';\nimport { ArrayFieldStateMachine, RunHandle } from './array-field-state-machine';\nimport { determineDifferentialOperation, getArrayValue, ResolvedArrayItem } from '../../utils/array-field/array-field.types';\nimport { resolveArrayItem } from '../../utils/array-field/resolve-array-item';\nimport { observeArrayActions } from '../../utils/array-field/array-event-handler';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayFieldTree } from '@ng-forge/dynamic-forms/internal';\nimport { getNormalizedArrayMetadata } from '../../utils/array-field/normalized-array-metadata';\n\n/** Container component for rendering dynamic arrays of fields. */\n@Component({\n selector: 'array-field',\n imports: [DfFieldOutlet],\n template: `\n @for (item of resolvedItems(); track item.id; let i = $index) {\n <div\n class=\"df-array-item\"\n role=\"group\"\n [attr.aria-label]=\"'Item ' + (i + 1)\"\n [attr.data-array-item-id]=\"item.id\"\n [attr.data-array-item-index]=\"i\"\n >\n @for (field of item.fields; track $index) {\n @if (!field.hidden()) {\n <ng-container *dfFieldOutlet=\"field; environmentInjector: environmentInjector\" />\n }\n }\n </div>\n }\n `,\n styleUrl: './array-field.component.scss',\n host: {\n '[class]': 'hostClasses()',\n '[class.df-container-hidden]': 'hidden()',\n '[attr.aria-hidden]': 'hidden() || null',\n '[id]': '`${key()}`',\n '[attr.data-testid]': 'key()',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class ArrayFieldComponent<TModel extends Record<string, unknown> = Record<string, unknown>> {\n // ─────────────────────────────────────────────────────────────────────────────\n // Dependencies\n // ─────────────────────────────────────────────────────────────────────────────\n\n private readonly destroyRef = inject(DestroyRef);\n private readonly fieldRegistry = injectFieldRegistry();\n private readonly parentFieldSignalContext = injectFieldSignalContext<TModel>();\n private readonly parentInjector = inject(Injector);\n protected readonly environmentInjector = inject(EnvironmentInjector);\n private readonly eventBus = inject(EventBus);\n private readonly logger = inject(DynamicFormLogger);\n private readonly arrayItemRegistry = inject(ArrayItemRegistryService);\n // Form-scoped slot keyed by the array's path. Survives this component's lifecycle so that\n // `@if`-gated arrays can be destroyed and recreated without losing dynamically-added items.\n // NOTE: keyed by `field().key` for now — nested arrays with the same key in different scopes\n // would currently collide. Today's per-component-provider model has the same constraint\n // (no nested-array hide/show tests exist). Path-aware keying is a follow-up.\n // `field()` is `input.required`, so the slot reference is resolved lazily.\n private readonly slot = computed(() => this.arrayItemRegistry.slotFor(this.field().key));\n /** Bound id-generator reference threaded to `resolveArrayItem` so callsites stay terse. */\n private readonly generateItemId = (): string => this.slot().nextId();\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Inputs\n // ─────────────────────────────────────────────────────────────────────────────\n\n field = input.required<ArrayField>();\n key = input.required<string>();\n className = input<string>();\n hidden = input(false);\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Computed Signals\n // ─────────────────────────────────────────────────────────────────────────────\n\n readonly hostClasses = computed(() => {\n const base = 'df-field df-array';\n const custom = this.className();\n return custom ? `${base} ${custom}` : base;\n });\n\n private readonly rawFieldRegistry = computed(() => this.fieldRegistry.raw);\n\n /**\n * Gets the auto-remove button FieldDef from normalization metadata.\n * Set by simplified array normalization for primitive arrays with remove buttons.\n * The button is rendered alongside each item without wrapping in a row,\n * preserving flat primitive form values.\n */\n private readonly autoRemoveButton = computed<FieldDef<unknown> | undefined>(() => {\n const metadata = getNormalizedArrayMetadata(this.field());\n return metadata?.autoRemoveButton;\n });\n\n /**\n * For primitive array items, the key of the value field template (e.g., 'value').\n * Used to wrap the FormControl in the item context so getFieldTree(key) works.\n * Returns undefined for object arrays (FormGroup items have natural child navigation).\n */\n private readonly primitiveFieldKey = linkedSignal<string | undefined>(() => {\n // Priority 1: Normalization metadata (simplified arrays always have this)\n const metadata = getNormalizedArrayMetadata(this.field());\n if (metadata?.primitiveFieldKey) {\n return metadata.primitiveFieldKey;\n }\n\n // Priority 2: Existing item definitions (full-API arrays with initial items)\n const definitions = (this.field().fields as ArrayItemDefinition[]) || [];\n if (definitions.length > 0 && !Array.isArray(definitions[0])) {\n return (definitions[0] as FieldDef<unknown>).key;\n }\n\n return undefined;\n });\n\n /**\n * Normalized item templates WITHOUT auto-remove button appended.\n * Each element is normalized to an array: single FieldDef → [FieldDef], array stays as-is.\n */\n private readonly rawItemTemplates = computed<ArrayItemTemplate[]>(() => {\n const arrayField = this.field();\n const definitions = (arrayField.fields as ArrayItemDefinition[]) || [];\n\n return definitions.map((def) => {\n return (Array.isArray(def) ? def : [def]) as ArrayItemTemplate;\n });\n });\n\n /**\n * Resolves the effective fallback template for this array — used when the form\n * value contains an item with no registered template (i.e., items that were\n * neither added via event handlers nor covered by a positional entry in `fields`).\n */\n private readonly fallbackTemplate = computed<FieldDef<unknown>[] | undefined>(() => {\n const raw = getNormalizedArrayMetadata(this.field())?.template;\n if (!raw) return undefined;\n return (Array.isArray(raw) ? [...raw] : [raw]) as FieldDef<unknown>[];\n });\n\n /**\n * Gets the item templates (field definitions) for the array.\n * Each element can be either:\n * - A single FieldDef (primitive item) - normalized to [FieldDef]\n * - An array of FieldDefs (object item) - used as-is\n */\n private readonly itemTemplates = computed<ArrayItemTemplate[]>(() => {\n const raw = this.rawItemTemplates();\n const removeButton = this.autoRemoveButton();\n\n if (!removeButton) return raw;\n\n return raw.map((normalized) => {\n return [...normalized, removeButton] as unknown as ArrayItemTemplate;\n });\n });\n\n /** The array's own FieldTree, navigated from the parent context's (possibly group-scoped) form. */\n private readonly arrayFieldTree = computed<ArrayFieldTree<unknown> | undefined>(() => {\n const parentForm = this.parentFieldSignalContext.form;\n return (parentForm as Record<string, ArrayFieldTree<unknown> | undefined>)[this.field().key];\n });\n\n /**\n * Current array value, read from the array's own FieldTree when available.\n * The parent context's `value` signal is root-scoped, so group-nested arrays\n * (group > row > array) are invisible in it; the FieldTree is authoritative.\n * Falls back to the root-value lookup during the init window before the tree exists.\n */\n private readonly currentArrayValue = computed<unknown[]>(() => {\n const tree = this.arrayFieldTree();\n if (tree) {\n const value = tree().value();\n return Array.isArray(value) ? value : [];\n }\n return getArrayValue(this.parentFieldSignalContext.value(), this.field().key);\n });\n\n private readonly arrayFieldTrees = computed<readonly (FieldTree<unknown> | null)[]>(() => {\n const arrayValue = this.currentArrayValue();\n\n if (arrayValue.length === 0) return [];\n\n const arrayFieldTree = this.arrayFieldTree();\n if (!arrayFieldTree) return arrayValue.map(() => null);\n\n // Access array items via bracket notation - Angular Signal Forms arrays support this\n const items: (FieldTree<unknown> | null)[] = [];\n for (let i = 0; i < arrayValue.length; i++) {\n // Access item FieldTree directly via numeric indexing (ArrayFieldTree supports this)\n const itemFieldTree = arrayFieldTree[i];\n items.push(itemFieldTree ?? null);\n }\n\n return items;\n });\n\n // ─────────────────────────────────────────────────────────────────────────────\n // State Signals\n // ─────────────────────────────────────────────────────────────────────────────\n\n private readonly resolvedItemsSignal = signal<ResolvedArrayItem[]>([]);\n // Explicit state machine for async resolution + componentInitialized emission.\n // Replaces the prior updateVersion + pendingInitializationCycle + settledInitializationCycle trio.\n private readonly fsm = new ArrayFieldStateMachine();\n\n /**\n * Map of item IDs to their current positions. O(1) lookup vs O(n) indexOf().\n * Used by child linkedSignals to reactively track their position in the array.\n */\n private readonly itemPositionMap = computed(() => {\n const items = this.resolvedItemsSignal();\n return new Map(items.map((item, idx) => [item.id, idx]));\n });\n\n /** Read-only view of resolved items for template consumption. */\n readonly resolvedItems = computed(() => this.resolvedItemsSignal());\n\n private readonly allResolvedFieldsRenderReady = computed(() =>\n this.resolvedItems().every((item) => item.fields.every((field) => field.renderReady())),\n );\n\n /**\n * Whether the array has reached its configured maxLength.\n * Exposed as a public signal so add-button components can bind [disabled]=\"atMaxLength()\".\n */\n readonly atMaxLength: Signal<boolean> = computed(() => {\n const maxLength = this.field().maxLength;\n if (maxLength === undefined) return false;\n return this.currentArrayValue().length >= maxLength;\n });\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Auto-remove Cache\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Caches the result of appending the auto-remove button to template arrays.\n * Keyed by template array reference — cache hits occur during recreate/resolution\n * operations where stored template references are reused. Add operations via\n * handleAddFromEvent always create a fresh `[...template]` copy (line ~277),\n * so each add is a cache miss by design (the spread is needed for mutability).\n */\n private readonly autoRemoveCache = new WeakMap<readonly FieldDef<unknown>[], FieldDef<unknown>[]>();\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Constructor\n // ─────────────────────────────────────────────────────────────────────────────\n\n constructor() {\n this.setupEffects();\n this.setupEventHandlers();\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n private setupEffects(): void {\n // Sync field components when array data changes\n explicitEffect([this.arrayFieldTrees], ([fieldTrees]) => {\n this.performDifferentialUpdate(fieldTrees);\n });\n\n explicitEffect([this.fsm.state, this.allResolvedFieldsRenderReady], ([, allReady]) => {\n if (this.fsm.shouldEmit(allReady)) {\n emitComponentInitialized(this.eventBus, 'array', this.field().key, this.parentInjector);\n }\n });\n }\n\n private setupEventHandlers(): void {\n observeArrayActions(this.eventBus, () => this.key())\n .pipe(takeUntilDestroyed())\n .subscribe((action) => {\n if (action.action === 'add') {\n // Template can be single FieldDef (primitive) or FieldDef[] (object)\n const templates = Array.isArray(action.template) ? action.template : action.template ? [action.template] : [];\n if (templates.length === 0) {\n this.logger.error(\n `Cannot add item to array '${this.key()}': template is REQUIRED. ` +\n 'Buttons must specify an explicit template property. ' +\n 'There is no default template - each add operation must provide its own.',\n );\n return;\n }\n void this.handleAddFromEvent(action.template, action.index);\n } else if (action.action === 'move') {\n this.moveItem(action.fromIndex, action.toIndex);\n } else {\n this.removeItem(action.index);\n }\n });\n }\n\n /**\n * Handles add operations from events (append, prepend, insert).\n * Creates resolved items FIRST, then updates form value.\n * This ensures prepend/insert work correctly - differential update sees \"none\"\n * because resolved items count already matches the new array length.\n */\n private async handleAddFromEvent(template: FieldDef<unknown> | readonly FieldDef<unknown>[], index?: number): Promise<void> {\n // Normalize template to mutable array for consistent handling.\n // A single FieldDef with valueHandling: 'flatten' (container, row) is an object item\n // whose children should be flattened — NOT a primitive item. Only true leaf fields\n // (input, checkbox, etc.) are primitive when passed as a single FieldDef.\n const templates: FieldDef<unknown>[] = Array.isArray(template) ? [...template] : [template];\n const isSingleField = !Array.isArray(template);\n const isPrimitiveItem = isSingleField && getFieldValueHandling(templates[0].type, this.rawFieldRegistry()) !== 'flatten';\n\n // Track primitive field key for full-API arrays that start empty.\n // Simplified arrays already have this info from normalization metadata.\n if (isPrimitiveItem && templates[0].key && !this.primitiveFieldKey()) {\n this.primitiveFieldKey.set(templates[0].key);\n }\n\n if (templates.length === 0) {\n this.logger.error(\n `Cannot add item to array '${this.field().key}': no field templates provided. ` +\n 'Buttons must specify an explicit template property when adding array items.',\n );\n return;\n }\n\n const arrayKey = this.field().key;\n const parentForm = this.parentFieldSignalContext.form;\n const currentValue = parentForm().value() as TModel;\n const currentArray = getArrayValue(currentValue as Partial<TModel>, arrayKey);\n const insertIndex = index !== undefined ? Math.min(index, currentArray.length) : currentArray.length;\n\n // Compute default value\n let value: unknown;\n\n if (isPrimitiveItem) {\n // Primitive item: single field's value is extracted directly\n value = getFieldDefaultValue(templates[0], this.rawFieldRegistry());\n } else {\n // Object item: merge all template defaults into an object\n value = {};\n for (const templateField of templates) {\n const rawValue = getFieldDefaultValue(templateField, this.rawFieldRegistry());\n const valueHandling = getFieldValueHandling(templateField.type, this.rawFieldRegistry());\n const isContainer = templateField.type === 'group' || templateField.type === 'row' || templateField.type === 'container';\n\n if (isContainer) {\n if (isGroupField(templateField)) {\n // Groups wrap their fields under the group key\n value = { ...(value as Record<string, unknown>), [templateField.key]: rawValue };\n } else {\n // Rows and containers flatten their fields directly\n value = { ...(value as Record<string, unknown>), ...(rawValue as Record<string, unknown>) };\n }\n } else if (valueHandling === 'include' && templateField.key) {\n value = { ...(value as Record<string, unknown>), [templateField.key]: rawValue };\n }\n }\n }\n\n // Dispatch an append run — bumps runId so any in-flight differential update\n // becomes stale, but state stays in its current phase.\n const run = this.fsm.dispatch({ kind: 'append' });\n\n // Append auto-remove button to resolution templates (for rendering only)\n const resolveTemplates = this.withAutoRemove(templates);\n\n // Resolve the new item\n const resolvedItem = await firstValueFrom(\n resolveArrayItem({\n index: insertIndex,\n templates: resolveTemplates,\n arrayField: this.field(),\n itemPositionMap: this.itemPositionMap,\n parentFieldSignalContext: this.parentFieldSignalContext,\n parentInjector: this.parentInjector,\n registry: this.rawFieldRegistry(),\n destroyRef: this.destroyRef,\n loadTypeComponent: (type: string) => this.fieldRegistry.loadTypeComponent(type),\n generateItemId: this.generateItemId,\n primitiveFieldKey: isPrimitiveItem ? templates[0].key : undefined,\n }).pipe(\n catchError((error) => {\n this.logger.error(`Failed to resolve array item at index ${insertIndex}:`, error);\n return of(undefined);\n }),\n ),\n );\n\n if (run.isStale() || !resolvedItem) {\n return;\n }\n\n // Store the template used for this item so it can be re-used during recreate operations\n this.slot().templates.set(resolvedItem.id, templates);\n // Mirror the insert into slot.itemOrder so survival across hide/show stays consistent.\n this.slot().itemOrder.splice(insertIndex, 0, resolvedItem.id);\n\n // Insert resolved item at correct position\n this.resolvedItemsSignal.update((current) => {\n const newItems = [...current];\n newItems.splice(insertIndex, 0, resolvedItem);\n return newItems;\n });\n\n // Update form value - differential update sees \"none\" (lengths match)\n const newArray = [...currentArray];\n newArray.splice(insertIndex, 0, value);\n // The `as any` is required because Angular Signal Forms uses complex conditional types\n // that cannot infer the correct type when setting a dynamically-keyed property\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n parentForm().value.set({ ...currentValue, [arrayKey]: newArray } as any);\n }\n\n private performDifferentialUpdate(fieldTrees: readonly (FieldTree<unknown> | null)[]): void {\n const resolvedItems = this.resolvedItemsSignal();\n const operation = determineDifferentialOperation(resolvedItems, fieldTrees.length);\n\n // \"none\" operations (value-only changes) don't kick the state machine — they shouldn't\n // interfere with in-flight async resolutions.\n if (operation.type === 'none') {\n return;\n }\n\n switch (operation.type) {\n case 'clear': {\n // Clean up template registry when all items are removed\n this.slot().templates.clear();\n this.resolvedItemsSignal.set([]);\n const run = this.fsm.dispatch({ kind: 'clear' });\n // First-ever clear with no items: synthesize a resolve so the emit-effect fires\n // the init pulse (matches the form-starts-empty case from the prior implementation).\n if (resolvedItems.length === 0 && !this.fsm.hasEverEmitted) {\n run.resolve();\n }\n break;\n }\n case 'initial': {\n // If the slot has surviving state from a prior ArrayFieldComponent instance (same\n // form, same array path, just a different render cycle — e.g. an `@if`-gated array\n // that was hidden then re-shown), reuse the stored templates AND ids so dynamically-\n // added items rebuild correctly. When the slot is empty this collapses to a normal\n // initial resolve via Priority 2/3.\n const slot = this.slot();\n const useSurvival = slot.itemOrder.length === fieldTrees.length && fieldTrees.length > 0;\n const positionalTemplates = useSurvival ? slot.itemOrder.map((id) => slot.templates.get(id)) : undefined;\n const existingItemIds = useSurvival ? slot.itemOrder.slice() : undefined;\n const run = this.fsm.dispatch({ kind: 'initial' });\n void this.resolveAllItems(fieldTrees, run, positionalTemplates, existingItemIds);\n break;\n }\n case 'append': {\n const run = this.fsm.dispatch({ kind: 'append' });\n void this.appendItems(fieldTrees, operation.startIndex, operation.endIndex, run);\n break;\n }\n case 'recreate': {\n // Capture templates by item ID so each item is recreated with its original template,\n // even after move operations that change positions. Registry entries (from dynamic adds\n // or moves) take priority over positional itemTemplates lookup.\n const positionalTemplates: (FieldDef<unknown>[] | undefined)[] = resolvedItems.map((item) => {\n // Registry entries (from dynamic adds or moves) take priority;\n // unmoved initial items return undefined → falls through to itemTemplates[idx] during resolve.\n return this.slot().templates.get(item.id);\n });\n\n this.resolvedItemsSignal.set([]);\n const run = this.fsm.dispatch({ kind: 'recreate' });\n void this.resolveAllItems(fieldTrees, run, positionalTemplates);\n break;\n }\n }\n }\n\n private async resolveAllItems(\n fieldTrees: readonly (FieldTree<unknown> | null)[],\n run: RunHandle,\n positionalTemplates?: (FieldDef<unknown>[] | undefined)[],\n existingItemIds?: readonly (string | undefined)[],\n ): Promise<void> {\n if (fieldTrees.length === 0) {\n this.resolvedItemsSignal.set([]);\n return;\n }\n\n // Wrap each item observable to catch individual errors\n const safeItemObservables = fieldTrees.map((_, i) =>\n this.createResolveItemObservable(i, positionalTemplates?.[i], existingItemIds?.[i]).pipe(\n catchError((error) => {\n this.logger.error(`Failed to resolve array item at index ${i}:`, error);\n return of(undefined);\n }),\n ),\n );\n\n try {\n const items = await firstValueFrom(\n forkJoin(safeItemObservables).pipe(map((items) => items.filter((item): item is ResolvedArrayItem => item !== undefined))),\n );\n\n if (!run.isStale()) {\n // Update template registry with new item IDs for dynamically added items\n if (positionalTemplates) {\n // Clean up old entries and add new ones\n const newItemIds = new Set(items.map((item) => item.id));\n for (const existingId of this.slot().templates.keys()) {\n if (!newItemIds.has(existingId)) {\n this.slot().templates.delete(existingId);\n }\n }\n items.forEach((item, idx) => {\n const template = positionalTemplates[idx];\n if (template) {\n this.slot().templates.set(item.id, template);\n }\n });\n }\n // Mirror the resolved item ids into the slot's itemOrder so a future component\n // recreate (e.g. after an @if hide cycle) can rehydrate via the survival path.\n const slot = this.slot();\n slot.itemOrder.length = 0;\n for (const item of items) slot.itemOrder.push(item.id);\n this.resolvedItemsSignal.set(items);\n // Mark this run settled so the emit-effect can fire componentInitialized once allReady is true.\n run.resolve();\n }\n } catch (err) {\n this.logger.error('Failed to resolve array items:', err);\n this.resolvedItemsSignal.set([]);\n }\n }\n\n private async appendItems(\n fieldTrees: readonly (FieldTree<unknown> | null)[],\n startIndex: number,\n endIndex: number,\n run: RunHandle,\n ): Promise<void> {\n const itemsToResolve = fieldTrees.slice(startIndex, endIndex);\n if (itemsToResolve.length === 0) return;\n\n // Wrap each item observable to catch individual errors\n const safeItemObservables = itemsToResolve.map((_, i) => {\n const index = startIndex + i;\n return this.createResolveItemObservable(index).pipe(\n catchError((error) => {\n this.logger.error(`Failed to resolve array item at index ${index}:`, error);\n return of(undefined);\n }),\n );\n });\n\n try {\n const newItems = await firstValueFrom(\n forkJoin(safeItemObservables).pipe(map((items) => items.filter((item): item is ResolvedArrayItem => item !== undefined))),\n );\n\n if (!run.isStale()) {\n const slot = this.slot();\n for (const item of newItems) slot.itemOrder.push(item.id);\n this.resolvedItemsSignal.update((current) => [...current, ...newItems]);\n }\n } catch (err) {\n this.logger.error('Failed to append array items:', err);\n }\n }\n\n /** Creates an observable that resolves a single array item. */\n private createResolveItemObservable(\n index: number,\n overrideTemplate?: FieldDef<unknown>[],\n existingItemId?: string,\n ): Observable<ResolvedArrayItem | undefined> {\n const itemTemplates = this.itemTemplates();\n const primitiveKey = this.primitiveFieldKey();\n\n // Priority 1: Use override template (from recreate with stored templates)\n if (overrideTemplate && overrideTemplate.length > 0) {\n return resolveArrayItem({\n index,\n templates: this.withAutoRemove(overrideTemplate),\n arrayField: this.field(),\n itemPositionMap: this.itemPositionMap,\n parentFieldSignalContext: this.parentFieldSignalContext,\n parentInjector: this.parentInjector,\n registry: this.rawFieldRegistry(),\n destroyRef: this.destroyRef,\n loadTypeComponent: (type: string) => this.fieldRegistry.loadTypeComponent(type),\n generateItemId: this.generateItemId,\n existingItemId,\n primitiveFieldKey: primitiveKey,\n });\n }\n\n // Priority 2: Use defined template at this index\n const templates = itemTemplates[index];\n if (templates && templates.length > 0) {\n return resolveArrayItem({\n index,\n templates: templates as FieldDef<unknown>[],\n arrayField: this.field(),\n itemPositionMap: this.itemPositionMap,\n parentFieldSignalContext: this.parentFieldSignalContext,\n parentInjector: this.parentInjector,\n registry: this.rawFieldRegistry(),\n destroyRef: this.destroyRef,\n loadTypeComponent: (type: string) => this.fieldRegistry.loadTypeComponent(type),\n generateItemId: this.generateItemId,\n existingItemId,\n primitiveFieldKey: primitiveKey,\n });\n }\n\n // Priority 3: Use the metadata fallback template for untracked items in the form value.\n // Homogeneous only — every fallback item receives the same template regardless of value shape.\n const fallback = this.fallbackTemplate();\n if (fallback && fallback.length > 0) {\n return resolveArrayItem({\n index,\n templates: this.withAutoRemove(fallback),\n arrayField: this.field(),\n itemPositionMap: this.itemPositionMap,\n parentFieldSignalContext: this.parentFieldSignalContext,\n parentInjector: this.parentInjector,\n registry: this.rawFieldRegistry(),\n destroyRef: this.destroyRef,\n loadTypeComponent: (type: string) => this.fieldRegistry.loadTypeComponent(type),\n generateItemId: this.generateItemId,\n existingItemId,\n primitiveFieldKey: primitiveKey,\n }).pipe(\n tap((item) => {\n // Register the fallback template against the generated item id so subsequent\n // recreates hit Priority 1 instead of re-resolving via this branch.\n if (item) this.slot().templates.set(item.id, fallback);\n }),\n );\n }\n\n // Priority 4: no template available. Full-API arrays are positional by design —\n // `fields` declares one template per item, so values extending past `fields.length`\n // (e.g., from external `value.set`, parent two-way binding, or initial values on a\n // `fields: []` array) have no template to render. Use the simplified array API\n // (`template` + `value`) when you need homogeneous arrays with value-driven items.\n this.logger.warn(\n `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.`,\n );\n return of(undefined);\n }\n\n /**\n * Appends the auto-remove button to a templates array for rendering.\n * The button is added for visual rendering only — it doesn't affect the form schema.\n * Original templates are stored in templateRegistry WITHOUT the remove button,\n * so this method is called during resolution to add it dynamically.\n */\n private withAutoRemove(templates: FieldDef<unknown>[]): FieldDef<unknown>[] {\n const removeButton = this.autoRemoveButton();\n if (!removeButton) return templates;\n\n let cached = this.autoRemoveCache.get(templates);\n if (cached) return cached;\n\n cached = [...templates, removeButton];\n this.autoRemoveCache.set(templates, cached);\n return cached;\n }\n\n /**\n * Handles move operations — reorders an existing item without destroying it.\n * Updates resolvedItems and form value atomically. Since the array length\n * doesn't change, `determineDifferentialOperation` returns 'none' and no\n * recreate is triggered. The `@for` loop tracks by `item.id`, so Angular\n * moves the DOM node instead of destroying/recreating. The `itemPositionMap`\n * computed auto-recomputes, propagating new indices to child linkedSignals.\n */\n private moveItem(fromIndex: number, toIndex: number): void {\n const arrayKey = this.field().key;\n const parentForm = this.parentFieldSignalContext.form;\n const currentValue = parentForm().value() as TModel;\n const currentArray = getArrayValue(currentValue as Partial<TModel>, arrayKey);\n const length = currentArray.length;\n\n if (fromIndex < 0 || fromIndex >= length) {\n this.logger.warn(\n `moveArrayItem fromIndex ${fromIndex} is out of bounds for array '${arrayKey}' with length ${length}. Operation skipped.`,\n );\n return;\n }\n\n if (toIndex < 0 || toIndex >= length) {\n this.logger.warn(\n `moveArrayItem toIndex ${toIndex} is out of bounds for array '${arrayKey}' with length ${length}. Operation skipped.`,\n );\n return;\n }\n\n if (fromIndex === toIndex) return;\n\n // Register raw (pre-auto-remove) templates for all items whose position will change.\n // Initial items use itemTemplates[currentIndex] during recreate, but after\n // a move their position no longer matches their original template. Stashing\n // the template by item ID lets the recreate path resolve the correct one.\n // We use rawItemTemplates (without auto-remove button) because the recreate path\n // calls withAutoRemove() during resolution — storing the synthesized version would\n // cause duplicate remove buttons.\n const rawTemplates = this.rawItemTemplates();\n const currentItems = this.resolvedItemsSignal();\n const lo = Math.min(fromIndex, toIndex);\n const hi = Math.max(fromIndex, toIndex);\n for (let i = lo; i <= hi; i++) {\n const item = currentItems[i];\n if (item && !this.slot().templates.has(item.id) && i < rawTemplates.length) {\n this.slot().templates.set(item.id, rawTemplates[i] as FieldDef<unknown>[]);\n }\n }\n\n // Reorder resolvedItems — splice preserves object identity (no destroy/recreate)\n this.resolvedItemsSignal.update((current) => {\n const newItems = [...current];\n const [moved] = newItems.splice(fromIndex, 1);\n newItems.splice(toIndex, 0, moved);\n return newItems;\n });\n // Keep slot.itemOrder in sync so the post-recreate survival path sees the moved order.\n this.slot().moveItem(fromIndex, toIndex);\n\n // Reorder form value array the same way\n const newArray = [...currentArray];\n const [movedValue] = newArray.splice(fromIndex, 1);\n newArray.splice(toIndex, 0, movedValue);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n parentForm().value.set({ ...currentValue, [arrayKey]: newArray } as any);\n }\n\n /**\n * Handles remove operations from events (pop, shift, removeAt).\n * Updates resolvedItems FIRST, then form value - this ensures differential\n * update sees \"none\" (lengths match) and avoids unnecessary recreates.\n * Remaining items' linkedSignal indices auto-update via itemPositionMap.\n */\n private removeItem(index?: number): void {\n const arrayKey = this.field().key;\n const parentForm = this.parentFieldSignalContext.form;\n const currentValue = parentForm().value() as TModel;\n const currentArray = getArrayValue(currentValue as Partial<TModel>, arrayKey);\n\n if (currentArray.length === 0) return;\n\n // When index is undefined or -1, remove the last item.\n let removeIndex: number;\n if (index === undefined || index === -1) {\n removeIndex = currentArray.length - 1;\n } else if (index < -1 || index >= currentArray.length) {\n this.logger.warn(\n `removeArrayItem index ${index} is out of bounds for array '${arrayKey}' with length ${currentArray.length}. Operation skipped.`,\n );\n return;\n } else {\n removeIndex = index;\n }\n\n // Update resolvedItems FIRST - remove the item at the specified index.\n // This ensures differential update sees \"none\" (lengths already match).\n // Remaining items' linkedSignal indices auto-update via itemPositionMap.\n //\n // When the item is removed from resolvedItemsSignal, the @for loop removes the DOM element\n // and NgComponentOutlet destroys the component view, triggering DestroyRef callbacks.\n // Async validators use Angular's resource API tied to the form-level schema path — when the\n // array value is updated below, the resource's params re-evaluate. If the removed item's path\n // no longer exists, params returns undefined, cancelling the pending validation automatically.\n const removedItem = this.resolvedItemsSignal()[removeIndex];\n if (removedItem) {\n this.slot().templates.delete(removedItem.id);\n }\n // Keep slot.itemOrder in sync so the post-recreate survival path doesn't try to\n // restore an id that's no longer in the form value.\n this.slot().removeAt(removeIndex);\n this.resolvedItemsSignal.update((current) => {\n const newItems = [...current];\n newItems.splice(removeIndex, 1);\n return newItems;\n });\n\n // Update the parent form with the new array value\n const newArray = [...currentArray];\n newArray.splice(removeIndex, 1);\n\n // The `as any` is required due to Angular Signal Forms' complex conditional types\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n parentForm().value.set({ ...currentValue, [arrayKey]: newArray } as any);\n }\n}\n\nexport { ArrayFieldComponent };\n"]}
@@ -1,6 +1,6 @@
1
1
  import { untracked, isWritableSignal, isSignal, inject, Injector, DestroyRef, computed } from '@angular/core';
2
2
  import { createDefaultDerivationLogConfig, shouldLog, ExpressionParser, DynamicFormError, getNestedValue, createFormFieldStateMap, isEqual, readFieldStateInfo, evaluateCondition, extractStringDependencies, extractExpressionDependencies, isDerivationLogicConfig, hasTargetProperty, getChangedKeys, resolveHttpRequest, DynamicFormLogger, FunctionRegistryService, FORM_OPTIONS, EXTERNAL_DATA } from '@ng-forge/dynamic-forms/internal';
3
- import { t as traverseFieldsWithContext, j as DEV_MODE, k as DEFAULT_DEBOUNCE_MS, l as DERIVATION_WARNING_TRACKER, F as FormStateManager, n as DERIVATION_LOG_CONFIG, P as PROPERTY_OVERRIDE_STORE } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms-C59pvc26.mjs';
3
+ import { t as traverseFieldsWithContext, j as DEV_MODE, k as DEFAULT_DEBOUNCE_MS, l as DERIVATION_WARNING_TRACKER, F as FormStateManager, n as DERIVATION_LOG_CONFIG, P as PROPERTY_OVERRIDE_STORE } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.mjs';
4
4
  import { HttpClient } from '@angular/common/http';
5
5
  import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
6
  import { explicitEffect } from 'ngxtension/explicit-effect';
@@ -586,22 +586,33 @@ function tryApplyArrayDerivation(entry, context, chainContext) {
586
586
  }
587
587
  function createEvaluationContext$1(entry, formValue, context, chainContext) {
588
588
  const fieldValue = getNestedValue(formValue, entry.fieldKey);
589
- const fieldAccessor = context.rootForm[entry.fieldKey];
589
+ const fieldAccessor = getFieldTreeByPath(context.rootForm, entry.fieldKey);
590
590
  const fieldState = fieldAccessor ? readFieldStateInfo(fieldAccessor, false) : undefined;
591
591
  const parentPath = getParentPathInScope(entry.fieldKey);
592
592
  const groupValue = parentPath ? getNestedValue(formValue, parentPath) : undefined;
593
+ const scopedToGroup = parentPath !== undefined;
593
594
  return {
594
595
  fieldValue,
595
- formValue,
596
+ formValue: scopedToGroup ? (groupValue ?? {}) : formValue,
596
597
  fieldPath: entry.fieldKey,
597
598
  groupValue,
598
599
  customFunctions: context.customFunctions,
599
600
  externalData: context.externalData,
600
601
  logger: context.logger,
602
+ rootFormValue: formValue,
601
603
  fieldState,
602
604
  formFieldState: chainContext.formFieldState,
603
605
  };
604
606
  }
607
+ function getFieldTreeByPath(root, path) {
608
+ let current = root;
609
+ for (const segment of path.split('.')) {
610
+ if (current === undefined || current === null)
611
+ return undefined;
612
+ current = current[segment];
613
+ }
614
+ return current;
615
+ }
605
616
  function createArrayItemEvaluationContext$1(entry, arrayItem, rootFormValue, itemIndex, arrayPath, context, chainContext) {
606
617
  const pathInfo = parseArrayPath(entry.fieldKey);
607
618
  let fieldState;
@@ -727,17 +738,21 @@ function topologicalSort(entries) {
727
738
  }
728
739
  }
729
740
  for (const entryB of entries) {
741
+ const groupPath = entryB.fieldKey.includes('.') && !entryB.fieldKey.includes('.$.')
742
+ ? entryB.fieldKey.slice(0, entryB.fieldKey.lastIndexOf('.'))
743
+ : undefined;
730
744
  for (const dep of entryB.dependsOn) {
731
745
  if (dep === '*')
732
746
  continue;
733
- const producers = derivationsByField.get(dep);
734
- if (producers) {
735
- for (const entryA of producers) {
736
- const entryAEdges = graph.get(entryA);
737
- if (entryA !== entryB && entryAEdges && !entryAEdges.has(entryB)) {
738
- entryAEdges.add(entryB);
739
- inDegree.set(entryB, (inDegree.get(entryB) ?? 0) + 1);
740
- }
747
+ const producers = [
748
+ ...(derivationsByField.get(dep) ?? []),
749
+ ...(groupPath && !dep.includes('.') ? (derivationsByField.get(`${groupPath}.${dep}`) ?? []) : []),
750
+ ];
751
+ for (const entryA of producers) {
752
+ const entryAEdges = graph.get(entryA);
753
+ if (entryA !== entryB && entryAEdges && !entryAEdges.has(entryB)) {
754
+ entryAEdges.add(entryB);
755
+ inDegree.set(entryB, (inDegree.get(entryB) ?? 0) + 1);
741
756
  }
742
757
  }
743
758
  }
@@ -1680,7 +1695,15 @@ function getEntriesForChangedFields(entries, changedFields) {
1680
1695
  return entries.filter((entry) => {
1681
1696
  if (entry.dependsOn.includes('*'))
1682
1697
  return true;
1683
- return entry.dependsOn.some((dep) => changedFields.has(dep));
1698
+ if (entry.dependsOn.some((dep) => changedFields.has(dep)))
1699
+ return true;
1700
+ for (const changed of changedFields) {
1701
+ if (entry.fieldKey.startsWith(changed + '.'))
1702
+ return true;
1703
+ if (entry.dependsOn.some((dep) => dep.startsWith(changed + '.')))
1704
+ return true;
1705
+ }
1706
+ return false;
1684
1707
  });
1685
1708
  }
1686
1709
  function tryApplyPropertyDerivation(entry, context) {
@@ -1731,14 +1754,16 @@ function createEvaluationContext(entry, formValue, context) {
1731
1754
  const fieldValue = getNestedValue(formValue, entry.fieldKey);
1732
1755
  const parentPath = getParentPathInScope(entry.fieldKey);
1733
1756
  const groupValue = parentPath ? getNestedValue(formValue, parentPath) : undefined;
1757
+ const scopedToGroup = parentPath !== undefined;
1734
1758
  return {
1735
1759
  fieldValue,
1736
- formValue,
1760
+ formValue: scopedToGroup ? (groupValue ?? {}) : formValue,
1737
1761
  fieldPath: entry.fieldKey,
1738
1762
  groupValue,
1739
1763
  customFunctions: context.customFunctions,
1740
1764
  externalData: context.externalData,
1741
1765
  logger: context.logger,
1766
+ rootFormValue: formValue,
1742
1767
  };
1743
1768
  }
1744
1769
  function createArrayItemEvaluationContext(entry, arrayItem, rootFormValue, itemIndex, arrayPath, context) {
@@ -2344,4 +2369,4 @@ function bootstrapDerivationOrchestrator() {
2344
2369
  }
2345
2370
 
2346
2371
  export { bootstrapDerivationOrchestrator };
2347
- //# sourceMappingURL=ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-D6ntLtW9.mjs.map
2372
+ //# sourceMappingURL=ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C9aVachF.mjs.map