@ng-forge/dynamic-forms 1.0.0-rc.2 → 1.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-8rodySeE.mjs → ng-forge-dynamic-forms-array-field.component-DZWGHZjj.mjs} +23 -17
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-DZWGHZjj.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C8kpF0tw.mjs → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-ueIHE70C.mjs} +39 -14
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-ueIHE70C.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-DFgjWEZN.mjs → ng-forge-dynamic-forms-container-field.component-Dt5yfGQ7.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-DFgjWEZN.mjs.map → ng-forge-dynamic-forms-container-field.component-Dt5yfGQ7.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-Ca4AqdZu.mjs → ng-forge-dynamic-forms-group-field.component-BgoDlVYp.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-Ca4AqdZu.mjs.map → ng-forge-dynamic-forms-group-field.component-BgoDlVYp.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-BJi_rtey.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-CRCXs4f8.mjs} +10 -10
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CRCXs4f8.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-IsAtMNjD.mjs → ng-forge-dynamic-forms-page-field.component-W9f5VyQJ.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-IsAtMNjD.mjs.map → ng-forge-dynamic-forms-page-field.component-W9f5VyQJ.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +4 -4
- package/types/ng-forge-dynamic-forms-integration.d.ts +1 -5
- package/types/ng-forge-dynamic-forms-internal.d.ts +35 -8
- package/types/ng-forge-dynamic-forms.d.ts +5 -5
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-8rodySeE.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C8kpF0tw.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BJi_rtey.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { inject, DestroyRef, Injector, EnvironmentInjector, input, computed, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
-
import { i as injectFieldRegistry, c as createFieldResolutionPipe, D as DfFieldOutlet } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms-
|
|
3
|
+
import { i as injectFieldRegistry, c as createFieldResolutionPipe, D as DfFieldOutlet } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms-CRCXs4f8.mjs';
|
|
4
4
|
import { outputFromObservable } from '@angular/core/rxjs-interop';
|
|
5
5
|
import { explicitEffect } from 'ngxtension/explicit-effect';
|
|
6
6
|
import { EventBus, DynamicFormLogger, validatePageNesting, derivedFromDeferred } from '@ng-forge/dynamic-forms/internal';
|
|
@@ -94,4 +94,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImpor
|
|
|
94
94
|
}], 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 }] }], pageIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageIndex", required: true }] }], isVisible: [{ type: i0.Input, args: [{ isSignal: true, alias: "isVisible", required: true }] }], nextPage: [{ type: i0.Output, args: ["nextPage"] }], previousPage: [{ type: i0.Output, args: ["previousPage"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }] } });
|
|
95
95
|
|
|
96
96
|
export { PageFieldComponent as default };
|
|
97
|
-
//# sourceMappingURL=ng-forge-dynamic-forms-page-field.component-
|
|
97
|
+
//# sourceMappingURL=ng-forge-dynamic-forms-page-field.component-W9f5VyQJ.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ng-forge-dynamic-forms-page-field.component-
|
|
1
|
+
{"version":3,"file":"ng-forge-dynamic-forms-page-field.component-W9f5VyQJ.mjs","mappings":";;;;;;;;AAsCc;YAKK;;AAEA;AACE;AACF;AACA;;;;;AAQjB;;;;;;YAQS;;AAEA;;AAEA;;;;YAQA;AACA;AACA;YAMQ;AACf;AACE;;;;;AAKe;AAGf;AACA;;;AAGA;AACE;;;;AAOH;YAQH;;;;;AAWE;;;;;AAQA;;;AA/FiB;;;;;;AAdlB;;;;AARS;;;;;;;AAWR;AACA;AACA;AACA;AACA;AACA;AACA;AACA","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms/src/lib/fields/page/page-field.component.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, DestroyRef, EnvironmentInjector, inject, Injector, input } from '@angular/core';\nimport { DfFieldOutlet } from '../../directives/df-field-outlet/df-field-outlet.directive';\nimport { outputFromObservable } from '@angular/core/rxjs-interop';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { derivedFromDeferred } from '@ng-forge/dynamic-forms/internal';\nimport { createFieldResolutionPipe, ResolvedField } from '../../utils/resolve-field/resolve-field';\nimport { computeContainerHostClasses, setupContainerInitEffect } from '../../utils/container-utils/container-utils';\nimport { PageField, validatePageNesting } from '@ng-forge/dynamic-forms/internal';\nimport { injectFieldRegistry } from '../../utils/inject-field-registry/inject-field-registry';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { NextPageEvent, PageChangeEvent, PreviousPageEvent } from '../../events/constants';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\n\n/** Renders a single page in multi-page (wizard) forms. */\n@Component({\n selector: 'section[page-field]',\n imports: [DfFieldOutlet],\n template: `\n @for (field of resolvedFields(); track field.key) {\n @if (!field.hidden()) {\n <ng-container *dfFieldOutlet=\"field; environmentInjector: environmentInjector\" />\n }\n }\n `,\n styleUrl: './page-field.component.scss',\n host: {\n '[class]': 'hostClasses()',\n '[class.disabled]': 'disabled()',\n '[class.df-page-visible]': 'isVisible()',\n '[class.df-page-hidden]': '!isVisible()',\n '[attr.aria-hidden]': '!isVisible()',\n '[attr.data-page-index]': 'pageIndex()',\n '[id]': '`${key()}`',\n '[attr.data-testid]': 'key()',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class PageFieldComponent {\n // ─────────────────────────────────────────────────────────────────────────────\n // Dependencies\n // ─────────────────────────────────────────────────────────────────────────────\n\n private readonly destroyRef = inject(DestroyRef);\n private readonly fieldRegistry = injectFieldRegistry();\n private readonly injector = inject(Injector);\n protected readonly environmentInjector = inject(EnvironmentInjector);\n private readonly eventBus = inject(EventBus);\n private readonly logger = inject(DynamicFormLogger);\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Inputs\n // ─────────────────────────────────────────────────────────────────────────────\n\n field = input.required<PageField>();\n key = input.required<string>();\n className = input<string>();\n pageIndex = input.required<number>();\n isVisible = input.required<boolean>();\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Computed Signals\n // ─────────────────────────────────────────────────────────────────────────────\n\n readonly hostClasses = computed(() => computeContainerHostClasses('page-field', this.className()));\n\n readonly disabled = computed(() => this.field().disabled || false);\n\n readonly isValid = computed(() => validatePageNesting(this.field()));\n\n private readonly rawFieldRegistry = computed(() => this.fieldRegistry.raw);\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Outputs\n // ─────────────────────────────────────────────────────────────────────────────\n\n readonly nextPage = outputFromObservable(this.eventBus.on<NextPageEvent>('next-page'));\n readonly previousPage = outputFromObservable(this.eventBus.on<PreviousPageEvent>('previous-page'));\n readonly pageChange = outputFromObservable(this.eventBus.on<PageChangeEvent>('page-change'));\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Field Resolution\n // ─────────────────────────────────────────────────────────────────────────────\n\n private readonly fieldsSource = computed(() => {\n if (!this.isValid()) {\n return [];\n }\n return this.field().fields || [];\n });\n\n protected readonly resolvedFields = derivedFromDeferred(\n this.fieldsSource,\n createFieldResolutionPipe(() => ({\n loadTypeComponent: (type: string) => this.fieldRegistry.loadTypeComponent(type),\n registry: this.rawFieldRegistry(),\n injector: this.injector,\n destroyRef: this.destroyRef,\n onError: (fieldDef: FieldDef<unknown>, error: unknown) => {\n const fieldKey = fieldDef.key || '<no key>';\n this.logger.error(\n `Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +\n `within page '${this.field().key}'. Ensure the field type is registered in your field registry.`,\n error,\n );\n },\n })),\n { initialValue: [] as ResolvedField[], injector: this.injector },\n );\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Constructor\n // ─────────────────────────────────────────────────────────────────────────────\n\n constructor() {\n this.setupEffects();\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n private setupEffects(): void {\n setupContainerInitEffect(this.resolvedFields, this.eventBus, 'page', () => this.field().key, this.injector);\n\n explicitEffect([this.isValid, this.field], ([valid, pageField]) => {\n if (!valid) {\n this.logger.error(\n `Invalid configuration: Page '${pageField.key}' contains nested page fields. ` +\n `Pages cannot contain other pages. Consider using groups or rows for nested structure.`,\n pageField,\n );\n }\n });\n }\n}\n"]}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { o as AppendArrayItemEvent, B as BUILT_IN_FIELDS, p as ComponentAddonComponent, q as ConsoleLogger, s as DfTemplate, u as DynamicForm, E as EventDispatcher, v as FormClearEvent, w as FormResetEvent, x as FormSubmitEvent, I as InsertArrayItemEvent, M as MoveArrayItemEvent, N as NextPageEvent, y as PageChangeEvent, z as PopArrayItemEvent, G as PrependArrayItemEvent, H as PreviousPageEvent, R as RemoveAtIndexEvent, S as ShiftArrayItemEvent, T as TemplateAddonComponent, J as TextAddonComponent, K as arrayEvent, L as createWrappers, O as isWrappersBundle, Q as provideDynamicForm, U as sanitizeFormConfig, V as withAddonActions, W as withCustomAddon, X as withEventFormValue, Y as withLegacyStatusClasses, Z as withLoggerConfig, _ as withValidationExecutionDefaults, $ as withValueExclusionDefaults, a0 as wrapperProps } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms-
|
|
1
|
+
export { o as AppendArrayItemEvent, B as BUILT_IN_FIELDS, p as ComponentAddonComponent, q as ConsoleLogger, s as DfTemplate, u as DynamicForm, E as EventDispatcher, v as FormClearEvent, w as FormResetEvent, x as FormSubmitEvent, I as InsertArrayItemEvent, M as MoveArrayItemEvent, N as NextPageEvent, y as PageChangeEvent, z as PopArrayItemEvent, G as PrependArrayItemEvent, H as PreviousPageEvent, R as RemoveAtIndexEvent, S as ShiftArrayItemEvent, T as TemplateAddonComponent, J as TextAddonComponent, K as arrayEvent, L as createWrappers, O as isWrappersBundle, Q as provideDynamicForm, U as sanitizeFormConfig, V as withAddonActions, W as withCustomAddon, X as withEventFormValue, Y as withLegacyStatusClasses, Z as withLoggerConfig, _ as withValidationExecutionDefaults, $ as withValueExclusionDefaults, a0 as wrapperProps } from './ng-forge-dynamic-forms-ng-forge-dynamic-forms-CRCXs4f8.mjs';
|
|
2
2
|
export { DF_FIELD_TEMPLATES, DynamicFormError, DynamicFormLogger, NoopLogger, hasFormValue, isArrayField, isContainerField, isDisplayOnlyField, isFieldBoundContext, isFormStateCondition, isGroupField, isHiddenField, isLeafField, isPageField, isRowField, isSimplifiedArrayField, isValueBearingField, isWrapperTypeDefinition, resolveDynamicValue } from '@ng-forge/dynamic-forms/internal';
|
|
3
3
|
//# sourceMappingURL=ng-forge-dynamic-forms.mjs.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ng-forge/dynamic-forms",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
4
|
"description": "Type-safe, signal-powered dynamic forms for Angular. Build complex forms from configuration with full TypeScript inference.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"schematics": "./schematics/collection.json",
|
|
34
34
|
"generators": "./schematics/generators.json",
|
|
35
35
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
36
|
+
"node": "^22.22.3 || ^24.15.0 || >=26.0.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@angular/common": "~22.0.0",
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
},
|
|
72
72
|
"ngForgeSchematicDeps": {
|
|
73
73
|
"bootstrap": "^5.3.8",
|
|
74
|
-
"primeng": "^21.1.
|
|
74
|
+
"primeng": "^21.1.9",
|
|
75
75
|
"@primeuix/themes": "^2.0.3",
|
|
76
76
|
"primeicons": "^7.0.0",
|
|
77
|
-
"@ionic/angular": "^8.8.
|
|
77
|
+
"@ionic/angular": "^8.8.9"
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -111,10 +111,6 @@ type HtmlInputType = 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' |
|
|
|
111
111
|
type InputType = Extract<HtmlInputType, 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search'>;
|
|
112
112
|
/** Infers the TypeScript value type for a given input type. */
|
|
113
113
|
type InferInputValue<T extends HtmlInputType> = T extends 'number' ? number : string;
|
|
114
|
-
/**
|
|
115
|
-
* @deprecated Use `InferInputValue` instead. Will be removed in v1.0.0.
|
|
116
|
-
*/
|
|
117
|
-
type InputTypeToValueType<T extends InputType> = InferInputValue<T>;
|
|
118
114
|
/**
|
|
119
115
|
* Props for input fields with optional type specification.
|
|
120
116
|
* Generic parameter allows extending InputType with additional HtmlInputType values.
|
|
@@ -933,4 +929,4 @@ declare function createNgForgeActionFixture<C, TEvent extends FormEvent = FormEv
|
|
|
933
929
|
declare function provideTestValidationMessages(messages: ValidationMessages): Provider;
|
|
934
930
|
|
|
935
931
|
export { ADDON_PRESET_HANDLER, NG_FORGE_ACTION_INPUTS, NG_FORGE_ADDONS_INPUTS, NG_FORGE_ADDON_ACTION_INPUTS, NG_FORGE_FIELD_SHELL_INPUTS, NG_FORGE_VALUE_FIELD_INPUTS, NgForgeAction, NgForgeActionHost, NgForgeAddonAction, NgForgeAddonActionBase, NgForgeAddons, NgForgeAddonsBase, NgForgeControl, NgForgeField, NgForgeFieldHost, NgForgeFieldShell, NgForgeHostControl, addArrayItemButtonMapper, buildArrayButtonEventContext, buildValueFieldInputs, buttonFieldMapper, checkboxFieldMapper, createAriaDescribedBySignal, createNgForgeActionFixture, createNgForgeFieldFixture, createResolvedErrorsSignal, datepickerFieldMapper, injectNgForgeAction, injectNgForgeAddonAction, injectNgForgeAddons, injectNgForgeField, insertArrayItemButtonMapper, nextButtonFieldMapper, optionsFieldMapper, popArrayItemButtonMapper, prependArrayItemButtonMapper, previousButtonFieldMapper, provideTestValidationMessages, removeArrayItemButtonMapper, resolveArrayButtonContext, resolveArrayButtonEventArgs, resolveValueFieldContext, setupMetaTracking, shiftArrayItemButtonMapper, shouldShowErrors, submitButtonFieldMapper, valueFieldMapper };
|
|
936
|
-
export type { AddonPresetHandler, ArrayButtonContext, ArrayButtonEventContext, Autocapitalize, AutocompleteValue, BaseArrayAddButtonField, BaseArrayRemoveButtonField, BaseInsertArrayItemButtonField, BaseNavigationButtonField, ButtonField, CheckboxField, CreateNgForgeActionFixtureOptions, CreateNgForgeFieldFixtureOptions, DatepickerField, DatepickerProps, EnterKeyHint, EventArg, EventArgs, FieldWithOptions, HtmlInputType, InferInputValue, InputField, InputMeta, InputMode, InputProps, InputType,
|
|
932
|
+
export type { AddonPresetHandler, ArrayButtonContext, ArrayButtonEventContext, Autocapitalize, AutocompleteValue, BaseArrayAddButtonField, BaseArrayRemoveButtonField, BaseInsertArrayItemButtonField, BaseNavigationButtonField, ButtonField, CheckboxField, CreateNgForgeActionFixtureOptions, CreateNgForgeFieldFixtureOptions, DatepickerField, DatepickerProps, EnterKeyHint, EventArg, EventArgs, FieldWithOptions, HtmlInputType, InferInputValue, InputField, InputMeta, InputMode, InputProps, InputType, MetaTrackingOptions, MultiCheckboxField, NgForgeActionFixture, NgForgeFieldFixture, NumericInputType, RadioField, ResolvedError, SelectField, SelectProps, SliderField, StringInputType, TextareaField, TextareaMeta, TextareaProps, TextareaWrap, ToggleField, TypedNgForgeAddonAction, TypedNgForgeAddons, TypedNgForgeField, ValueFieldContext };
|
|
@@ -265,7 +265,11 @@ interface EvaluationContext<TValue = unknown, TFormValue extends Record<string,
|
|
|
265
265
|
* field has no inner group above the array boundary).
|
|
266
266
|
*/
|
|
267
267
|
groupValue?: unknown;
|
|
268
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* Root form value. Reaches the whole form when `formValue` is scoped to an
|
|
270
|
+
* array item or a parent group; equals `formValue` at form root. Always set
|
|
271
|
+
* in derivation contexts; in condition contexts only set inside array items.
|
|
272
|
+
*/
|
|
269
273
|
rootFormValue?: Record<string, unknown>;
|
|
270
274
|
/** Current array index when inside an array derivation. */
|
|
271
275
|
arrayIndex?: number;
|
|
@@ -813,19 +817,27 @@ type InferArrayValue<T, D extends number> = T extends {
|
|
|
813
817
|
template: infer TArr;
|
|
814
818
|
} ? TArr extends readonly RegisteredFieldTypes[] ? InferFormValueWithDepth<[...TArr], Depth[D]>[] : InferSingleTemplateValue<TArr, D>[] : T extends {
|
|
815
819
|
fields: infer F;
|
|
816
|
-
} ?
|
|
820
|
+
} ? InferContainerChildren<F, Depth[D], unknown>[] : unknown[];
|
|
821
|
+
/**
|
|
822
|
+
* Recurse into a container's child fields, but only when they form a fixed-length
|
|
823
|
+
* tuple — i.e. a config authored with `as const`. A general `RegisteredFieldTypes[]`
|
|
824
|
+
* array (notably the base default `InferFormValue<RegisteredFieldTypes[]>` over the
|
|
825
|
+
* whole registry) bails to `TFallback`; recursing there would make the type explode
|
|
826
|
+
* past TypeScript's union-complexity limit (TS2590) and break consumers of the
|
|
827
|
+
* abstract `FormConfig`. `[...F]` re-mutabilises the readonly tuple for the inner
|
|
828
|
+
* `InferFormValueWithDepth` constraint.
|
|
829
|
+
*/
|
|
830
|
+
type InferContainerChildren<F, D extends number, TFallback> = F extends readonly RegisteredFieldTypes[] ? number extends F['length'] ? TFallback : InferFormValueWithDepth<[...F], D> : TFallback;
|
|
817
831
|
/** Process a single field and determine its contribution to the form value type */
|
|
818
832
|
type ProcessField<T, D extends number = 5> = [D] extends [never] ? Record<string, unknown> : T extends {
|
|
819
833
|
type: 'page' | 'row';
|
|
820
834
|
fields: infer F;
|
|
821
|
-
} ?
|
|
835
|
+
} ? InferContainerChildren<F, Depth[D], never> : T extends {
|
|
822
836
|
type: 'group';
|
|
823
837
|
key: infer K;
|
|
824
838
|
fields: infer F;
|
|
825
|
-
} ? K extends string ?
|
|
826
|
-
[P in K]:
|
|
827
|
-
} : {
|
|
828
|
-
[P in K]: Record<string, unknown>;
|
|
839
|
+
} ? K extends string ? {
|
|
840
|
+
[P in K]: InferContainerChildren<F, Depth[D], Record<string, unknown>>;
|
|
829
841
|
} : never : T extends {
|
|
830
842
|
type: 'array';
|
|
831
843
|
key: infer K;
|
|
@@ -859,6 +871,21 @@ type ExtractFields<T> = T extends {
|
|
|
859
871
|
* Limited to 5 levels of nesting to prevent infinite type instantiation.
|
|
860
872
|
*/
|
|
861
873
|
type InferFormValue<T> = ExtractFields<T> extends readonly RegisteredFieldTypes[] ? InferFormValueWithDepth<ExtractFields<T> extends RegisteredFieldTypes[] ? ExtractFields<T> : [...ExtractFields<T>], 5> : never;
|
|
874
|
+
/**
|
|
875
|
+
* Default `TModel` for `DynamicForm` / `FormStateManager` generics.
|
|
876
|
+
*
|
|
877
|
+
* Equals `InferFormValue<T>` for any concrete config (an object type). Falls back
|
|
878
|
+
* to `Record<string, unknown>` only when inference yields a non-record — e.g. an
|
|
879
|
+
* empty `fields` array, where `InferFormValue` is `unknown` — so the `TModel
|
|
880
|
+
* extends Record<string, unknown>` bound always holds by construction.
|
|
881
|
+
*
|
|
882
|
+
* Crucially, unlike the older `InferFormValue<T> & Record<string, unknown>`
|
|
883
|
+
* default, this does NOT graft an index signature onto the model. That matters at
|
|
884
|
+
* the consumer call site: the index signature made `value['anyTypo']` resolve to
|
|
885
|
+
* `unknown` instead of erroring, silently defeating the typo-safety the inference
|
|
886
|
+
* is meant to provide.
|
|
887
|
+
*/
|
|
888
|
+
type InferFormModel<T, M = InferFormValue<T>> = M extends Record<string, unknown> ? M : Record<string, unknown>;
|
|
862
889
|
|
|
863
890
|
/**
|
|
864
891
|
* Custom validator function signature using Angular's public FieldContext API
|
|
@@ -3683,4 +3710,4 @@ declare function safeReadPathKeys(fieldContext: FieldContext<unknown>): readonly
|
|
|
3683
3710
|
declare function stableStringify(value: unknown): string;
|
|
3684
3711
|
|
|
3685
3712
|
export { ADDON_ACTION_HANDLERS, ADDON_ACTION_REGISTRY, ADDON_TYPE_COMPONENT_CACHE, ADDON_TYPE_DEFINITIONS, ADDON_TYPE_REGISTRY, ARRAY_CONTEXT, AsyncConditionFunctionCacheService, ComponentInitializedEvent, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, DEFAULT_WRAPPERS, DEPRECATION_WARNING_TRACKER, DF_FIELD_TEMPLATES, DfAddonSlot, DynamicFormError, DynamicFormLogger, DynamicTextPipe, DynamicValueFunctionCacheService, EMIT_FORM_VALUE_ON_EVENTS, EXTERNAL_DATA, Evaluator, EventBus, ExpressionParser, ExpressionParserError, FIELD_REGISTRY, FIELD_SIGNAL_CONTEXT, FORM_ID_PREFIX, FORM_OPTIONS, FieldContextRegistryService, FunctionRegistryService, GROUP_CONTEXT, HTTP_CONDITION_CACHE, HttpConditionCache, HttpConditionFunctionCacheService, INITIALIZATION_TIMEOUT_MS, LogicFunctionCacheService, NoopLogger, Parser, READONLY_FIELD_TREE_CACHE, RootFormRegistryService, TokenType, Tokenizer, WRAPPER_AUTO_ASSOCIATIONS, WRAPPER_COMPONENT_CACHE, WRAPPER_REGISTRY, applyHiddenLogic, applyMetaToElement, applyValidator, applyValidators, arrayFieldMapper, assertNever, baseFieldMapper, buildBaseInputs, buildClassName, compareValues, containerFieldMapper, createAddonActionRegistry, createAsyncConditionLogicFunction, createDebouncedLogicFunction, createDebouncedResourceLogicFn, createDefaultDerivationLogConfig, createDynamicValueFunction, createFormFieldStateMap, createHttpConditionLogicFunction, createInitializationTracker, createLogicFunction, createWarningTracker, deepMergeDefaults, derivedFromDeferred, detectFormMode, dynamicTextToObservable, evaluateCondition, evaluateHttpValidationResponse, evaluateNonFieldDisabled, evaluateNonFieldHidden, extractExpressionDependencies, extractStringDependencies, getArrayLength, getChangedKeys, getFieldValueHandling, getGridClassString, getNestedValue, groupFieldMapper, hasChildFields, hasCrossFieldWhenCondition, hasFormValue, hasNestedProperty, hasTargetProperty, injectAddonTypeRegistry, injectFieldSignalContext, injectFieldsSupportingAddons, interpolateParams, isArrayField, isCheckedField, isContainerField, isCrossFieldBuiltInValidator, isCrossFieldExpression, isCrossFieldSchema, isCrossFieldStateLogic, isCrossFieldValidator, isDerivationLogicConfig, isDisplayOnlyField, isEqual, isFieldBoundContext, isFormStateCondition, isFunctionHttpValidator, isGenericContainerField, isGroupField, isHiddenField, isLeafField, isNonPagedForm, isPageField, isPagedForm, isResourceBasedValidator, isRowField, isSimplifiedArrayField, isStateLogicConfig, isValidNonPagedForm, isValidPagedForm, isValueBearingField, isValueField, isWrapperTypeDefinition, keyBy, mapValues, memoize, normalizeFieldsArray, omit, pageFieldMapper, readFieldStateInfo, resolveDynamicValue, resolveHttpRequest, resolveNextButtonDisabled, resolveNonFieldDisabled, resolveNonFieldHidden, resolveSubmitButtonDisabled, resolveTokens, rowFieldMapper, runPresetAction, safeReadPathKeys, setupInitializationTracking, shouldLog, stableStringify, textFieldMapper, toReadonlyFieldTree, toReadonlyFieldTreeCached, validatePageNesting, withPreviousValue, writeToFieldValue };
|
|
3686
|
-
export type { ASTNode, AddonActionContext, AddonActionHandler, AddonActionPreset, AddonShapeValidator, AddonSlot, AddonTypeDefinition, AddonTypeRegistryRef, AddonTypeSchema, AndCondition, AnyAddon, ArrayAllowedChildren, ArrayButtonConfig, ArrayComponent, ArrayContext, ArrayField, ArrayFieldTree, ArrayItemDefinition, ArrayItemTemplate, ArrayLiteralNode, ArrowFunctionNode, AsyncCondition, AsyncConditionFunction, AsyncCustomValidator, AsyncDerivationFunction, AsyncValidatorConfig, AvailableFieldTypes, BaseAddon, BaseCheckedField, BaseValidatorConfig, BaseValueField, BinaryOpNode, BuiltInValidatorConfig, ButtonLogicContext, CallExpressionNode, CheckedFieldComponent, CommonAddonActionPreset, CommonAddonSlot, ComparisonOperator, ComponentAddon, ComputedMemberAccessNode, ConditionalExpression, ConditionalNode, ContainerAllowedChildren, ContainerComponent, ContainerField, ContainerFieldTypes, ContainerLogicConfig, CrossFieldDetectionContext, CssWrapper, CustomCondition, CustomFnConfig, CustomFunction, CustomFunctionOptions, CustomFunctionScope, CustomValidator, CustomValidatorConfig, DebouncedResourceLogicFnConfig, DeclarativeHttpValidatorConfig, DerivationLogConfig, DerivationLogLevel, DerivationLogicConfig, DerivedFromDeferredOptions, DynamicFormActionRegistry, DynamicFormAddonActionPresetRegistry, DynamicFormAddonRegistry, DynamicFormAddonSlotRegistry, DynamicFormFieldRegistry, DynamicText, DynamicValue, EvaluationContext, EvaluationScope, ExtractField, FieldAddonSupport, FieldAddonSupportEntry, FieldBoundAddonActionContext, FieldComponent, FieldDef, FieldMeta, FieldOption, FieldPathAccess, FieldRegistryContainers, FieldRegistryLeaves, FieldRegistryWrappers, FieldScope, FieldSignalContext, FieldStateContext, FieldStateInfo, FieldTreeRecord, FieldTypeDefinition, FieldValueCondition, FieldWithValidation, FieldWrapper, FormConfig, FormEvent, FormEventConstructor, FormFieldStateMap, FormMode, FormModeDetectionResult, FormOptions, FormStateCondition, FunctionHttpValidatorConfig, GroupAllowedChildren, GroupComponent, GroupContext, GroupField, HiddenField, HiddenScalar, HiddenValue, HttpCondition, HttpCustomValidator, HttpRequestConfig, HttpResourceRequest, HttpValidationResponseMapping, IdentifierNode, InferFormValue, InitializationTrackingOptions, JavascriptCondition, LazyComponentLoader, LeafFieldTypes, LiteralNode, Logger, LogicConfig, LogicTrigger, MapperFn, MemberAccessNode, MemoizeOptions, NarrowField, NarrowFields, NextButtonOptions, NonFieldLogicConfig, NonFieldLogicContext, NonFieldLogicType, ObjectLiteralNode, OrCondition, OrphanAddonActionContext, PageAllowedChildren, PageField, PresetCollaborators, Prettify, ReadonlyFieldTree, RegisteredActionRef, RegisteredFieldTypes, RegisteredWrapperTypes, RenderReadyInput, ResolvedValidationExecutionConfig, ResolvedValueExclusionConfig, RowAllowedChildren, RowComponent, RowField, RowWrapper, SchemaApplicationConfig, SchemaDefinition, SimplifiedArrayField, StateLogicConfig, StateLogicType, SubmissionActionResult, SubmissionConfig, SubmitButtonOptions, TemplateAddon, TextAddon, TextElementType, TextField, TextProps, Token, TokenContext, Trigger, UnaryOpNode, ValidationError, ValidationExecutionConfig, ValidationMessages, ValidatorConfig, ValueExclusionConfig, ValueFieldComponent, ValueHandlingMode, ValueType, WarningTracker, WithInputSignals, WrapperAutoAssociations, WrapperConfig, WrapperFieldInputs, WrapperTypeDefinition };
|
|
3713
|
+
export type { ASTNode, AddonActionContext, AddonActionHandler, AddonActionPreset, AddonShapeValidator, AddonSlot, AddonTypeDefinition, AddonTypeRegistryRef, AddonTypeSchema, AndCondition, AnyAddon, ArrayAllowedChildren, ArrayButtonConfig, ArrayComponent, ArrayContext, ArrayField, ArrayFieldTree, ArrayItemDefinition, ArrayItemTemplate, ArrayLiteralNode, ArrowFunctionNode, AsyncCondition, AsyncConditionFunction, AsyncCustomValidator, AsyncDerivationFunction, AsyncValidatorConfig, AvailableFieldTypes, BaseAddon, BaseCheckedField, BaseValidatorConfig, BaseValueField, BinaryOpNode, BuiltInValidatorConfig, ButtonLogicContext, CallExpressionNode, CheckedFieldComponent, CommonAddonActionPreset, CommonAddonSlot, ComparisonOperator, ComponentAddon, ComputedMemberAccessNode, ConditionalExpression, ConditionalNode, ContainerAllowedChildren, ContainerComponent, ContainerField, ContainerFieldTypes, ContainerLogicConfig, CrossFieldDetectionContext, CssWrapper, CustomCondition, CustomFnConfig, CustomFunction, CustomFunctionOptions, CustomFunctionScope, CustomValidator, CustomValidatorConfig, DebouncedResourceLogicFnConfig, DeclarativeHttpValidatorConfig, DerivationLogConfig, DerivationLogLevel, DerivationLogicConfig, DerivedFromDeferredOptions, DynamicFormActionRegistry, DynamicFormAddonActionPresetRegistry, DynamicFormAddonRegistry, DynamicFormAddonSlotRegistry, DynamicFormFieldRegistry, DynamicText, DynamicValue, EvaluationContext, EvaluationScope, ExtractField, FieldAddonSupport, FieldAddonSupportEntry, FieldBoundAddonActionContext, FieldComponent, FieldDef, FieldMeta, FieldOption, FieldPathAccess, FieldRegistryContainers, FieldRegistryLeaves, FieldRegistryWrappers, FieldScope, FieldSignalContext, FieldStateContext, FieldStateInfo, FieldTreeRecord, FieldTypeDefinition, FieldValueCondition, FieldWithValidation, FieldWrapper, FormConfig, FormEvent, FormEventConstructor, FormFieldStateMap, FormMode, FormModeDetectionResult, FormOptions, FormStateCondition, FunctionHttpValidatorConfig, GroupAllowedChildren, GroupComponent, GroupContext, GroupField, HiddenField, HiddenScalar, HiddenValue, HttpCondition, HttpCustomValidator, HttpRequestConfig, HttpResourceRequest, HttpValidationResponseMapping, IdentifierNode, InferFormModel, InferFormValue, InitializationTrackingOptions, JavascriptCondition, LazyComponentLoader, LeafFieldTypes, LiteralNode, Logger, LogicConfig, LogicTrigger, MapperFn, MemberAccessNode, MemoizeOptions, NarrowField, NarrowFields, NextButtonOptions, NonFieldLogicConfig, NonFieldLogicContext, NonFieldLogicType, ObjectLiteralNode, OrCondition, OrphanAddonActionContext, PageAllowedChildren, PageField, PresetCollaborators, Prettify, ReadonlyFieldTree, RegisteredActionRef, RegisteredFieldTypes, RegisteredWrapperTypes, RenderReadyInput, ResolvedValidationExecutionConfig, ResolvedValueExclusionConfig, RowAllowedChildren, RowComponent, RowField, RowWrapper, SchemaApplicationConfig, SchemaDefinition, SimplifiedArrayField, StateLogicConfig, StateLogicType, SubmissionActionResult, SubmissionConfig, SubmitButtonOptions, TemplateAddon, TextAddon, TextElementType, TextField, TextProps, Token, TokenContext, Trigger, UnaryOpNode, ValidationError, ValidationExecutionConfig, ValidationMessages, ValidatorConfig, ValueExclusionConfig, ValueFieldComponent, ValueHandlingMode, ValueType, WarningTracker, WithInputSignals, WrapperAutoAssociations, WrapperConfig, WrapperFieldInputs, WrapperTypeDefinition };
|
|
@@ -2,7 +2,7 @@ import * as rxjs from 'rxjs';
|
|
|
2
2
|
import * as _angular_core from '@angular/core';
|
|
3
3
|
import { Type, Injector, Signal, EnvironmentInjector, WritableSignal, Provider, EnvironmentProviders, TemplateRef } from '@angular/core';
|
|
4
4
|
import * as _ng_forge_dynamic_forms_internal from '@ng-forge/dynamic-forms/internal';
|
|
5
|
-
import { FieldDef, FormEvent, RegisteredFieldTypes,
|
|
5
|
+
import { FieldDef, FormEvent, RegisteredFieldTypes, InferFormModel, FormConfig, FormOptions, EventBus, LazyComponentLoader, WrapperTypeDefinition, FieldTypeDefinition, InferFormValue as InferFormValue$1, DerivationLogLevel, Logger, ValueExclusionConfig, ValidationExecutionConfig, TextAddon, WrapperFieldInputs, TemplateAddon, ComponentAddon, BaseAddon, AddonTypeDefinition, AddonActionHandler, ArrayItemDefinition } from '@ng-forge/dynamic-forms/internal';
|
|
6
6
|
export { AddonActionContext, AddonActionPreset, AddonSlot, AddonTypeDefinition, AnyAddon, ArrayAllowedChildren, ArrayButtonConfig, ArrayField, ArrayItemTemplate, AsyncCondition, AsyncConditionFunction, AsyncCustomValidator, AsyncDerivationFunction, AsyncValidatorConfig, AvailableFieldTypes, BaseAddon, BaseCheckedField, BaseValidatorConfig, BaseValueField, BuiltInValidatorConfig, CheckedFieldComponent, CommonAddonActionPreset, CommonAddonSlot, ComponentAddon, ConditionalExpression, ContainerAllowedChildren, ContainerField, ContainerFieldTypes, CssWrapper, CustomFnConfig, CustomFunction, CustomValidator, CustomValidatorConfig, DF_FIELD_TEMPLATES, DeclarativeHttpValidatorConfig, DynamicFormActionRegistry, DynamicFormAddonActionPresetRegistry, DynamicFormAddonRegistry, DynamicFormAddonSlotRegistry, DynamicFormError, DynamicFormFieldRegistry, DynamicFormLogger, DynamicText, DynamicValue, EvaluationContext, ExtractField, FieldBoundAddonActionContext, FieldComponent, FieldDef, FieldMeta, FieldOption, FieldPathAccess, FieldRegistryContainers, FieldRegistryLeaves, FieldRegistryWrappers, FieldWithValidation, FieldWrapper, FormConfig, FormEvent, FormEventConstructor, FormMode, FormModeDetectionResult, FormOptions, FormStateCondition, GroupAllowedChildren, GroupField, HiddenField, HiddenScalar, HiddenValue, HttpCondition, HttpCustomValidator, HttpRequestConfig, HttpValidationResponseMapping, InferFormValue, LeafFieldTypes, Logger, LogicConfig, NarrowField, NarrowFields, NextButtonOptions, NoopLogger, OrphanAddonActionContext, PageAllowedChildren, PageField, RegisteredActionRef, RegisteredFieldTypes, ResolvedValidationExecutionConfig, ResolvedValueExclusionConfig, RowAllowedChildren, RowField, SchemaApplicationConfig, SchemaDefinition, SimplifiedArrayField, StateLogicConfig, SubmissionActionResult, SubmissionConfig, SubmitButtonOptions, TemplateAddon, TextAddon, TextElementType, TextField, TextProps, TokenContext, ValidationError, ValidationExecutionConfig, ValidationMessages, ValidatorConfig, ValueExclusionConfig, ValueFieldComponent, ValueType, WithInputSignals, WrapperConfig, WrapperTypeDefinition, hasFormValue, isArrayField, isContainerField, isDisplayOnlyField, isFieldBoundContext, isFormStateCondition, isGroupField, isHiddenField, isLeafField, isPageField, isRowField, isSimplifiedArrayField, isValueBearingField, isWrapperTypeDefinition, resolveDynamicValue } from '@ng-forge/dynamic-forms/internal';
|
|
7
7
|
import * as _angular_forms_signals from '@angular/forms/signals';
|
|
8
8
|
import { FieldTree } from '@angular/forms/signals';
|
|
@@ -114,9 +114,9 @@ declare class PageNavigationStateChangeEvent implements FormEvent {
|
|
|
114
114
|
* }
|
|
115
115
|
* ```
|
|
116
116
|
*/
|
|
117
|
-
declare class DynamicForm<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[], TModel extends Record<string, unknown> =
|
|
117
|
+
declare class DynamicForm<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[], TModel extends Record<string, unknown> = InferFormModel<TFields>> {
|
|
118
118
|
/** Form configuration defining the structure, validation, and behavior. */
|
|
119
|
-
config: _angular_core.InputSignal<FormConfig<TFields, InferFormValue
|
|
119
|
+
config: _angular_core.InputSignal<FormConfig<TFields, _ng_forge_dynamic_forms.InferFormValue<TFields extends readonly RegisteredFieldTypes[] ? TFields : RegisteredFieldTypes[]>, Record<string, unknown>, unknown>>;
|
|
120
120
|
/** Runtime form options that override config options when provided. */
|
|
121
121
|
formOptions: _angular_core.InputSignal<FormOptions | undefined>;
|
|
122
122
|
/** Form values for two-way data binding. */
|
|
@@ -148,7 +148,7 @@ declare class DynamicForm<TFields extends RegisteredFieldTypes[] = RegisteredFie
|
|
|
148
148
|
protected readonly idPrefix: Signal<string>;
|
|
149
149
|
private componentId;
|
|
150
150
|
/** The currently active config used for form rendering */
|
|
151
|
-
activeConfig: Signal<FormConfig<TFields, InferFormValue
|
|
151
|
+
activeConfig: Signal<FormConfig<TFields, _ng_forge_dynamic_forms.InferFormValue<TFields extends readonly RegisteredFieldTypes[] ? TFields : RegisteredFieldTypes[]>, Record<string, unknown>, unknown> | undefined>;
|
|
152
152
|
/** Current render phase: 'render' = showing form, 'teardown' = hiding old components */
|
|
153
153
|
renderPhase: Signal<"teardown" | "render">;
|
|
154
154
|
/** Computed form mode detection with validation */
|
|
@@ -208,7 +208,7 @@ declare class DynamicForm<TFields extends RegisteredFieldTypes[] = RegisteredFie
|
|
|
208
208
|
/** Emits when form dirty state changes. */
|
|
209
209
|
dirtyChange: _angular_core.OutputRef<boolean>;
|
|
210
210
|
/** Emits form values when submitted (via FormSubmitEvent) and form is valid. */
|
|
211
|
-
submitted: _angular_core.OutputRef<
|
|
211
|
+
submitted: _angular_core.OutputRef<TModel>;
|
|
212
212
|
/** Emits when form is reset to default values. */
|
|
213
213
|
reset: _angular_core.OutputRef<FormResetEvent>;
|
|
214
214
|
/** Emits when form is cleared to empty state. */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ng-forge-dynamic-forms-array-field.component-8rodySeE.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;;AC/CM;AAGJ;;QAQA;AAEI;;AAEE;;AAGF;;;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;;AChIM;;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;;ACVc;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;;;AAGe;;AAEf;AACA;AAEA;AAA6B;AAE7B;AACA;;;AAIA;YAEE;AACA;;AAGF;;;;;QAUe;AAEjB,IAIiB;AACf;;;;;;AAOe;;AAIjB,IAIS;;;AAEsB;;AAE7B;AACA;;;YAOF,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;;;AA5tBiB;;;;;;;;;;;;;;;;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 { RootFormRegistryService } from '@ng-forge/dynamic-forms/internal';\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 // Get root form registry - it's guaranteed to be available since root form\n // is registered before resolvedFields computes (dependency chain ensures this)\n const rootFormRegistry = parentInjector.get(RootFormRegistryService);\n\n // Create a computed that derives the array item's FieldTree from root form.\n // Uses isEqual to prevent unnecessary re-computation when index changes but value is same.\n const itemFormAccessor = computed(\n () => {\n const rootForm = rootFormRegistry.rootForm();\n if (!rootForm) {\n return undefined;\n }\n\n const index = indexSignal();\n // Navigate: rootForm['arrayKey'][index]\n const arrayFieldTree = (rootForm as Record<string, unknown>)[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 { FieldSignalContext } 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 private readonly arrayFieldTrees = computed<readonly (FieldTree<unknown> | null)[]>(() => {\n const arrayKey = this.field().key;\n const parentForm = this.parentFieldSignalContext.form;\n const arrayValue = getArrayValue(this.parentFieldSignalContext.value(), arrayKey);\n\n if (arrayValue.length === 0) return [];\n\n const arrayFieldTree = (parentForm as Record<string, ArrayFieldTree<unknown>>)[arrayKey];\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 const arrayKey = this.field().key;\n const currentArray = getArrayValue(this.parentFieldSignalContext.value(), arrayKey);\n return currentArray.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"]}
|