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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -34
  3. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-D5qCzJsL.mjs → ng-forge-dynamic-forms-array-field.component-CIheZh11.mjs} +26 -20
  4. package/fesm2022/ng-forge-dynamic-forms-array-field.component-CIheZh11.mjs.map +1 -0
  5. package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-D6ntLtW9.mjs → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C9aVachF.mjs} +39 -14
  6. package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C9aVachF.mjs.map +1 -0
  7. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-nB6KFqVs.mjs → ng-forge-dynamic-forms-container-field.component-DNafCSO8.mjs} +5 -5
  8. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-nB6KFqVs.mjs.map → ng-forge-dynamic-forms-container-field.component-DNafCSO8.mjs.map} +1 -1
  9. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-AScQAJTQ.mjs → ng-forge-dynamic-forms-css-wrapper.component-BmHGlmES.mjs} +4 -4
  10. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-AScQAJTQ.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-BmHGlmES.mjs.map} +1 -1
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-Bq6Sgd6Z.mjs → ng-forge-dynamic-forms-group-field.component-CKVIw2K5.mjs} +5 -5
  12. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-Bq6Sgd6Z.mjs.map → ng-forge-dynamic-forms-group-field.component-CKVIw2K5.mjs.map} +1 -1
  13. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +34 -34
  14. package/fesm2022/ng-forge-dynamic-forms-internal.mjs +1397 -1401
  15. package/fesm2022/ng-forge-dynamic-forms-internal.mjs.map +1 -1
  16. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-C59pvc26.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.mjs} +70 -57
  17. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.mjs.map +1 -0
  18. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-DRaxbiqA.mjs → ng-forge-dynamic-forms-page-field.component-BRixkgyS.mjs} +5 -5
  19. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-DRaxbiqA.mjs.map → ng-forge-dynamic-forms-page-field.component-BRixkgyS.mjs.map} +1 -1
  20. package/fesm2022/{ng-forge-dynamic-forms-row-wrapper.component-BlVa801t.mjs → ng-forge-dynamic-forms-row-wrapper.component-BBWmqQA_.mjs} +4 -4
  21. package/fesm2022/{ng-forge-dynamic-forms-row-wrapper.component-BlVa801t.mjs.map → ng-forge-dynamic-forms-row-wrapper.component-BBWmqQA_.mjs.map} +1 -1
  22. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CNCpmf5_.mjs → ng-forge-dynamic-forms-text-field.component-C-f80sbk.mjs} +4 -4
  23. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CNCpmf5_.mjs.map → ng-forge-dynamic-forms-text-field.component-C-f80sbk.mjs.map} +1 -1
  24. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  25. package/package.json +6 -6
  26. package/types/ng-forge-dynamic-forms-integration.d.ts +1 -1
  27. package/types/ng-forge-dynamic-forms-internal.d.ts +21 -200
  28. package/fesm2022/ng-forge-dynamic-forms-array-field.component-D5qCzJsL.mjs.map +0 -1
  29. package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-D6ntLtW9.mjs.map +0 -1
  30. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-C59pvc26.mjs.map +0 -1
@@ -249,7 +249,7 @@ type FormFieldStateMap = Record<string, FieldStateInfo | undefined>;
249
249
  /** Field state context for the current field being evaluated. */
250
250
  type FieldStateContext = FieldStateInfo;
251
251
 
252
- interface EvaluationContext<TValue = unknown, TFormValue extends Record<string, unknown> = any> {
252
+ interface EvaluationContext<TValue = unknown, TFormValue extends Record<string, unknown> = Record<string, unknown>> {
253
253
  /** Current field value */
254
254
  fieldValue: TValue;
255
255
  /** Form value for the current evaluation scope. */
@@ -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
- /** Root form value when inside an array context. */
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;
@@ -298,12 +302,12 @@ interface CustomFunctionOptions {
298
302
  scope?: CustomFunctionScope;
299
303
  }
300
304
  /** Custom function signature for conditional expressions */
301
- type CustomFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => unknown;
305
+ type CustomFunction<TFormValue extends Record<string, unknown> = Record<string, unknown>> = (context: EvaluationContext<unknown, TFormValue>) => unknown;
302
306
 
303
307
  /** Async function for derivations — returns any value. */
304
- type AsyncDerivationFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => Promise<unknown> | Observable<unknown>;
308
+ type AsyncDerivationFunction<TFormValue extends Record<string, unknown> = Record<string, unknown>> = (context: EvaluationContext<unknown, TFormValue>) => Promise<unknown> | Observable<unknown>;
305
309
  /** Async function for conditions — returns boolean. */
306
- type AsyncConditionFunction<TFormValue extends Record<string, unknown> = any> = (context: EvaluationContext<unknown, TFormValue>) => Promise<boolean> | Observable<boolean>;
310
+ type AsyncConditionFunction<TFormValue extends Record<string, unknown> = Record<string, unknown>> = (context: EvaluationContext<unknown, TFormValue>) => Promise<boolean> | Observable<boolean>;
307
311
 
308
312
  /** Configuration for an HTTP request used by declarative HTTP validators, derivations, and conditions. */
309
313
  interface HttpRequestConfig {
@@ -842,7 +846,7 @@ type ProcessField<T, D extends number = 5> = [D] extends [never] ? Record<string
842
846
  } : never : T extends {
843
847
  type: 'text';
844
848
  } ? never : T extends {
845
- type: 'submit' | 'button' | 'next' | 'previous' | 'addArrayItem' | 'removeArrayItem';
849
+ type: 'submit' | 'button' | 'next' | 'previous' | 'addArrayItem' | 'add-array-item' | 'prependArrayItem' | 'prepend-array-item' | 'insertArrayItem' | 'insert-array-item' | 'removeArrayItem' | 'remove-array-item' | 'popArrayItem' | 'pop-array-item' | 'shiftArrayItem' | 'shift-array-item';
846
850
  } ? never : T extends {
847
851
  key: infer K;
848
852
  value: infer V;
@@ -1963,11 +1967,12 @@ declare const ADDON_TYPE_REGISTRY: InjectionToken<Map<string, AddonTypeDefinitio
1963
1967
  declare const ADDON_TYPE_COMPONENT_CACHE: InjectionToken<Map<string, Type<unknown>>>;
1964
1968
 
1965
1969
  /**
1966
- * Multi-provider token used by `withCustomAddon(...)` features to contribute
1967
- * addon type definitions. `provideDynamicForm` aggregates all values into
1968
- * the form-scoped `ADDON_TYPE_REGISTRY` map.
1970
+ * Multi-provider token used by `withCustomAddon(...)` features and UI adapters
1971
+ * to contribute addon type definitions. `provideDynamicForm` aggregates all
1972
+ * values into the form-scoped `ADDON_TYPE_REGISTRY` map.
1969
1973
  *
1970
- * @internal
1974
+ * Part of the `/integration` adapter contract: each UI adapter registers its
1975
+ * addon kinds against this token in its provider.
1971
1976
  */
1972
1977
  declare const ADDON_TYPE_DEFINITIONS: InjectionToken<readonly AddonTypeDefinition<_ng_forge_dynamic_forms_internal.BaseAddon<_ng_forge_dynamic_forms_internal.AddonSlot>>[]>;
1973
1978
 
@@ -2359,80 +2364,18 @@ declare class AsyncConditionFunctionCacheService {
2359
2364
  readonly asyncConditionFunctionCache: Map<string, LogicFn<unknown, boolean>>;
2360
2365
  }
2361
2366
 
2362
- /** Creates a logic function for an async condition. */
2363
- declare function createAsyncConditionLogicFunction<TValue>(condition: AsyncCondition): LogicFn<TValue, boolean>;
2364
-
2365
2367
  /**
2366
2368
  * Evaluate conditional expression
2367
2369
  * Uses secure AST-based parsing for JavaScript expressions
2368
2370
  */
2369
2371
  declare function evaluateCondition(expression: ConditionalExpression, context: EvaluationContext): boolean;
2370
2372
 
2371
- /**
2372
- * Trigger value carried through the debounced reactive pipeline.
2373
- *
2374
- * @internal
2375
- */
2376
- interface Trigger<TPayload> {
2377
- key: string;
2378
- payload: TPayload;
2379
- }
2380
- /**
2381
- * Configuration passed to {@link createDebouncedResourceLogicFn}.
2382
- *
2383
- * @internal
2384
- */
2385
- interface DebouncedResourceLogicFnConfig<TValue, TPayload, TResult> {
2386
- /** Default value returned by the resource while loading / pending. */
2387
- pendingValue: TResult;
2388
- /** Debounce window (ms) applied to the trigger signal before the resource sees it. */
2389
- debounceMs: number;
2390
- injector: Injector;
2391
- /**
2392
- * Builds the per-emission inner observable. Called every time the debounced
2393
- * trigger key changes; the resource swaps to the new stream automatically.
2394
- */
2395
- buildStream: (payload: TPayload) => Observable<TResult>;
2396
- /**
2397
- * Called per LogicFn invocation. Returns one of:
2398
- * - `{ kind: 'trigger', key, payload }` — set the trigger to this and
2399
- * return the (potentially still-loading) resource value.
2400
- * - `{ kind: 'returnEarly', value }` — bypass the resource and return
2401
- * this value directly (used by HTTP for response-cache hits — the
2402
- * cached value is authoritative and overrides any sticky resource value).
2403
- * - `{ kind: 'holdPrevious' }` — bypass the resource update but return
2404
- * the resource's current sticky value (its last resolved value via
2405
- * `withPreviousValue`, or `pendingValue` if it never resolved). Used
2406
- * by HTTP when the request template is temporarily unresolvable so
2407
- * the UI doesn't flicker back to `pendingValue`.
2408
- */
2409
- resolve: (ctx: FieldContext<TValue>) => {
2410
- kind: 'trigger';
2411
- key: string;
2412
- payload: TPayload;
2413
- } | {
2414
- kind: 'returnEarly';
2415
- value: TResult;
2416
- } | {
2417
- kind: 'holdPrevious';
2418
- };
2419
- }
2420
- /**
2421
- * Shared scaffolding for both async-function and HTTP condition LogicFns.
2422
- *
2423
- * @internal
2424
- */
2425
- declare function createDebouncedResourceLogicFn<TValue, TPayload, TResult>(config: DebouncedResourceLogicFnConfig<TValue, TPayload, TResult>): LogicFn<TValue, TResult>;
2426
-
2427
2373
  /** DI-scoped cache for HTTP condition logic functions. */
2428
2374
  declare class HttpConditionFunctionCacheService {
2429
2375
  /** Cache for HTTP condition logic functions, keyed by serialized condition. */
2430
2376
  readonly httpConditionFunctionCache: Map<string, LogicFn<unknown, boolean>>;
2431
2377
  }
2432
2378
 
2433
- /** Creates a logic function for an HTTP condition. */
2434
- declare function createHttpConditionLogicFunction<TValue>(condition: HttpCondition): LogicFn<TValue, boolean>;
2435
-
2436
2379
  /**
2437
2380
  * Create a logic function from a conditional expression.
2438
2381
  *
@@ -2615,24 +2558,6 @@ declare class ExpressionParserError extends DynamicFormError {
2615
2558
  interface EvaluationScope {
2616
2559
  [key: string]: unknown;
2617
2560
  }
2618
- /** Safely evaluates an AST node with a given context */
2619
- declare class Evaluator {
2620
- private readonly scope;
2621
- private readonly expression;
2622
- constructor(scope: EvaluationScope, expression: string);
2623
- /** Evaluate an AST node */
2624
- evaluate(node: ASTNode): unknown;
2625
- private evaluateIdentifier;
2626
- private evaluateMemberAccess;
2627
- private evaluateComputedMemberAccess;
2628
- private evaluateBinaryOp;
2629
- private evaluateUnaryOp;
2630
- private evaluateCallExpression;
2631
- private evaluateArrayLiteral;
2632
- private evaluateObjectLiteral;
2633
- private evaluateArrowFunction;
2634
- private isMethodSafe;
2635
- }
2636
2561
 
2637
2562
  /** Secure expression parser that uses AST-based evaluation */
2638
2563
  declare class ExpressionParser {
@@ -2652,66 +2577,6 @@ declare class ExpressionParser {
2652
2577
  };
2653
2578
  }
2654
2579
 
2655
- /**
2656
- * Parses tokens into an Abstract Syntax Tree (AST)
2657
- * Uses recursive descent parsing with operator precedence
2658
- */
2659
- declare class Parser {
2660
- private tokens;
2661
- private current;
2662
- private readonly expression;
2663
- constructor(expression: string);
2664
- /** Parse the expression into an AST */
2665
- parse(): ASTNode;
2666
- private parseExpression;
2667
- /**
2668
- * Ternary `test ? consequent : alternate` — right-associative, sits between
2669
- * the arrow-function check and logical-or in precedence.
2670
- */
2671
- private parseConditional;
2672
- /**
2673
- * Attempts to parse an arrow function. Returns null and leaves position
2674
- * unchanged if the next tokens don't form an arrow function head.
2675
- */
2676
- private tryParseArrowFunction;
2677
- private checkAt;
2678
- private parseLogicalOr;
2679
- private parseLogicalAnd;
2680
- private parseEquality;
2681
- private parseComparison;
2682
- private parseAddition;
2683
- private parseMultiplication;
2684
- private parseUnary;
2685
- private parsePostfix;
2686
- private parsePrimary;
2687
- private parseArgumentList;
2688
- private parseArrayElements;
2689
- private parseObjectProperties;
2690
- private match;
2691
- private check;
2692
- private advance;
2693
- private isAtEnd;
2694
- private peek;
2695
- private previous;
2696
- private consume;
2697
- }
2698
-
2699
- /** Tokenizes an expression string into tokens */
2700
- declare class Tokenizer {
2701
- private position;
2702
- private readonly expression;
2703
- constructor(expression: string);
2704
- /** Tokenize the entire expression */
2705
- tokenize(): Token[];
2706
- private skipWhitespace;
2707
- private nextToken;
2708
- private readString;
2709
- private readNumber;
2710
- private readIdentifierOrKeyword;
2711
- private getKeywordType;
2712
- private readOperatorOrPunctuation;
2713
- }
2714
-
2715
2580
  /** TTL cache for HTTP condition responses with max entries eviction. */
2716
2581
  declare class HttpConditionCache {
2717
2582
  private readonly cache;
@@ -2730,9 +2595,6 @@ declare const HTTP_CONDITION_CACHE: InjectionToken<HttpConditionCache>;
2730
2595
  */
2731
2596
  declare function resolveHttpRequest(config: HttpRequestConfig, context: EvaluationContext): HttpResourceRequest | null;
2732
2597
 
2733
- /** Evaluates an HTTP response against a `HttpValidationResponseMapping` to produce a validation result. */
2734
- declare function evaluateHttpValidationResponse(response: unknown, mapping: HttpValidationResponseMapping, logger: Logger): ValidationError$1 | null;
2735
-
2736
2598
  /**
2737
2599
  * Service that provides field evaluation context by combining
2738
2600
  * field context with root form registry information.
@@ -2793,7 +2655,7 @@ declare class FunctionRegistryService {
2793
2655
  * @param fn - Function that receives EvaluationContext and returns any value
2794
2656
  * @param options - Optional configuration for the function
2795
2657
  */
2796
- registerCustomFunction(name: string, fn: CustomFunction, options?: CustomFunctionOptions): void;
2658
+ registerCustomFunction<TFormValue extends Record<string, unknown> = Record<string, unknown>>(name: string, fn: CustomFunction<TFormValue>, options?: CustomFunctionOptions): void;
2797
2659
  /**
2798
2660
  * Get the scope of a registered custom function.
2799
2661
  *
@@ -2829,7 +2691,7 @@ declare class FunctionRegistryService {
2829
2691
  *
2830
2692
  * @param derivations - Object mapping function names to derivation functions
2831
2693
  */
2832
- setDerivationFunctions(derivations: Record<string, CustomFunction> | undefined): void;
2694
+ setDerivationFunctions<TFormValue extends Record<string, unknown> = Record<string, unknown>>(derivations: Record<string, CustomFunction<TFormValue>> | undefined): void;
2833
2695
  /** Clear all derivation functions */
2834
2696
  clearDerivationFunctions(): void;
2835
2697
  /**
@@ -2849,7 +2711,7 @@ declare class FunctionRegistryService {
2849
2711
  *
2850
2712
  * @param fns - Object mapping function names to async derivation functions
2851
2713
  */
2852
- setAsyncDerivationFunctions(fns: Record<string, AsyncDerivationFunction> | undefined): void;
2714
+ setAsyncDerivationFunctions<TFormValue extends Record<string, unknown> = Record<string, unknown>>(fns: Record<string, AsyncDerivationFunction<TFormValue>> | undefined): void;
2853
2715
  /** Clear all async derivation functions */
2854
2716
  clearAsyncDerivationFunctions(): void;
2855
2717
  /**
@@ -2869,7 +2731,7 @@ declare class FunctionRegistryService {
2869
2731
  *
2870
2732
  * @param fns - Object mapping function names to async condition functions
2871
2733
  */
2872
- setAsyncConditionFunctions(fns: Record<string, AsyncConditionFunction> | undefined): void;
2734
+ setAsyncConditionFunctions<TFormValue extends Record<string, unknown> = Record<string, unknown>>(fns: Record<string, AsyncConditionFunction<TFormValue>> | undefined): void;
2873
2735
  /** Clear all async condition functions */
2874
2736
  clearAsyncConditionFunctions(): void;
2875
2737
  /**
@@ -3135,19 +2997,6 @@ interface TokenContext {
3135
2997
  */
3136
2998
  declare function resolveTokens(args: readonly (string | number | boolean | null | undefined)[], context: TokenContext): (string | number | boolean | null | undefined | unknown)[];
3137
2999
 
3138
- interface FieldWithHiddenLogic {
3139
- hidden?: boolean;
3140
- logic?: readonly (ContainerLogicConfig | LogicConfig)[];
3141
- }
3142
- /**
3143
- * Applies hidden logic to a mapper's input record.
3144
- *
3145
- * @param inputs The mutable input record to potentially add `hidden` to
3146
- * @param fieldDef The field definition containing `hidden` and `logic`
3147
- * @param rootFormRegistry The root form registry for accessing form state
3148
- */
3149
- declare function applyHiddenLogic(inputs: Record<string, unknown>, fieldDef: FieldWithHiddenLogic, rootFormRegistry: RootFormRegistryService): void;
3150
-
3151
3000
  /**
3152
3001
  * Maps an array field definition to component inputs.
3153
3002
  *
@@ -3559,21 +3408,6 @@ declare function dynamicTextToObservable(value: DynamicText | undefined, injecto
3559
3408
  /** Normalises a {@link DynamicValue} into a `Signal<T>`. */
3560
3409
  declare function resolveDynamicValue<T>(value: DynamicValue<T> | undefined, fallback: T, injector?: Injector): Signal<T>;
3561
3410
 
3562
- /**
3563
- * Generates CSS class string for responsive grid layout based on field column configuration.
3564
- *
3565
- * @param fieldDef - Field definition containing column configuration
3566
- * @returns CSS class string for grid layout, empty if no valid column configuration
3567
- */
3568
- declare function getGridClassString(fieldDef: FieldDef<unknown>): string;
3569
- /**
3570
- * Builds a combined className string from a field definition.
3571
- *
3572
- * @param fieldDef - Field definition containing className and col properties
3573
- * @returns Combined class string, or undefined if no classes
3574
- */
3575
- declare function buildClassName(fieldDef: FieldDef<unknown>): string | undefined;
3576
-
3577
3411
  /**
3578
3412
  * Injection token for configuring the initialization timeout in milliseconds.
3579
3413
  * Defaults to 10 seconds. When the timeout is reached, a warning is logged
@@ -3692,18 +3526,5 @@ declare function assertNever(value: never): never;
3692
3526
  */
3693
3527
  declare function withPreviousValue<T>(input: Resource<T>): Resource<T>;
3694
3528
 
3695
- /**
3696
- * Safely reads `pathKeys` from a FieldContext.
3697
- * Returns an empty array if `pathKeys` is not available (e.g., in tests or older Angular versions).
3698
- */
3699
- declare function safeReadPathKeys(fieldContext: FieldContext<unknown>): readonly string[];
3700
-
3701
- /**
3702
- * Serializes a value to a deterministic string for cache key generation.
3703
- * Unlike JSON.stringify, this sorts object keys to ensure consistent output
3704
- * regardless of property insertion order.
3705
- */
3706
- declare function stableStringify(value: unknown): string;
3707
-
3708
- 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 };
3709
- 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 };
3529
+ 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, 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, READONLY_FIELD_TREE_CACHE, RootFormRegistryService, TokenType, WRAPPER_AUTO_ASSOCIATIONS, WRAPPER_COMPONENT_CACHE, WRAPPER_REGISTRY, applyMetaToElement, applyValidator, applyValidators, arrayFieldMapper, assertNever, baseFieldMapper, buildBaseInputs, compareValues, containerFieldMapper, createAddonActionRegistry, createDebouncedLogicFunction, createDefaultDerivationLogConfig, createDynamicValueFunction, createFormFieldStateMap, createInitializationTracker, createLogicFunction, createWarningTracker, deepMergeDefaults, derivedFromDeferred, detectFormMode, dynamicTextToObservable, evaluateCondition, evaluateNonFieldDisabled, evaluateNonFieldHidden, extractExpressionDependencies, extractStringDependencies, getArrayLength, getChangedKeys, getFieldValueHandling, 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, setupInitializationTracking, shouldLog, textFieldMapper, toReadonlyFieldTree, toReadonlyFieldTreeCached, validatePageNesting, withPreviousValue, writeToFieldValue };
3530
+ 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, DeclarativeHttpValidatorConfig, DerivationLogConfig, DerivationLogLevel, DerivationLogicConfig, DerivedFromDeferredOptions, DynamicFormActionRegistry, DynamicFormAddonActionPresetRegistry, DynamicFormAddonRegistry, DynamicFormAddonSlotRegistry, DynamicFormFieldRegistry, DynamicText, DynamicValue, EvaluationContext, 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, UnaryOpNode, ValidationError, ValidationExecutionConfig, ValidationMessages, ValidatorConfig, ValueExclusionConfig, ValueFieldComponent, ValueHandlingMode, ValueType, WarningTracker, WithInputSignals, WrapperAutoAssociations, WrapperConfig, WrapperFieldInputs, WrapperTypeDefinition };
@@ -1 +0,0 @@
1
- {"version":3,"file":"ng-forge-dynamic-forms-array-field.component-D5qCzJsL.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"]}