@ng-forge/dynamic-forms 1.1.0-next.1 → 1.1.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-CIheZh11.mjs → ng-forge-dynamic-forms-array-field.component-CftX-bCB.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-CIheZh11.mjs.map → ng-forge-dynamic-forms-array-field.component-CftX-bCB.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C9aVachF.mjs → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-CFSJLZPZ.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C9aVachF.mjs.map → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-CFSJLZPZ.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-DNafCSO8.mjs → ng-forge-dynamic-forms-container-field.component-B_bQ-f2g.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-DNafCSO8.mjs.map → ng-forge-dynamic-forms-container-field.component-B_bQ-f2g.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-BmHGlmES.mjs → ng-forge-dynamic-forms-css-wrapper.component-D_yRuLpu.mjs} +4 -3
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-D_yRuLpu.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-CKVIw2K5.mjs → ng-forge-dynamic-forms-group-field.component-Bl9WRPvF.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-CKVIw2K5.mjs.map → ng-forge-dynamic-forms-group-field.component-Bl9WRPvF.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +12 -1
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs +40 -24
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-fMB33l2f.mjs} +22 -14
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-fMB33l2f.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-BRixkgyS.mjs → ng-forge-dynamic-forms-page-field.component-C9bQkVjQ.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-BRixkgyS.mjs.map → ng-forge-dynamic-forms-page-field.component-C9bQkVjQ.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +1 -1
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-BmHGlmES.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ng-forge-dynamic-forms-integration.mjs","mappings":";;;;;;;;;;;;;;AA2BE;;AAGE;wBAGE;;AAIE;;AAGF;;;AAGN;;;;AAmBE;AACE;AACA;;AAGF;AACE;;;AAMF;AACE;;AAGF;AACF;AAQM;IAIJ;AACA;;;AAQA;AACF;;ACvFM;AACJ;AACA;;;;AAME;AACE;AACA;;AAGF;AACE;;gBAKF;;AAEA;AACE;;AAGF;AACF;AACF;;AChCA;;AAEI;;AAEF;AACE;;IAGF;AACA;AACF;AAQM;AACJ;AACA;;;QAME;;;AAGA;;;AAGA;;;AAIA;AACF;AACF;;AC5BM;AACJ;AACA;;;QAOE;AAEA;AACF;AACF;;ACrBM;AACJ;AACA;;;QAME;AACE;;AAEF;AACE;;AAEF;AACE;;AAGF;AACF;AACF;;;;ACeE;;;;AAKM;;;;AAID;AACD;;;;AAOE;;;;AAID;AACD;;;;QAIF;AACE;;AAEF;AACE;;;AAIJ;AACF;;;;;;AAmBI;AACE;;;;YAKD;;AAGH;AACF;AAQM;AACJ;AACA;;AAEF;;AC9GM;AAGJ;AACA;;;AAIE;AAEA;AACE;;;AAIA;AACA;;AAEJ;AACF;;;ACcE;AACA;IAGA;;AAGE;AAEI;;IAKN;AACE;;AACA;;;;;;AAMA;;;AAGJ;;AAeE;AACE;AACA,kCACA;;;;AAKA;;AAGF;AACF;AAeM;;AAKN;;AC/BM;AACJ;AACA;AACA;AACA;;;;AAMI;AACA;AACA;AACA;AACA;;AAEJ;AACF;AAMM;AACJ;AACA;AACA;AACA;;;;AAMI;AACA;AACA;AACA;AACA;;AAEJ;AACF;AAMM;AACJ;AACA;AACA;AACA;;;;AAMI;AACA;AACA;AACA;AACA;;AAEJ;AACF;AAUM;AACJ;AACA;AACA;AACA;YAKA;AACA;;;;AAMI;AACA;;;AAGA;;AAEJ;AACF;AAGM;AACJ;AACA;AACA;AACA;;;;AAMI;AACA;;;AAGA;;AAEJ;AACF;AAGM;AACJ;AACA;AACA;AACA;;;;AAMI;AACA;;;AAGA;;AAEJ;AACF;;AC5LM;AACJ;AACA;AACA;AACA;;;;AAME;;;AAMM;;;;;AAKD;;AAGL;AACE;;;;;AAOF;AACE;;AAGF;AACF;AACF;AAOM;AACJ;AACA;AACA;AACA;AACA;;;;AAME;;;;;;;AAQC;AAED;AACE;AACA;;;;AAKF;AACE;;AAGF;AACF;AACF;AAOM;AACJ;AACA;AACA;;;;AAME;AAEA;AACE;AACA;;QAIF;AACE;;;AAIF;AACE;;AAGF;AACF;AACF;;;;;;;;;;QC1GE;AACE;AACA;;;;;;;;AAaI;;;QAOF;AACE;AACA;;QAMN;AACF;AAQA;;;;;AAkBI;AAEI;AAEJ;;;;AAOA,2BACA;;;AAME;;AAGN;;AC9GM;;AAEF;AACA;AAEA;AACF;AACF;;ACgBA;AAKE;AACA;;AAEA;AACA;AACA;AAEA;;YAGI;AACA;AACA;AAEA;;AAEA;;;;;;;;AAgBI;;AAEA;AAEI;;;wBAOR;;;;AAKA;AACE;AACA;;;AAGL;AACH;;;AAeA;;AAkBE;AAGI;AACA;;AAIE;AACA;AACA;AACA;;AAEJ;AAGJ;;AC/HM;;AAOF;;;;;;AAMA;AACF;AACF;;;;;;;;;;;;;ACrBW;AACT;AACE;AACA;AACA;AACD;AACF;;;;;gBC0BkB;IAER;IAEA;;;AAOA;;AACA;;AACA;;AACA;;AACA;;AACA;;YAMQ;YAOR;AAKA;;;;;;;AAUA;AACP;;;;AAIO;AACP;;;AAGO;;;;AAaP;;;AAKF;;;AAMI;;AAEI;;AACA;AACA;;AACA;;;;AAIA;;AAGE;AACF;AACE;;;AAEA;;;AAGL;;;;;;;;AA5GI;AACT;AACE;AACA;AACD;AACF;;AAkHM;AAIP;AAKA;;AAmBE;AACF;;;gBCrJmB;AACR;AACA;;;;;;;AASA;;AACA;;AACA;;AACA;;AACA;;YAMQ;;AAGjB;AAQE;AACA;;AACA;;AAEE;;;AAGF;AACE;AACI;AACA;AACA;AACA;AACD;AACH;AACF;;;;;;;AA3DO;AACT;AACE;AACA;AACD;AACF;;;AAoED;AACA;;AAOE;AACF;;;ICnFmB;;;AAKjB,IAKiB;;AACR;AAEA;;AAEP;;;;AAIO;;;;;;AAIT,IAIS;AACP;;AAEE;AACA;;;AAGF;;;AAGF,IAIiB;AAEjB;AACE;AACE;AACA;AACA;;;4DAQI;AACA;AACA;;AAEE;AACA;;;;AAGJ;;AAEF;AACF;;;;;;;;;AAKG;AAGP;AAEA;;;AA0Ba;;;;AAHF;AACT;AACD;;;AAoBC;AACF;;;;;;;ACrGmB;AACA;;;;;AAKjB,IASiB;AACA;;;;;AAIR;;AACA;;AAET;AACE;AACE;AACA;AACF;;;AAIA;AACA;;;AAGE;AAAc;;AAEhB;;AAGF;AAOE;AACA;AAEA;AACA;AACE;;AAKF;AACE;;;;AAMA;;;AAIF;AACE;;;;;;;AAOQ;AACA;;;;AAMV;AACE;;;;AAOJ;AAME;AACA;AACA;AACA;;;;AAOI;AACA;AACA;;;;;AAKF;AACA;AACA;;;;;;;;;;;AAQN;AAKA;;;AAMa;;;;AAHF;AACT;AACD;;;AASC;AACF;;;;AC7JE;AACE;AACA;;;;;;;;;;;AAcF;AACE;AACA;;;;;;;;;;;;;;;;;AC7BO;AACT;;;AAGC;AACF;;;;;;;;AAQU;AACT;;;AAGC;AACF;;;ACKK;;AAMJ;AACA;;;AAGA;AACA;AACE;AAAqB;;AAIjB;;AAGN;AAEA;;AAEqB;;;;AAKrB;AACE;;;;AAKF;AACF;AAuBM;;;;;;;;;AAWJ;;AACA;;AACA;AACE;;;;AAKF;AACF;AAGM;AACJ;AACF","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms/integration/src/mappers/value/value-field.mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/checkbox/checkbox-field-mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/datepicker/datepicker-field-mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/select/options-field-mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/slider/slider-field-mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/button/non-field-logic.utils.ts","../../../../packages/dynamic-forms/integration/src/mappers/button/button-field-mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/button/array-button.utils.ts","../../../../packages/dynamic-forms/integration/src/mappers/button/array-button.mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/button/navigation-button.mapper.ts","../../../../packages/dynamic-forms/integration/src/utils/create-resolved-errors-signal.ts","../../../../packages/dynamic-forms/integration/src/utils/should-show-errors.ts","../../../../packages/dynamic-forms/integration/src/utils/setup-meta-tracking.ts","../../../../packages/dynamic-forms/integration/src/utils/create-aria-described-by.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-field-shell.directive.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-field.directive.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-action.directive.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-addons.directive.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-addon-action.directive.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-controls.ts","../../../../packages/dynamic-forms/integration/src/directives/host-directive-presets.ts","../../../../packages/dynamic-forms/integration/src/testing/create-field-fixture.ts"],"sourcesContent":["import { BaseValueField, FieldMeta } from '@ng-forge/dynamic-forms/internal';\nimport { computed, inject, Signal } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\nimport { buildBaseInputs, DEFAULT_PROPS } from '@ng-forge/dynamic-forms/internal';\nimport { injectFieldSignalContext } from '@ng-forge/dynamic-forms/internal';\nimport { omit } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Context for value field mapping, containing the injected context reference.\n * Used by specialized mappers to avoid duplicate injection.\n */\nexport interface ValueFieldContext {\n /**\n * Gets the field tree for a given key from the form context.\n * This is reactive for array items - the form getter is evaluated each time,\n * allowing the computed to re-run when the root form structure updates.\n */\n getFieldTree: (fieldKey: string) => FieldTree<unknown> | undefined;\n}\n\n/**\n * Creates a value field context from the injected FIELD_SIGNAL_CONTEXT.\n * Must be called within an injection context.\n *\n * @returns The value field context with reactive field tree accessor\n */\nexport function resolveValueFieldContext(): ValueFieldContext {\n const context = injectFieldSignalContext();\n\n return {\n getFieldTree: (fieldKey: string) => {\n // Access context.form here (inside a function that will be called from a computed)\n // This allows the getter to be evaluated reactively\n const formRoot = context.form as Record<string, FieldTree<unknown> | undefined> | undefined;\n\n // Handle case where form is undefined (e.g., newly added array items before root form updates)\n if (!formRoot) {\n return undefined;\n }\n\n return formRoot[fieldKey];\n },\n };\n}\n\n/**\n * Builds the base inputs for a value field.\n * This is a helper function used by valueFieldMapper and specialized mappers.\n *\n * @param fieldDef The value field definition\n * @param ctx The resolved value field context\n * @param defaultProps Default props from the form configuration (may be undefined if not configured)\n * @returns Record of input names to values\n */\nexport function buildValueFieldInputs<TProps, TValue = unknown>(\n fieldDef: BaseValueField<TProps, TValue, FieldMeta, boolean>,\n ctx: ValueFieldContext,\n defaultProps?: Record<string, unknown>,\n): Record<string, unknown> {\n const omittedFields = omit(fieldDef, ['value']);\n const baseInputs = buildBaseInputs(omittedFields, defaultProps);\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n validationMessages: fieldDef.validationMessages ?? {},\n };\n\n if (fieldDef.placeholder !== undefined) {\n inputs['placeholder'] = fieldDef.placeholder;\n }\n\n // Resolve field tree reactively - this call is inside a computed,\n // so it establishes the reactive dependency correctly\n const fieldTree = ctx.getFieldTree(fieldDef.key);\n if (fieldTree !== undefined) {\n inputs['field'] = fieldTree;\n }\n\n return inputs;\n}\n\n/**\n * Maps a value field definition to component inputs.\n *\n * @param fieldDef The value field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function valueFieldMapper<TProps = unknown, TValue = unknown>(\n fieldDef: BaseValueField<TProps, TValue, FieldMeta, boolean>,\n): Signal<Record<string, unknown>> {\n // Get the context once at injection time (captures the FIELD_SIGNAL_CONTEXT reference)\n const ctx = resolveValueFieldContext();\n const defaultProps = inject(DEFAULT_PROPS);\n\n // The computed calls ctx.getFieldTree(fieldDef.key) inside, which:\n // 1. Accesses context.form (triggering the getter for array items)\n // 2. Returns the field tree if available, or undefined if not yet\n // 3. Re-runs when the form structure updates (reactive dependency)\n return computed(() => {\n return buildValueFieldInputs(fieldDef, ctx, defaultProps());\n });\n}\n","import { BaseCheckedField, FieldMeta } from '@ng-forge/dynamic-forms/internal';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { computed, inject, Signal } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\nimport { buildBaseInputs, DEFAULT_PROPS } from '@ng-forge/dynamic-forms/internal';\nimport { injectFieldSignalContext } from '@ng-forge/dynamic-forms/internal';\nimport { omit } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Maps a checkbox/toggle field definition to component inputs.\n *\n * @param fieldDef The checkbox field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function checkboxFieldMapper(fieldDef: BaseCheckedField<unknown, FieldMeta, boolean>): Signal<Record<string, unknown>> {\n const context = injectFieldSignalContext();\n const defaultProps = inject(DEFAULT_PROPS);\n\n return computed(() => {\n const omittedFields = omit(fieldDef, ['value']) as FieldDef<unknown>;\n const baseInputs = buildBaseInputs(omittedFields, defaultProps());\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n validationMessages: fieldDef.validationMessages ?? {},\n };\n\n if (fieldDef.placeholder !== undefined) {\n inputs['placeholder'] = fieldDef.placeholder;\n }\n\n // Access form inside computed for reactivity and to handle cases where\n // form may not be immediately available (e.g., during array item initialization)\n const formRoot = context.form as Record<string, FieldTree<unknown> | undefined> | undefined;\n const fieldTree = formRoot?.[fieldDef.key];\n if (fieldTree !== undefined) {\n inputs['field'] = fieldTree;\n }\n\n return inputs;\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { DEFAULT_PROPS } from '@ng-forge/dynamic-forms/internal';\nimport { DatepickerField } from '../../definitions';\nimport { resolveValueFieldContext, buildValueFieldInputs } from '../value/value-field.mapper';\n\n/**\n * Converts a date value (string, Date, or null) to a Date object or null.\n * This ensures UI libraries that expect Date objects receive the correct type.\n */\nfunction toDate(value: Date | string | null | undefined): Date | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (value instanceof Date) {\n return value;\n }\n // Parse string to Date\n const date = new Date(value);\n return isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Maps a datepicker field to component inputs.\n *\n * @param fieldDef The datepicker field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function datepickerFieldMapper<TProps>(fieldDef: DatepickerField<TProps>): Signal<Record<string, unknown>> {\n const ctx = resolveValueFieldContext();\n const defaultProps = inject(DEFAULT_PROPS);\n\n return computed(() => {\n const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());\n\n // Add datepicker-specific properties, converting strings to Date objects\n if (fieldDef.minDate !== undefined) {\n inputs['minDate'] = toDate(fieldDef.minDate);\n }\n if (fieldDef.maxDate !== undefined) {\n inputs['maxDate'] = toDate(fieldDef.maxDate);\n }\n if (fieldDef.startAt !== undefined) {\n inputs['startAt'] = toDate(fieldDef.startAt);\n }\n\n return inputs;\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { SelectField } from '../../definitions';\nimport { RadioField } from '../../definitions';\nimport { MultiCheckboxField } from '../../definitions';\nimport { BaseValueField, DEFAULT_PROPS, FieldMeta } from '@ng-forge/dynamic-forms/internal';\nimport { resolveValueFieldContext, buildValueFieldInputs } from '../value/value-field.mapper';\n\n/** Field types that have an options property. */\nexport type FieldWithOptions<T = unknown, TProps = unknown> =\n | SelectField<T, TProps>\n | RadioField<T, TProps>\n | MultiCheckboxField<T, TProps>;\n\n/**\n * Maps a field with options (select, radio, multi-checkbox) to component inputs.\n *\n * @param fieldDef The field definition with options\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function optionsFieldMapper<T, TProps>(fieldDef: FieldWithOptions<T, TProps>): Signal<Record<string, unknown>> {\n const ctx = resolveValueFieldContext();\n const defaultProps = inject(DEFAULT_PROPS);\n\n return computed(() => {\n // Cast to BaseValueField - safe because all FieldWithOptions types extend BaseValueField\n const inputs = buildValueFieldInputs(fieldDef as BaseValueField<TProps, unknown, FieldMeta, boolean>, ctx, defaultProps());\n\n // Add options property\n inputs['options'] = fieldDef.options;\n\n return inputs;\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { DEFAULT_PROPS } from '@ng-forge/dynamic-forms/internal';\nimport { SliderField } from '../../definitions';\nimport { resolveValueFieldContext, buildValueFieldInputs } from '../value/value-field.mapper';\n\n/**\n * Maps a slider field to component inputs.\n *\n * @param fieldDef The slider field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function sliderFieldMapper<TProps>(fieldDef: SliderField<TProps>): Signal<Record<string, unknown>> {\n const ctx = resolveValueFieldContext();\n const defaultProps = inject(DEFAULT_PROPS);\n\n return computed(() => {\n const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());\n\n // Add slider-specific properties (these are at field level, not in props)\n if (fieldDef.minValue !== undefined) {\n inputs['minValue'] = fieldDef.minValue;\n }\n if (fieldDef.maxValue !== undefined) {\n inputs['maxValue'] = fieldDef.maxValue;\n }\n if (fieldDef.step !== undefined) {\n inputs['step'] = fieldDef.step;\n }\n\n return inputs;\n });\n}\n","import { inject } from '@angular/core';\nimport {\n EvaluationContext,\n injectNonFieldEvaluationContext,\n LogicConfig,\n NonFieldLogicConfig,\n resolveNonFieldDisabled,\n resolveNonFieldHidden,\n RootFormRegistryService,\n} from '@ng-forge/dynamic-forms/internal';\nimport { FieldTree } from '@angular/forms/signals';\n\n// Single source lives in @ng-forge/dynamic-forms/internal; re-exported so the button\n// mappers keep importing it from this module.\nexport { injectNonFieldEvaluationContext };\n\n/**\n * Field definition with optional hidden/disabled and logic properties.\n * This is the minimal interface needed by applyNonFieldLogic.\n */\nexport interface FieldDefWithLogic {\n key?: string;\n hidden?: boolean;\n disabled?: boolean;\n logic?: NonFieldLogicConfig[] | LogicConfig[];\n}\n\n/**\n * Result of applying non-field logic to a field.\n * Only includes properties that have values to apply.\n */\nexport interface NonFieldLogicResult {\n hidden?: boolean;\n disabled?: boolean;\n}\n\n/**\n * Applies hidden and disabled logic to a non-form-bound field.\n * Must be called inside a computed() context (reads form signals).\n */\nexport function applyNonFieldLogic(\n rootFormRegistry: RootFormRegistryService,\n fieldDef: FieldDefWithLogic,\n evaluationContext?: () => EvaluationContext,\n): NonFieldLogicResult {\n const result: NonFieldLogicResult = {};\n const rootForm = rootFormRegistry.rootForm();\n\n if (rootForm) {\n if (fieldDef.hidden !== undefined || fieldDef.logic?.some((l) => l.type === 'hidden')) {\n const hiddenSignal = resolveNonFieldHidden({\n form: rootForm,\n fieldLogic: fieldDef.logic,\n explicitValue: fieldDef.hidden,\n evaluationContext,\n });\n result.hidden = hiddenSignal();\n }\n\n // Resolve disabled state if explicitly set or has logic\n if (fieldDef.disabled !== undefined || fieldDef.logic?.some((l) => l.type === 'disabled')) {\n // Create the signal and READ IT to establish reactive dependencies\n const disabledSignal = resolveNonFieldDisabled({\n form: rootForm,\n fieldLogic: fieldDef.logic,\n explicitValue: fieldDef.disabled,\n evaluationContext,\n });\n result.disabled = disabledSignal();\n }\n } else {\n // Fallback to static values when rootForm is not available\n if (fieldDef.hidden !== undefined) {\n result.hidden = fieldDef.hidden;\n }\n if (fieldDef.disabled !== undefined) {\n result.disabled = fieldDef.disabled;\n }\n }\n\n return result;\n}\n\n/**\n * Applies hidden logic to a non-form-bound field.\n *\n * @param rootForm The root form FieldTree (can be undefined)\n * @param fieldDef The field definition with optional hidden/logic\n * @returns The resolved hidden boolean value, or undefined if no hidden logic\n */\nexport function resolveHiddenValue(\n rootForm: FieldTree<unknown, string | number> | undefined,\n fieldDef: FieldDefWithLogic,\n evaluationContext?: () => EvaluationContext,\n): boolean | undefined {\n if (!rootForm) {\n return fieldDef.hidden;\n }\n\n if (fieldDef.hidden !== undefined || fieldDef.logic?.some((l) => l.type === 'hidden')) {\n return resolveNonFieldHidden({\n form: rootForm,\n fieldLogic: fieldDef.logic,\n explicitValue: fieldDef.hidden,\n evaluationContext,\n // formValue omitted intentionally - resolver will read form.value() reactively\n })();\n }\n\n return undefined;\n}\n\n/**\n * Injects everything a non-form-bound button mapper needs to evaluate hidden/disabled logic,\n * returning a thunk to invoke inside the mapper's computed. Used by adapter button mappers so a\n * generic `{ type: 'button', logic: [...] }` evaluates its conditions with `externalData` and\n * custom functions in scope, matching the built-in button field types.\n */\nexport function injectNonFieldLogicResolver(fieldDef: FieldDefWithLogic): () => NonFieldLogicResult {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n return () => applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext);\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { FormEvent } from '@ng-forge/dynamic-forms';\nimport { buildBaseInputs, DEFAULT_PROPS } from '@ng-forge/dynamic-forms/internal';\nimport { ButtonField } from '../../definitions';\nimport { injectNonFieldLogicResolver } from './non-field-logic.utils';\n\n/**\n * Maps a button field to component inputs.\n *\n * @param fieldDef The button field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function buttonFieldMapper<TProps, TEvent extends FormEvent>(\n fieldDef: ButtonField<TProps, TEvent>,\n): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const resolveLogic = injectNonFieldLogicResolver(fieldDef);\n\n return computed(() => {\n const inputs = buildBaseInputs(fieldDef, defaultProps());\n inputs['event'] = fieldDef.event;\n\n if (fieldDef.eventArgs !== undefined) {\n inputs['eventArgs'] = fieldDef.eventArgs;\n }\n\n return {\n ...inputs,\n ...resolveLogic(),\n };\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms';\nimport { ARRAY_CONTEXT, ArrayAllowedChildren } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Context for array button mapping, containing resolved array information.\n * Used by array button mappers to build event context.\n */\nexport interface ArrayButtonContext {\n /** The target array key (from explicit arrayKey or ARRAY_CONTEXT) */\n targetArrayKey: string | undefined;\n /** Computed signal that returns the current array index (-1 if not inside array) */\n indexSignal: Signal<number>;\n /** The current form value for token resolution */\n formValue: Record<string, unknown>;\n /** Whether this button is inside an array context */\n isInsideArray: boolean;\n}\n\n/** Event context passed to button components for array operations. */\nexport interface ArrayButtonEventContext {\n /** The button field key */\n key: string;\n /** The current array index (-1 if outside array) */\n index: number;\n /** The target array key */\n arrayKey: string;\n /** The form value for token resolution */\n formValue: Record<string, unknown>;\n /**\n * Optional template for add operations.\n * Can be a single field (primitive item) or array of fields (object item).\n */\n template?: ArrayAllowedChildren | readonly ArrayAllowedChildren[];\n}\n\n/**\n * Resolves array button context for button mappers.\n *\n * @param fieldKey The button field key (for logging)\n * @param buttonType The button type name (for logging)\n * @param explicitArrayKey Optional explicit arrayKey from field definition\n * @returns The array button context with reactive index signal\n */\nexport function resolveArrayButtonContext(fieldKey: string, buttonType: string, explicitArrayKey?: string): ArrayButtonContext {\n const arrayContext = inject(ARRAY_CONTEXT, { optional: true });\n const logger = inject(DynamicFormLogger);\n\n // Priority: explicit arrayKey from fieldDef > arrayKey from context\n const targetArrayKey = explicitArrayKey ?? arrayContext?.arrayKey;\n\n if (!targetArrayKey) {\n logger.warn(\n `${buttonType} button \"${fieldKey}\" has no array context. ` +\n 'Either place it inside an array field, or provide an explicit arrayKey property.',\n );\n }\n\n // Create index signal once at injection time - computed to track arrayContext.index changes\n const indexSignal = computed(() => {\n if (!arrayContext) return -1;\n return arrayContext.index();\n });\n\n return {\n targetArrayKey,\n indexSignal,\n formValue: (arrayContext?.formValue ?? {}) as Record<string, unknown>,\n isInsideArray: arrayContext !== null,\n };\n}\n\n/**\n * Builds the event context passed to the button component.\n *\n * @param fieldKey The button field key\n * @param ctx The resolved array button context\n * @param template Optional template for add operations (single field or array of fields)\n * @returns The event context for the button component\n */\nexport function buildArrayButtonEventContext(\n fieldKey: string,\n ctx: ArrayButtonContext,\n template?: ArrayAllowedChildren | readonly ArrayAllowedChildren[],\n): ArrayButtonEventContext {\n const eventContext: ArrayButtonEventContext = {\n key: fieldKey,\n index: ctx.indexSignal(), // Evaluated inside computed\n arrayKey: ctx.targetArrayKey ?? '',\n formValue: ctx.formValue,\n };\n\n if (template) {\n eventContext.template = template;\n }\n\n return eventContext;\n}\n\n/**\n * Event argument type - supports static values and tokens.\n * Tokens: $key, $index, $arrayKey, $template, formValue\n */\nexport type EventArg = string | number | boolean | null | undefined;\n\n/**\n * Resolves eventArgs with defaults for array buttons.\n *\n * @param fieldEventArgs The eventArgs from field definition (may be undefined)\n * @param defaultEventArgs The default eventArgs to use if not specified\n * @returns The resolved eventArgs array\n */\nexport function resolveArrayButtonEventArgs(\n fieldEventArgs: readonly EventArg[] | undefined,\n defaultEventArgs: readonly EventArg[] = ['$arrayKey', '$template'],\n): readonly EventArg[] {\n return fieldEventArgs ?? defaultEventArgs;\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport {\n AppendArrayItemEvent,\n InsertArrayItemEvent,\n PopArrayItemEvent,\n PrependArrayItemEvent,\n RemoveAtIndexEvent,\n ShiftArrayItemEvent,\n} from '@ng-forge/dynamic-forms';\nimport {\n ArrayAllowedChildren,\n buildBaseInputs,\n DEFAULT_PROPS,\n NonFieldLogicConfig,\n RootFormRegistryService,\n} from '@ng-forge/dynamic-forms/internal';\nimport { buildArrayButtonEventContext, EventArg, resolveArrayButtonContext, resolveArrayButtonEventArgs } from './array-button.utils';\nimport { applyNonFieldLogic, injectNonFieldEvaluationContext } from './non-field-logic.utils';\n\n// =============================================================================\n// Base Interfaces\n// =============================================================================\n\n/**\n * Base interface for array add button fields (append, prepend, insert).\n * Template is required to define the structure for new items.\n */\nexport interface BaseArrayAddButtonField<TProps = unknown> {\n key: string;\n type: string;\n label?: string;\n disabled?: boolean;\n hidden?: boolean;\n className?: string;\n props?: TProps;\n /** Target array key (required when outside array, optional inside) */\n arrayKey?: string;\n /**\n * Template defining the field structure for new items. REQUIRED.\n * - Single field (ArrayAllowedChildren): Creates a primitive item (field's value is extracted directly)\n * - Array of fields (ArrayAllowedChildren[]): Creates an object item (fields merged into object)\n */\n template: ArrayAllowedChildren | readonly ArrayAllowedChildren[];\n /** Optional custom eventArgs */\n eventArgs?: readonly EventArg[];\n /** Logic rules for dynamic hidden/disabled state (only hidden and disabled are supported) */\n logic?: NonFieldLogicConfig[];\n}\n\n/**\n * Base interface for array remove button fields (remove, pop, shift).\n * No template needed - removes existing items.\n */\nexport interface BaseArrayRemoveButtonField<TProps = unknown> {\n key: string;\n type: string;\n label?: string;\n disabled?: boolean;\n hidden?: boolean;\n className?: string;\n props?: TProps;\n /** Target array key (required when outside array, optional inside) */\n arrayKey?: string;\n /** Optional custom eventArgs */\n eventArgs?: readonly EventArg[];\n /** Logic rules for dynamic hidden/disabled state (only hidden and disabled are supported) */\n logic?: NonFieldLogicConfig[];\n}\n\n/**\n * Interface for insert array item button fields.\n * Extends add button with required index.\n */\nexport interface BaseInsertArrayItemButtonField<TProps = unknown> extends BaseArrayAddButtonField<TProps> {\n /** The index at which to insert the new item */\n index: number;\n}\n\n// =============================================================================\n// Add Array Item Mappers\n// =============================================================================\n\n/**\n * Mapper for add array item button — appends new item to end of array.\n * Template is REQUIRED to define the field structure for new items.\n */\nexport function addArrayItemButtonMapper<TProps>(fieldDef: BaseArrayAddButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'addArrayItem', fieldDef.arrayKey);\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: AppendArrayItemEvent,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', '$template']),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n\n/**\n * Mapper for prepend array item button — inserts new item at beginning of array.\n * Template is REQUIRED to define the field structure for new items.\n */\nexport function prependArrayItemButtonMapper<TProps>(fieldDef: BaseArrayAddButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'prependArrayItem', fieldDef.arrayKey);\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: PrependArrayItemEvent,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', '$template']),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n\n/**\n * Mapper for insert array item button — inserts new item at specific index.\n * Template and index are REQUIRED.\n */\nexport function insertArrayItemButtonMapper<TProps>(fieldDef: BaseInsertArrayItemButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'insertArrayItem', fieldDef.arrayKey);\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: InsertArrayItemEvent,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', fieldDef.index, '$template']),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n\n// =============================================================================\n// Remove Array Item Mappers\n// =============================================================================\n\n/**\n * Mapper for remove array item button.\n * Inside array: RemoveAtIndexEvent. Outside array: PopArrayItemEvent.\n */\nexport function removeArrayItemButtonMapper<TProps>(fieldDef: BaseArrayRemoveButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'removeArrayItem', fieldDef.arrayKey);\n\n // Choose event type based on context:\n // - Inside array: RemoveAtIndexEvent (remove specific item)\n // - Outside array: PopArrayItemEvent (remove last item)\n const eventType = ctx.isInsideArray ? RemoveAtIndexEvent : PopArrayItemEvent;\n const defaultEventArgs = ctx.isInsideArray ? ['$arrayKey', '$index'] : ['$arrayKey'];\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: eventType,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, defaultEventArgs),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n\n/** Mapper for pop array item button — removes the last item from array. */\nexport function popArrayItemButtonMapper<TProps>(fieldDef: BaseArrayRemoveButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'popArrayItem', fieldDef.arrayKey);\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: PopArrayItemEvent,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey']),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n\n/** Mapper for shift array item button — removes the first item from array. */\nexport function shiftArrayItemButtonMapper<TProps>(fieldDef: BaseArrayRemoveButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'shiftArrayItem', fieldDef.arrayKey);\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: ShiftArrayItemEvent,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey']),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { FieldWithValidation, FormSubmitEvent, NextPageEvent, PreviousPageEvent } from '@ng-forge/dynamic-forms';\nimport {\n buildBaseInputs,\n DEFAULT_PROPS,\n FieldDef,\n injectFieldSignalContext,\n FORM_OPTIONS,\n resolveNextButtonDisabled,\n resolveSubmitButtonDisabled,\n RootFormRegistryService,\n} from '@ng-forge/dynamic-forms/internal';\nimport { ButtonField } from '../../definitions';\nimport { injectNonFieldEvaluationContext, resolveHiddenValue } from './non-field-logic.utils';\n\n// =============================================================================\n// Base Interfaces\n// =============================================================================\n\n/** Base interface for navigation button fields (submit, next, previous). */\nexport type BaseNavigationButtonField<TProps = unknown> = ButtonField<TProps, FormSubmitEvent | NextPageEvent | PreviousPageEvent> & {\n type: string;\n /** Logic rules for dynamic disabled state */\n logic?: FieldWithValidation['logic'];\n};\n\n// =============================================================================\n// Submit Button Mapper\n// =============================================================================\n\n/**\n * Mapper for submit button — configures native form submission via type=\"submit\".\n * Triggers native form submission rather than dispatching events directly.\n */\nexport function submitButtonFieldMapper<TProps>(fieldDef: BaseNavigationButtonField<TProps>): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const defaultProps = inject(DEFAULT_PROPS);\n const formOptions = inject(FORM_OPTIONS);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n\n const fieldWithLogic = fieldDef as FieldDef<Record<string, unknown>> & Partial<FieldWithValidation>;\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n const rootForm = rootFormRegistry.rootForm();\n\n // Use rootForm (not fieldSignalContext.form) — submit needs root form validity (#157).\n // Before the root form is available, fall back to the explicit disabled value.\n const disabled = rootForm\n ? resolveSubmitButtonDisabled({\n form: rootForm,\n formOptions: formOptions(),\n fieldLogic: fieldWithLogic.logic,\n explicitlyDisabled: fieldDef.disabled,\n evaluationContext,\n })()\n : (fieldDef.disabled ?? false);\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n // type=\"submit\" triggers native form submission (no event dispatch needed)\n props: { ...(fieldDef.props as Record<string, unknown>), type: 'submit' },\n disabled,\n };\n\n const hidden = resolveHiddenValue(rootForm, fieldWithLogic, evaluationContext);\n if (hidden !== undefined) {\n inputs['hidden'] = hidden;\n }\n\n return inputs;\n });\n}\n\n// =============================================================================\n// Next Page Button Mapper\n// =============================================================================\n\n/** Mapper for next page button — preconfigures NextPageEvent. */\nexport function nextButtonFieldMapper<TProps>(fieldDef: BaseNavigationButtonField<TProps>): Signal<Record<string, unknown>> {\n const fieldSignalContext = injectFieldSignalContext();\n const rootFormRegistry = inject(RootFormRegistryService);\n const defaultProps = inject(DEFAULT_PROPS);\n const formOptions = inject(FORM_OPTIONS);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n\n const fieldWithLogic = fieldDef as FieldDef<Record<string, unknown>> & Partial<FieldWithValidation>;\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n const rootForm = rootFormRegistry.rootForm();\n\n const disabled = resolveNextButtonDisabled({\n form: fieldSignalContext.form,\n formOptions: formOptions(),\n fieldLogic: fieldWithLogic.logic,\n currentPageValid: fieldSignalContext.currentPageValid,\n evaluationContext,\n })();\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n event: NextPageEvent,\n disabled,\n };\n\n const hidden = resolveHiddenValue(rootForm, fieldWithLogic, evaluationContext);\n if (hidden !== undefined) {\n inputs['hidden'] = hidden;\n }\n\n return inputs;\n });\n}\n\n// =============================================================================\n// Previous Page Button Mapper\n// =============================================================================\n\n/** Mapper for previous page button — preconfigures PreviousPageEvent. */\nexport function previousButtonFieldMapper<TProps>(fieldDef: BaseNavigationButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n\n const fieldWithLogic = fieldDef as FieldDef<Record<string, unknown>> & Partial<FieldWithValidation>;\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n const rootForm = rootFormRegistry.rootForm();\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n event: PreviousPageEvent,\n };\n\n // Add disabled binding only if explicitly set by user\n if (fieldDef.disabled !== undefined) {\n inputs['disabled'] = fieldDef.disabled;\n }\n\n const hidden = resolveHiddenValue(rootForm, fieldWithLogic, evaluationContext);\n if (hidden !== undefined) {\n inputs['hidden'] = hidden;\n }\n\n return inputs;\n });\n}\n","import { computed, inject, Injector, signal, Signal } from '@angular/core';\nimport { toObservable, toSignal } from '@angular/core/rxjs-interop';\nimport { FieldTree, ValidationError } from '@angular/forms/signals';\nimport { combineLatest, Observable, of } from 'rxjs';\nimport { map, switchMap } from 'rxjs/operators';\nimport { ValidationMessages } from '@ng-forge/dynamic-forms/internal';\nimport { dynamicTextToObservable } from '@ng-forge/dynamic-forms/internal';\nimport { interpolateParams } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms';\nimport type { Logger } from '@ng-forge/dynamic-forms';\n\n/** Resolved validation error with interpolated message */\nexport interface ResolvedError {\n kind: string;\n message: string;\n}\n\n/**\n * Factory function that creates a signal of resolved validation errors\n * Handles async resolution of DynamicText validation messages\n *\n * @param field - Signal containing FieldTree\n * @param validationMessages - Signal containing custom field-level validation messages\n * @param defaultValidationMessages - Signal containing default validation messages (fallback)\n * @param injector - Optional injector for DynamicText resolution\n * @returns Signal<ResolvedError[]> - Reactively resolved error messages\n */\nexport function createResolvedErrorsSignal<T>(\n field: Signal<FieldTree<T>>,\n validationMessages: Signal<ValidationMessages | undefined>,\n defaultValidationMessages: Signal<ValidationMessages | undefined> = signal(undefined),\n injector = inject(Injector),\n): Signal<ResolvedError[]> {\n // Get logger from injector\n const logger = injector.get(DynamicFormLogger);\n\n // Ensure validationMessages is never undefined (mappers pass {} if not defined)\n const messages = computed(() => validationMessages() ?? {});\n const defaultMessages = computed(() => defaultValidationMessages() ?? {});\n\n // Create a computed signal that reads the actual errors from the field\n // This ensures the signal tracks changes to field().errors(), not just the field reference\n const errors = computed(() => {\n const control = field()();\n return control.errors();\n });\n\n // Convert signals to observables using toObservable with injector\n const errors$ = toObservable(errors, { injector });\n const messages$ = toObservable(messages, { injector });\n const defaultMessages$ = toObservable(defaultMessages, { injector });\n\n // Combine observables and process errors\n const resolvedErrors$ = combineLatest([errors$, messages$, defaultMessages$]).pipe(\n switchMap(([currentErrors, msgs, defaultMsgs]) => {\n // No errors - return empty array\n if (!currentErrors || currentErrors.length === 0) {\n return of([]);\n }\n\n // Create observable for each error's resolved message\n const errorResolvers = currentErrors.map((error: ValidationError) => resolveErrorMessage(error, msgs, defaultMsgs, injector, logger));\n\n // Combine all error message observables into single array emission, filtering out nulls\n return errorResolvers.length > 0\n ? combineLatest(errorResolvers).pipe(map((errors) => errors.filter((e): e is ResolvedError => e !== null)))\n : of([]);\n }),\n );\n\n // Convert observable back to signal using toSignal with injector\n // toSignal properly handles the injection context and manages subscriptions\n return toSignal(resolvedErrors$, { initialValue: [] as ResolvedError[], injector });\n}\n\n/**\n * Resolves a single error message from DynamicText sources with fallback logic\n * Priority: field-level message → default message → error.message → no message (logs warning)\n *\n * @internal\n */\nfunction resolveErrorMessage(\n error: ValidationError,\n fieldMessages: ValidationMessages,\n defaultMessages: ValidationMessages,\n injector: Injector,\n logger: Logger,\n): Observable<ResolvedError | null> {\n // Check for field-level custom message first\n const fieldMessage = fieldMessages[error.kind];\n\n // Fall back to default message if no field-level message exists\n const defaultMessage = defaultMessages[error.kind];\n\n // Determine which message to use: field-level → default → error.message (from schema validators)\n const messageToUse = fieldMessage ?? defaultMessage ?? error.message;\n\n // If no message found, log warning and return null (will be filtered out)\n if (!messageToUse) {\n logger.warn(\n `No validation message found for error kind \"${error.kind}\". ` +\n `Please provide a message via field-level validationMessages or form-level defaultValidationMessages.`,\n );\n return of(null);\n }\n\n // If using error.message directly (from schema validators), wrap in of() instead of dynamicTextToObservable\n // error.message is always a plain string, not DynamicText\n const isSchemaMessage = fieldMessage === undefined && defaultMessage === undefined;\n const messageObservable = isSchemaMessage\n ? of(messageToUse as string) // error.message is always string\n : dynamicTextToObservable(messageToUse, injector);\n\n // Apply parameter interpolation to support {{param}} syntax\n return messageObservable.pipe(\n map((msg) => ({\n kind: error.kind,\n message: interpolateParams(msg, error),\n })),\n );\n}\n","import { Signal, computed } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\n\n/**\n * Computed signal that determines if errors should be displayed.\n * Based on field's invalid, touched, and error count.\n *\n * @param field - Signal containing FieldTree\n * @returns Signal<boolean> - True if errors should be displayed\n */\nexport function shouldShowErrors<T>(field: Signal<FieldTree<T>>): Signal<boolean> {\n return computed(() => {\n const fieldTree = field();\n const control = fieldTree();\n\n return control.invalid() && control.touched() && control.errors().length > 0;\n });\n}\n","import { afterRenderEffect, ElementRef, isDevMode, Signal } from '@angular/core';\nimport { applyMetaToElement, FieldMeta, isEqual } from '@ng-forge/dynamic-forms/internal';\n\n/** Configuration options for setupMetaTracking. */\nexport interface MetaTrackingOptions {\n /**\n * CSS selector for target elements within the host.\n * If not provided, meta attributes are applied directly to the host element.\n */\n selector?: string;\n\n /**\n * Additional signals to track that should trigger re-application of meta.\n * Useful for components where DOM elements are dynamically created (e.g., radio options).\n */\n dependents?: Signal<unknown>[];\n}\n\n// Shared options: selector may be string or accessor; computeAttrs is the\n// merged attribute record (meta + optional parent aria).\ninterface ApplyAttrsOptions {\n readonly selector?: string | (() => string | undefined);\n readonly dependents?: readonly Signal<unknown>[];\n}\n\n/**\n * Resolve target set, compute combined attrs, apply via applyMetaToElement.\n * Target resolution is cached by selector identity — re-resolves only on\n * selector change or `dependents` change. afterRenderEffect is signal-tracked,\n * so the callback re-runs only when computeAttrs / dependents differ.\n *\n * @internal\n */\nfunction applyTrackedAttrsToTargets(\n elementRef: ElementRef<HTMLElement>,\n computeAttrs: () => FieldMeta | undefined,\n options: ApplyAttrsOptions = {},\n): void {\n const appliedByElement = new WeakMap<Element, Set<string>>();\n let cachedSelector: string | undefined;\n let cachedTargets: Element[] = [];\n let previousAttrs: FieldMeta | undefined;\n let previousDeps: readonly unknown[] | undefined;\n const warnedSelectors = isDevMode() ? new Set<string>() : undefined;\n\n afterRenderEffect({\n write: () => {\n // Read dependents first so afterRenderEffect tracks them.\n const currentDeps = options.dependents?.map((d) => d());\n const attrs = computeAttrs();\n const selector = (typeof options.selector === 'function' ? options.selector() : options.selector)?.trim() || undefined;\n\n const selectorChanged = selector !== cachedSelector;\n const prevDeps = previousDeps;\n const depsChanged =\n !!currentDeps && (!prevDeps || currentDeps.length !== prevDeps.length || currentDeps.some((d, i) => d !== prevDeps[i]));\n\n if (selectorChanged || depsChanged || cachedTargets.length === 0) {\n cachedSelector = selector;\n cachedTargets = selector ? Array.from(elementRef.nativeElement.querySelectorAll(selector)) : [elementRef.nativeElement];\n }\n\n previousDeps = currentDeps;\n\n // Dev-mode safety net: selector typo or wrong descendant query leaves\n // meta + aria unapplied. Warn the first time we'd apply non-empty attrs\n // to zero matches for a given selector. Re-warns only when the selector\n // string changes (so authors fixing one typo see the next).\n if (warnedSelectors && selector && cachedTargets.length === 0 && attrs && Object.keys(attrs).length > 0) {\n if (!warnedSelectors.has(selector)) {\n warnedSelectors.add(selector);\n const tag = elementRef.nativeElement.tagName.toLowerCase();\n console.warn(\n `[Dynamic Forms] ngForgeControl selector \"${selector}\" matched no descendants of <${tag}> — meta + aria attributes will not be applied. ` +\n `Check the selector for typos, or supply 'dependents' if the target is rendered after a signal emits.`,\n );\n }\n }\n\n // Bail when attrs unchanged AND nothing structural moved — keeps repeated\n // effect runs (driven by an unrelated tracked-signal flicker) cheap.\n if (!selectorChanged && !depsChanged && isEqual(attrs, previousAttrs)) {\n return;\n }\n previousAttrs = attrs;\n\n for (const el of cachedTargets) {\n const applied = appliedByElement.get(el) ?? new Set<string>();\n appliedByElement.set(el, applyMetaToElement(el as HTMLElement, attrs, applied));\n }\n },\n });\n}\n\n/**\n * Sets up automatic meta attribute tracking and application for wrapped components.\n *\n * @param elementRef - Reference to the host element\n * @param meta - Signal containing meta attributes to apply\n * @param options - Configuration options\n */\nexport function setupMetaTracking(\n elementRef: ElementRef<HTMLElement>,\n meta: Signal<FieldMeta | undefined>,\n options?: MetaTrackingOptions,\n): void {\n applyTrackedAttrsToTargets(elementRef, () => meta(), { selector: options?.selector, dependents: options?.dependents });\n}\n\n/**\n * Marker-directive variant: writes parent meta + aria to resolved targets.\n * Parent aria wins on key collision. Internal to NgForgeControl/HostControl.\n *\n * @internal\n */\nexport function setupMarkerAttrTracking(\n elementRef: ElementRef<HTMLElement>,\n parent: {\n readonly meta: Signal<FieldMeta | undefined>;\n readonly ariaInvalid: Signal<boolean>;\n readonly ariaRequired: Signal<true | null>;\n readonly ariaDescribedBy: Signal<string | null>;\n },\n selector: () => string | undefined,\n): void {\n applyTrackedAttrsToTargets(\n elementRef,\n () => {\n const ariaRequired = parent.ariaRequired();\n const ariaDescribedBy = parent.ariaDescribedBy();\n // Skip null aria-required/describedby so applyMetaToElement removes the\n // attr (rather than serializing \"null\") on transitions.\n return {\n ...(parent.meta() ?? {}),\n 'aria-invalid': parent.ariaInvalid(),\n ...(ariaRequired === null ? {} : { 'aria-required': ariaRequired }),\n ...(ariaDescribedBy === null ? {} : { 'aria-describedby': ariaDescribedBy }),\n };\n },\n { selector },\n );\n}\n","import { computed, Signal } from '@angular/core';\nimport { ResolvedError } from './create-resolved-errors-signal';\n\n/**\n * Creates a signal that computes the aria-describedby value based on errors and hint state.\n * Errors take precedence over hints - when errors are displayed, the hint is hidden.\n * Only the first error is displayed (single error ID, not indexed).\n *\n * @param errorsToDisplay Signal containing the array of errors currently being displayed\n * @param errorId Signal containing the ID for the error element (single error only)\n * @param hintId Signal containing the ID for the hint element\n * @param hasHint Function that returns true if a hint is configured\n * @returns Signal containing the aria-describedby value or null\n */\nexport function createAriaDescribedBySignal(\n errorsToDisplay: Signal<ResolvedError[]>,\n errorId: Signal<string>,\n hintId: Signal<string>,\n hasHint: () => boolean,\n): Signal<string | null> {\n return computed(() => {\n if (errorsToDisplay().length > 0) {\n return errorId();\n }\n if (hasHint()) {\n return hintId();\n }\n return null;\n });\n}\n","import { Directive, input } from '@angular/core';\n\n/**\n * Base directive composed into every ng-forge component via `hostDirectives`.\n * Owns the universal identity inputs (`key`, `className`) and the host\n * bindings that every field shape — value-bearing, action, future container —\n * shares: `[id]`, `[attr.data-testid]`, `[class]`.\n */\n@Directive({\n host: {\n '[id]': 'key()',\n '[attr.data-testid]': 'key()',\n '[class]': 'className()',\n },\n})\nexport class NgForgeFieldShell {\n readonly key = input.required<string>();\n readonly className = input<string>('');\n}\n\n/** Input names forwarded onto `NgForgeFieldShell` via `hostDirectives`. */\nexport const NG_FORGE_FIELD_SHELL_INPUTS = ['key', 'className'] as const;\n","import { afterRenderEffect, computed, Directive, inject, input, isDevMode, Signal, signal, untracked } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms';\nimport { DEFAULT_VALIDATION_MESSAGES, DynamicText, FieldMeta, ValidationMessages } from '@ng-forge/dynamic-forms/internal';\nimport { createAriaDescribedBySignal } from '../utils/create-aria-described-by';\nimport { createResolvedErrorsSignal, ResolvedError } from '../utils/create-resolved-errors-signal';\nimport { shouldShowErrors } from '../utils/should-show-errors';\nimport type { AssertTupleLockstep } from './assert-input-lockstep';\nimport { NgForgeFieldShell } from './ng-forge-field-shell.directive';\n\n/**\n * Value-bearing add-on directive — composed alongside `NgForgeFieldShell` via\n * the `NG_FORGE_FIELD` preset. Owns the value/validation/aria plumbing every\n * value-bearing field needs.\n *\n * @if (ngf.errorsToDisplay()[0]; as e) {\n * <span [id]=\"ngf.errorId()\">{{ e.message }}</span>\n * }\n * `,\n * })\n * export class MyInputField {\n * protected readonly ngf = injectNgForgeField<string>();\n * }\n * ```\n *\n * Selectorless — usage is exclusively via `hostDirectives`. Prefer the\n * `NG_FORGE_FIELD` preset over composing this directive manually.\n */\n@Directive({\n host: {\n '[attr.hidden]': 'field()().hidden() || null',\n '[attr.aria-disabled]': 'field()().disabled() || null',\n },\n})\nexport class NgForgeField {\n // ───────────────────────────────────────────────────────────────────────────\n // Shell-owned identity — re-exported so component templates keep using\n // `ngf.key()` / `ngf.className()` without reaching for the Shell directly.\n // ───────────────────────────────────────────────────────────────────────────\n\n private readonly shell = inject(NgForgeFieldShell);\n /** Shell-owned key, re-exported as plain `Signal<string>` so it doesn't get flagged by the host-directive input-lockstep assertion. */\n readonly key: Signal<string> = this.shell.key;\n /** Shell-owned className, re-exported as plain `Signal<string>` for the same reason. */\n readonly className: Signal<string> = this.shell.className;\n\n // ───────────────────────────────────────────────────────────────────────────\n // Forwarded inputs — the value-field-specific subset of mapper output.\n // ───────────────────────────────────────────────────────────────────────────\n\n readonly field = input.required<FieldTree<unknown>>();\n readonly label = input<DynamicText>();\n readonly placeholder = input<DynamicText>();\n readonly tabIndex = input<number>();\n readonly props = input<unknown>();\n readonly meta = input<FieldMeta>();\n readonly validationMessages = input<ValidationMessages>();\n\n // ───────────────────────────────────────────────────────────────────────────\n // Wiring\n // ───────────────────────────────────────────────────────────────────────────\n\n private readonly defaultValidationMessages: Signal<ValidationMessages | undefined> =\n inject(DEFAULT_VALIDATION_MESSAGES, { optional: true }) ?? signal(undefined);\n\n // ───────────────────────────────────────────────────────────────────────────\n // Error display\n // ───────────────────────────────────────────────────────────────────────────\n\n readonly errors: Signal<ResolvedError[]> = createResolvedErrorsSignal(\n this.field,\n this.validationMessages,\n this.defaultValidationMessages,\n );\n readonly showErrors: Signal<boolean> = shouldShowErrors(this.field);\n readonly errorsToDisplay: Signal<ResolvedError[]> = computed(() => (this.showErrors() ? this.errors() : []));\n\n // ───────────────────────────────────────────────────────────────────────────\n // Accessibility helpers\n // ───────────────────────────────────────────────────────────────────────────\n\n readonly errorId: Signal<string> = computed(() => `${this.key()}-error`);\n readonly hintId: Signal<string> = computed(() => `${this.key()}-hint`);\n\n readonly ariaInvalid: Signal<boolean> = computed(() => {\n const state = this.field()();\n return state.invalid() && state.touched();\n });\n\n readonly ariaRequired: Signal<true | null> = computed(() => {\n return this.field()().required?.() === true ? true : null;\n });\n\n readonly ariaDescribedBy: Signal<string | null> = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () =>\n Boolean((this.props() as { hint?: unknown } | undefined)?.hint),\n );\n\n // ───────────────────────────────────────────────────────────────────────────\n // Claim tracking — markers and ambient sub-components flip this to true so\n // dev-mode can warn when meta() is non-empty but nothing consumed it.\n // ───────────────────────────────────────────────────────────────────────────\n\n private readonly _claimed = signal(false);\n\n /** @internal Marker directives + ambient sub-components call this on construction. */\n markClaimed(): void {\n this._claimed.set(true);\n }\n\n private readonly logger = inject(DynamicFormLogger, { optional: true });\n\n constructor() {\n // Dev-mode safety net: a mapper or template-author binds meta() but no\n // NgForgeControl / NgForgeHostControl / ambient sub-component is in\n // place to consume it. Fires at most once per directive instance.\n if (isDevMode()) {\n let warned = false;\n afterRenderEffect({\n write: () => {\n if (warned) return;\n const meta = this.meta();\n if (!meta || Object.keys(meta).length === 0) return;\n if (untracked(this._claimed)) return;\n warned = true;\n // `untracked` safe — afterRenderEffect.write runs after first CD, so Shell's required `key` is bound (no NG0950).\n const key = untracked(this.key);\n const message =\n `NgForgeField - meta() provided for field \"${key}\" but no NgForgeControl / NgForgeHostControl / ` +\n `ambient sub-component claimed it. Meta will not be applied. ` +\n `See https://ng-forge.dev/building-an-adapter#meta-forwarding`;\n if (this.logger) {\n this.logger.warn(message);\n } else {\n console.warn(message);\n }\n },\n });\n }\n }\n}\n\n/**\n * Inputs forwarded onto `NgForgeField` via `hostDirectives`. Excludes\n * `key` / `className` (forwarded to `NgForgeFieldShell` separately via\n * `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this\n * directly — use the `NG_FORGE_FIELD` preset.\n */\nexport const NG_FORGE_VALUE_FIELD_INPUTS = ['field', 'label', 'placeholder', 'tabIndex', 'props', 'meta', 'validationMessages'] as const;\n\n// Compile-time lockstep: NG_FORGE_VALUE_FIELD_INPUTS must equal the declared\n// `input()` properties on NgForgeField. Drift in either direction fails the build.\nconst _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP: AssertTupleLockstep<\n NgForgeField,\n typeof NG_FORGE_VALUE_FIELD_INPUTS,\n 'NG_FORGE_VALUE_FIELD_INPUTS'\n> = true;\nvoid _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP;\n\n/**\n * `NgForgeField` typed for a specific value type. The directive itself stores\n * `field` as `Signal<FieldTree<unknown>>` because Angular's `hostDirectives`\n * doesn't propagate generics — `injectNgForgeField<T>()` narrows it at the\n * inject site so consumers don't have to declare a separate cast computed.\n */\nexport type TypedNgForgeField<T> = Omit<NgForgeField, 'field'> & {\n readonly field: Signal<FieldTree<T>>;\n};\n\n/**\n * Typed wrapper around `inject(NgForgeField)`. The generic narrows the\n * `field` signal to `Signal<FieldTree<T>>` for the calling component's value\n * type. The cast is unchecked — the runtime contract is that mappers only\n * forward a `field` whose value type matches the field-type definition.\n */\nexport function injectNgForgeField<T = unknown>(): TypedNgForgeField<T> {\n return inject(NgForgeField) as unknown as TypedNgForgeField<T>;\n}\n","import { Directive, inject, input, Signal } from '@angular/core';\nimport { type ArrayItemContext, type FormEvent, type FormEventConstructor } from '@ng-forge/dynamic-forms';\nimport { EventBus, resolveTokens } from '@ng-forge/dynamic-forms/internal';\nimport { ARRAY_CONTEXT, type DynamicText } from '@ng-forge/dynamic-forms/internal';\nimport type { EventArgs } from '../definitions/button-field';\nimport type { AssertTupleLockstep } from './assert-input-lockstep';\nimport { NgForgeFieldShell } from './ng-forge-field-shell.directive';\n\n/**\n * Action add-on directive — composed alongside `NgForgeFieldShell` via the\n * `NG_FORGE_ACTION` preset. Owns the inputs + dispatch logic every button /\n * action component needs (label, disabled, event, eventArgs, …) plus the\n * `[attr.hidden]` / `[attr.aria-disabled]` host bindings driven by its own\n * inputs (no field tree).\n */\n@Directive({\n host: {\n '[attr.hidden]': 'hidden() || null',\n '[attr.aria-disabled]': 'disabled() || null',\n },\n})\nexport class NgForgeAction<TEvent extends FormEvent = FormEvent> {\n // ───────────────────────────────────────────────────────────────────────────\n // Shell-owned identity — re-exported as plain `Signal<T>` so they don't get\n // flagged by the lockstep assertion below.\n // ───────────────────────────────────────────────────────────────────────────\n\n private readonly shell = inject(NgForgeFieldShell);\n readonly key: Signal<string> = this.shell.key;\n readonly className: Signal<string> = this.shell.className;\n\n // ───────────────────────────────────────────────────────────────────────────\n // Forwarded inputs — the action-specific subset of mapper output.\n // ───────────────────────────────────────────────────────────────────────────\n\n readonly label = input.required<DynamicText>();\n readonly disabled = input<boolean>(false);\n readonly hidden = input<boolean>(false);\n readonly tabIndex = input<number>();\n readonly event = input<FormEventConstructor<TEvent>>();\n readonly eventArgs = input<EventArgs>();\n readonly eventContext = input<ArrayItemContext>();\n readonly props = input<unknown>();\n\n // ───────────────────────────────────────────────────────────────────────────\n // Dispatch\n // ───────────────────────────────────────────────────────────────────────────\n\n private readonly eventBus = inject(EventBus);\n private readonly arrayContext = inject(ARRAY_CONTEXT, { optional: true });\n\n /**\n * Dispatch the configured event through the EventBus. Resolves `eventArgs`\n * tokens (`$key`, `$index`, `$arrayKey`, `formValue`, …) against the\n * ambient array context if present, falling back to a static `eventContext`\n * input or the field's own `key` otherwise. No-op when `event` is unset\n * (e.g. submit buttons handled by the native form lifecycle).\n */\n dispatch(): void {\n const event = this.event();\n if (!event) return;\n const args = this.eventArgs();\n if (!args || args.length === 0) {\n this.eventBus.dispatch(event);\n return;\n }\n const context: ArrayItemContext = this.arrayContext\n ? {\n key: this.key(),\n index: this.arrayContext.index(),\n arrayKey: this.arrayContext.arrayKey,\n formValue: this.arrayContext.formValue,\n }\n : (this.eventContext() ?? { key: this.key() });\n this.eventBus.dispatch(event, ...resolveTokens(args, context));\n }\n}\n\n/**\n * Inputs forwarded onto `NgForgeAction` via `hostDirectives`. Excludes\n * `key` / `className` (forwarded to `NgForgeFieldShell` separately via\n * `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this\n * directly — use the `NG_FORGE_ACTION` preset.\n */\nexport const NG_FORGE_ACTION_INPUTS = ['label', 'disabled', 'hidden', 'tabIndex', 'event', 'eventArgs', 'eventContext', 'props'] as const;\n\n// Compile-time lockstep: NG_FORGE_ACTION_INPUTS must equal the declared\n// `input()` properties on NgForgeAction. Drift fails the build.\nconst _NG_FORGE_ACTION_INPUTS_LOCKSTEP: AssertTupleLockstep<NgForgeAction, typeof NG_FORGE_ACTION_INPUTS, 'NG_FORGE_ACTION_INPUTS'> = true;\nvoid _NG_FORGE_ACTION_INPUTS_LOCKSTEP;\n\n/**\n * Typed wrapper around `inject(NgForgeAction)`. The generic narrows the\n * `event` input to a specific FormEvent subclass.\n */\nexport function injectNgForgeAction<TEvent extends FormEvent = FormEvent>(): NgForgeAction<TEvent> {\n return inject(NgForgeAction) as unknown as NgForgeAction<TEvent>;\n}\n","import { computed, Directive, inject, Injector, input, Signal, signal } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { AnyAddon, resolveDynamicValue } from '@ng-forge/dynamic-forms/internal';\nimport type { AssertTupleLockstep } from './assert-input-lockstep';\n\n/**\n * Universal addon-array plumbing: signal-cached `hidden` resolution and\n * slot bucketing. Used internally by the `NgForgeAddons` host wrapper —\n * consumers should compose `NgForgeAddons` directly and inject the\n * directive instance via `injectNgForgeAddons<TAddon>()`.\n */\n@Directive({})\nexport class NgForgeAddonsBase {\n /** Capture the host's injector so `toSignal` (inside `resolveDynamicValue`) ties to the host's `DestroyRef`. */\n private readonly hostInjector = inject(Injector);\n\n /** Addon array mapped from `FieldDef.addons` by `buildBaseInputs`. */\n readonly addons = input<ReadonlyArray<AnyAddon> | undefined>(undefined);\n\n /**\n * Per-addon `hidden` signal cache. Built via `explicitEffect` rather\n * than `computed` because `toSignal` (inside `resolveDynamicValue` for\n * Observable inputs) is disallowed in reactive contexts (NG0602).\n */\n private readonly _hiddenSignalCache = signal<ReadonlyMap<AnyAddon, Signal<boolean>>>(new Map());\n readonly hiddenSignalCache: Signal<ReadonlyMap<AnyAddon, Signal<boolean>>> = this._hiddenSignalCache.asReadonly();\n\n readonly visibleAddons: Signal<ReadonlyArray<AnyAddon>> = computed(() => {\n const list = this.addons() ?? [];\n const cache = this.hiddenSignalCache();\n return list.filter((a) => !(cache.get(a)?.() ?? false));\n });\n\n readonly hasAddons: Signal<boolean> = computed(() => this.visibleAddons().length > 0);\n readonly prefixAddons: Signal<ReadonlyArray<AnyAddon>> = computed(() => this.visibleAddons().filter((a) => a.slot === 'prefix'));\n readonly suffixAddons: Signal<ReadonlyArray<AnyAddon>> = computed(() => this.visibleAddons().filter((a) => a.slot === 'suffix'));\n\n /**\n * Visible addons grouped by their `slot` string. Convenience view for\n * adapters with non-universal slots (e.g., Ionic `start` / `end`).\n */\n readonly addonsBySlot: Signal<ReadonlyMap<string, ReadonlyArray<AnyAddon>>> = computed(() => {\n const map = new Map<string, AnyAddon[]>();\n for (const a of this.visibleAddons()) {\n const bucket = map.get(a.slot) ?? [];\n bucket.push(a);\n map.set(a.slot, bucket);\n }\n return map as ReadonlyMap<string, ReadonlyArray<AnyAddon>>;\n });\n\n /** Secondary index keyed by `hidden` value identity. Catches the common case\n * where a FormConfig re-emit produces structurally-different-but-content-\n * identical addon objects — without this, every re-emit would spawn a new\n * `toSignal` subscription for each addon's hidden axis. */\n private readonly _hiddenByValue = new WeakMap<object, Signal<boolean>>();\n\n constructor() {\n explicitEffect([this.addons], ([list]) => {\n const prev = this._hiddenSignalCache();\n const next = new Map<AnyAddon, Signal<boolean>>();\n for (const a of list ?? []) {\n // Tier 1: reference identity — the cheapest hit when the same addon\n // object is reused across emissions.\n let resolved = prev.get(a);\n if (!resolved) {\n // Tier 2: hidden-value identity — when `hidden` is an object\n // (Signal / Observable / function), the same instance can be passed\n // through multiple new addon objects on FormConfig re-emit.\n const h = a.hidden;\n const valueKey = typeof h === 'object' && h !== null ? h : null;\n resolved = valueKey ? this._hiddenByValue.get(valueKey) : undefined;\n if (!resolved) {\n resolved = resolveDynamicValue(a.hidden, false, this.hostInjector);\n if (valueKey) this._hiddenByValue.set(valueKey, resolved);\n }\n }\n next.set(a, resolved);\n }\n this._hiddenSignalCache.set(next);\n });\n }\n}\n\n/** Forwarded onto `NgForgeAddonsBase` via the `NgForgeAddons` wrapper. */\nexport const NG_FORGE_ADDONS_INPUTS = ['addons'] as const;\n\n// Compile-time lockstep — drift between the tuple and the declared inputs fails the build.\nconst _NG_FORGE_ADDONS_INPUTS_LOCKSTEP: AssertTupleLockstep<NgForgeAddonsBase, typeof NG_FORGE_ADDONS_INPUTS, 'NG_FORGE_ADDONS_INPUTS'> =\n true;\nvoid _NG_FORGE_ADDONS_INPUTS_LOCKSTEP;\n\n/**\n * Host wrapper for `NgForgeAddonsBase`. Consumers compose this directly:\n *\n * @if (ngfa.hasAddons()) {\n * <p-inputgroup>\n * @for (a of ngfa.prefixAddons(); track $index) {\n * <df-addon-slot [addon]=\"a\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n * }\n * <input ngForgeControl ... />\n * </p-inputgroup>\n * }\n * `,\n * })\n * export class MyInputField {\n * protected readonly ngfa = injectNgForgeAddons<MyInputAddon>();\n * }\n * ```\n *\n * The wrapper exists so `hostDirectives: [NgForgeAddons]` resolves\n * cross-package — same constraint that drives `NgForgeFieldHost`.\n */\n@Directive({\n hostDirectives: [{ directive: NgForgeAddonsBase, inputs: [...NG_FORGE_ADDONS_INPUTS] }],\n})\nexport class NgForgeAddons {}\n\n/** Narrowed view of `NgForgeAddonsBase` returned by `injectNgForgeAddons<TAddon>()`. */\nexport type TypedNgForgeAddons<TAddon extends AnyAddon = AnyAddon> = Omit<\n NgForgeAddonsBase,\n 'addons' | 'visibleAddons' | 'prefixAddons' | 'suffixAddons'\n> & {\n readonly addons: Signal<ReadonlyArray<TAddon> | undefined>;\n readonly visibleAddons: Signal<ReadonlyArray<TAddon>>;\n readonly prefixAddons: Signal<ReadonlyArray<TAddon>>;\n readonly suffixAddons: Signal<ReadonlyArray<TAddon>>;\n};\n\n/**\n * Typed inject helper. Always resolves to the underlying\n * `NgForgeAddonsBase` instance — the wrapper class has no instance\n * fields of its own.\n */\nexport function injectNgForgeAddons<TAddon extends AnyAddon = AnyAddon>(): TypedNgForgeAddons<TAddon> {\n return inject(NgForgeAddonsBase) as unknown as TypedNgForgeAddons<TAddon>;\n}\n","import { computed, Directive, inject, Injector, input, Signal, signal } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { BaseAddon, DynamicFormLogger, DynamicValue } from '@ng-forge/dynamic-forms';\nimport { ADDON_ACTION_REGISTRY, AddonActionHandler, WrapperFieldInputs } from '@ng-forge/dynamic-forms/internal';\nimport { AddonActionContext, resolveDynamicValue, writeToFieldValue } from '@ng-forge/dynamic-forms/internal';\nimport { ADDON_PRESET_HANDLER } from './addon-preset-handler.token';\nimport type { AssertTupleLockstep } from './assert-input-lockstep';\n\n/**\n * Permissive shape the directive reads from `addon()`. Adapter-specific\n * addon types (e.g., `PrimeButtonAddon`) narrow this at the inject site via\n * `injectNgForgeAddonAction<T>()`.\n */\ninterface ActionAddonShape extends BaseAddon {\n readonly preset?: string;\n readonly actionRef?: string;\n readonly action?: AddonActionHandler;\n readonly disabled?: DynamicValue<boolean>;\n readonly loading?: DynamicValue<boolean>;\n}\n\n/**\n * Universal click-dispatch plumbing for action-style addons (icon buttons,\n * labelled buttons, etc.): `disabled` / `loading` resolution from\n * `DynamicValue<boolean>`, action context construction, and the\n * `preset / actionRef / action` precedence dispatcher.\n */\n@Directive({})\nexport class NgForgeAddonActionBase {\n // The action registry is form-scoped. `optional: true` matches the\n // preset handler so addons rendered outside a form that called\n // `withAddonActions(...)` degrade gracefully instead of throwing —\n // `actionRef` dispatches then log an actionable warning and no-op.\n private readonly actionRegistry = inject(ADDON_ACTION_REGISTRY, { optional: true });\n private readonly presetHandler = inject(ADDON_PRESET_HANDLER, { optional: true });\n private readonly logger = inject(DynamicFormLogger);\n private readonly hostInjector = inject(Injector);\n\n readonly addon = input.required<ActionAddonShape>();\n readonly fieldInputs = input<WrapperFieldInputs | undefined>(undefined);\n\n /**\n * Per-value caches for `disabled` / `loading` signal resolution. Keyed by\n * the `DynamicValue` identity so a single `toSignal` subscription is\n * shared across addon-reference swaps that pass the same Observable.\n * Mirrors `NgForgeAddonsBase._hiddenByValue` — `resolveDynamicValue`\n * cannot run inside `computed`/`linkedSignal.computation` because\n * `toSignal` throws in reactive contexts (NG0602), and re-running it on\n * every recomputation would leak subscriptions on each cycle.\n */\n private readonly _disabledByValue = new WeakMap<object, Signal<boolean>>();\n private readonly _loadingByValue = new WeakMap<object, Signal<boolean>>();\n private readonly _resolvedDisabled = signal<Signal<boolean> | undefined>(undefined);\n private readonly _resolvedLoading = signal<Signal<boolean> | undefined>(undefined);\n\n readonly disabled: Signal<boolean> = computed(() => this._resolvedDisabled()?.() ?? false);\n readonly loading: Signal<boolean> = computed(() => this._resolvedLoading()?.() ?? false);\n\n constructor() {\n explicitEffect([this.addon], ([addon]) => {\n this._resolvedDisabled.set(this._resolveAxis(addon.disabled, this._disabledByValue));\n this._resolvedLoading.set(this._resolveAxis(addon.loading, this._loadingByValue));\n });\n }\n\n private _resolveAxis(value: DynamicValue<boolean> | undefined, cache: WeakMap<object, Signal<boolean>>): Signal<boolean> {\n const valueKey = typeof value === 'object' && value !== null ? value : null;\n let resolved = valueKey ? cache.get(valueKey) : undefined;\n if (!resolved) {\n resolved = resolveDynamicValue(value, false, this.hostInjector);\n if (valueKey) cache.set(valueKey, resolved);\n }\n return resolved;\n }\n\n /**\n * Resolve the click handler from the addon's three-axis click variant and\n * invoke it. Precedence on multi-set: `preset > actionRef > action`. The\n * validator drops multi-set configs at config-init time; this is\n * defence-in-depth + a logged warning so the misconfiguration is loud.\n */\n dispatch(): void {\n const addon = this.addon();\n const ctx = this.buildActionContext();\n\n const variantsSet = [addon.preset, addon.actionRef, addon.action].filter((v) => v !== undefined).length;\n if (variantsSet > 1) {\n this.logger.warn(\n `Addon button: more than one of preset/actionRef/action configured — dispatching by precedence (preset > actionRef > action). Ship at most one to silence this.`,\n );\n }\n\n if (addon.preset !== undefined) {\n if (!this.presetHandler) {\n this.logger.warn(\n `Addon button: preset '${addon.preset}' is configured but no ADDON_PRESET_HANDLER is registered in this scope. Did the adapter forget to provide one at the host field?`,\n );\n return;\n }\n void this.presetHandler.run(addon.preset, ctx);\n return;\n }\n\n if (addon.actionRef !== undefined) {\n const handler = this.actionRegistry?.get(addon.actionRef);\n if (handler) {\n handler(ctx);\n } else {\n this.logger.warn(\n `Addon button: actionRef '${addon.actionRef}' is not registered. ` +\n (this.actionRegistry\n ? `Did you call withAddonActions({ ${addon.actionRef}: ... })?`\n : `No withAddonActions(...) registry is reachable in this scope — was the addon rendered outside a form?`),\n );\n }\n return;\n }\n\n if (addon.action !== undefined) {\n addon.action(ctx);\n return;\n }\n\n // Decorative — no handler configured.\n }\n\n /**\n * Build an `AddonActionContext` for handlers from the wrapper-style\n * `fieldInputs` bag. Returns the field-bound variant when a host field\n * is reachable, or the orphan variant otherwise.\n */\n buildActionContext(): AddonActionContext {\n const inputs = this.fieldInputs();\n const key = inputs?.key ?? '';\n const type = inputs?.type ?? '';\n const tree = inputs?.field;\n if (tree) {\n // `writeToFieldValue` centralises the `Signal → WritableSignal` cast and\n // warns at runtime if Signal Forms ever stops storing a writable signal\n // here (the alternative — a silent no-op preset — is much worse).\n const setValue = inputs?.setValue ?? ((next: unknown) => writeToFieldValue(tree.value, next, this.logger));\n return {\n field: { key, type },\n form: tree,\n value: tree.value() ?? undefined,\n setValue,\n };\n }\n return {\n field: { key, type },\n form: null,\n value: undefined,\n };\n }\n}\n\n/** Forwarded onto `NgForgeAddonActionBase` via the `NgForgeAddonAction` wrapper. */\nexport const NG_FORGE_ADDON_ACTION_INPUTS = ['addon', 'fieldInputs'] as const;\n\nconst _NG_FORGE_ADDON_ACTION_INPUTS_LOCKSTEP: AssertTupleLockstep<\n NgForgeAddonActionBase,\n typeof NG_FORGE_ADDON_ACTION_INPUTS,\n 'NG_FORGE_ADDON_ACTION_INPUTS'\n> = true;\nvoid _NG_FORGE_ADDON_ACTION_INPUTS_LOCKSTEP;\n\n/** Host wrapper for `NgForgeAddonActionBase`. Consumers compose this directly: */\n@Directive({\n hostDirectives: [{ directive: NgForgeAddonActionBase, inputs: [...NG_FORGE_ADDON_ACTION_INPUTS] }],\n})\nexport class NgForgeAddonAction {}\n\n/** Narrowed view of `NgForgeAddonActionBase` returned by `injectNgForgeAddonAction<T>()`. */\nexport type TypedNgForgeAddonAction<TAddon extends ActionAddonShape = ActionAddonShape> = Omit<NgForgeAddonActionBase, 'addon'> & {\n readonly addon: Signal<TAddon>;\n};\n\nexport function injectNgForgeAddonAction<TAddon extends ActionAddonShape = ActionAddonShape>(): TypedNgForgeAddonAction<TAddon> {\n return inject(NgForgeAddonActionBase) as unknown as TypedNgForgeAddonAction<TAddon>;\n}\n","import { Directive, ElementRef, inject, input } from '@angular/core';\nimport { setupMarkerAttrTracking } from '../utils/setup-meta-tracking';\nimport { NgForgeField } from './ng-forge-field.directive';\n\n/**\n * Marker directive applied to the canonical control element of an\n * `NgForgeField`-using component. Forwards the field's `meta` AND\n * parent-derived aria signals (`aria-invalid`, `aria-required`,\n * `aria-describedby`) onto a target element.\n *\n * @for (option of options(); track option.value) {\n * <input type=\"radio\" ngForgeControl [value]=\"option.value\" />\n * }\n * ```\n *\n * Each `@for` iteration creates its own directive instance; Angular's\n * structural lifecycle handles add/remove naturally.\n */\n@Directive({ selector: '[ngForgeControl]' })\nexport class NgForgeControl {\n readonly target = input<string | undefined>(undefined, { alias: 'ngForgeControl' });\n\n constructor() {\n const parent = inject(NgForgeField);\n setupMarkerAttrTracking(inject(ElementRef), parent, () => this.target());\n parent.markClaimed();\n }\n}\n\n/**\n * Selectorless companion directive for shadow-DOM-encapsulated components\n * (PrimeNG `p-select`, Ionic web components, etc.) where no light-DOM child\n * is the canonical control. Added to the component's `hostDirectives` so the\n * field's `meta` + parent aria land on the component's own host element.\n */\n// Empty `@Directive({})` is intentional — selectorless; used only via `hostDirectives`.\n@Directive({})\nexport class NgForgeHostControl {\n constructor() {\n const parent = inject(NgForgeField);\n setupMarkerAttrTracking(inject(ElementRef), parent, () => undefined);\n parent.markClaimed();\n }\n}\n","import { Directive } from '@angular/core';\nimport { NgForgeAction, NG_FORGE_ACTION_INPUTS } from './ng-forge-action.directive';\nimport { NgForgeFieldShell, NG_FORGE_FIELD_SHELL_INPUTS } from './ng-forge-field-shell.directive';\nimport { NgForgeField, NG_FORGE_VALUE_FIELD_INPUTS } from './ng-forge-field.directive';\n\n/**\n * Value-bearing field host wrapper. Composes `NgForgeFieldShell` (universal\n * identity) with `NgForgeField` (value + validation + aria + meta-tracking)\n * in a single host-directive entry, so consumer components compose just one\n * directive instead of two.\n */\n@Directive({\n hostDirectives: [\n { directive: NgForgeFieldShell, inputs: [...NG_FORGE_FIELD_SHELL_INPUTS] },\n { directive: NgForgeField, inputs: [...NG_FORGE_VALUE_FIELD_INPUTS] },\n ],\n})\nexport class NgForgeFieldHost {}\n\n/**\n * Action / button host wrapper. Composes `NgForgeFieldShell` with\n * `NgForgeAction` (event dispatch + aria-disabled) for button-shaped\n * components.\n */\n@Directive({\n hostDirectives: [\n { directive: NgForgeFieldShell, inputs: [...NG_FORGE_FIELD_SHELL_INPUTS] },\n { directive: NgForgeAction, inputs: [...NG_FORGE_ACTION_INPUTS] },\n ],\n})\nexport class NgForgeActionHost {}\n","import { EnvironmentInjector, Provider, runInInjectionContext, signal, type Type, type WritableSignal } from '@angular/core';\nimport { ComponentFixture, TestBed } from '@angular/core/testing';\nimport { type FieldTree, form, schema, type SchemaPath } from '@angular/forms/signals';\nimport { type FormEvent, type FormEventConstructor } from '@ng-forge/dynamic-forms';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { DEFAULT_VALIDATION_MESSAGES, type DynamicText, type ValidationMessages } from '@ng-forge/dynamic-forms/internal';\nimport type { EventArgs } from '../definitions/button-field';\n\nexport interface CreateNgForgeFieldFixtureOptions<TValue = unknown> {\n /** Field key. Becomes the only property on the wrapping form. */\n readonly key: string;\n /** Initial value for the field. */\n readonly value?: TValue;\n /** Optional schema callback that receives the field's `SchemaPath`. Use it to attach validators (`required`, `minLength`, etc.). */\n readonly schema?: (fieldPath: SchemaPath<TValue>) => void;\n /** Mark the field as touched after creation (useful for invalid-state assertions). */\n readonly touched?: boolean;\n /** Extra inputs to set on the component before `detectChanges()`. */\n readonly inputs?: Record<string, unknown>;\n /** Extra TestBed providers. */\n readonly providers?: readonly Provider[];\n}\n\nexport interface NgForgeFieldFixture<C, TValue = unknown> {\n readonly fixture: ComponentFixture<C>;\n readonly field: FieldTree<TValue>;\n readonly rootValue: WritableSignal<Record<string, TValue>>;\n}\n\n/**\n * Builds a `ComponentFixture` for an `NgForgeField`-composing component with\n * `field` + `key` bound BEFORE `detectChanges()` — sidesteps the NG0950 that\n * Shell's required `key` and Field's required `field` would otherwise throw.\n */\nexport function createNgForgeFieldFixture<C, TValue = unknown>(\n component: Type<C>,\n options: CreateNgForgeFieldFixtureOptions<TValue>,\n): NgForgeFieldFixture<C, TValue> {\n TestBed.configureTestingModule({ imports: [component], providers: [...(options.providers ?? [])] });\n\n const key = options.key;\n const rootValue = signal<Record<string, TValue>>({ [key]: options.value as TValue });\n\n const injector = TestBed.inject(EnvironmentInjector);\n const schemaCallback = options.schema;\n const root = runInInjectionContext(injector, () => {\n if (!schemaCallback) return form(rootValue);\n return form(\n rootValue,\n schema<Record<string, TValue>>((path) => {\n schemaCallback(path[key] as SchemaPath<TValue>);\n }),\n );\n });\n\n const field = (root as unknown as Record<string, FieldTree<TValue>>)[key];\n\n if (options.touched) field().markAsTouched();\n\n const fixture = TestBed.createComponent(component);\n fixture.componentRef.setInput('field', field);\n fixture.componentRef.setInput('key', key);\n if (options.inputs) {\n for (const [name, value] of Object.entries(options.inputs)) {\n fixture.componentRef.setInput(name, value);\n }\n }\n\n return { fixture, field, rootValue };\n}\n\nexport interface CreateNgForgeActionFixtureOptions<TEvent extends FormEvent = FormEvent> {\n readonly key: string;\n readonly label: DynamicText;\n readonly event?: FormEventConstructor<TEvent>;\n readonly eventArgs?: EventArgs;\n readonly disabled?: boolean;\n readonly hidden?: boolean;\n readonly inputs?: Record<string, unknown>;\n readonly providers?: readonly Provider[];\n}\n\nexport interface NgForgeActionFixture<C> {\n readonly fixture: ComponentFixture<C>;\n readonly eventBus: EventBus;\n}\n\n/**\n * Builds a `ComponentFixture` for an `NgForgeAction`-composing component with\n * `key` + `label` bound and `EventBus` provided. The caller calls\n * `detectChanges()`; the harness handles the NG0950-avoidance bindings.\n */\nexport function createNgForgeActionFixture<C, TEvent extends FormEvent = FormEvent>(\n component: Type<C>,\n options: CreateNgForgeActionFixtureOptions<TEvent>,\n): NgForgeActionFixture<C> {\n TestBed.configureTestingModule({ imports: [component], providers: [EventBus, ...(options.providers ?? [])] });\n\n const fixture = TestBed.createComponent(component);\n fixture.componentRef.setInput('key', options.key);\n fixture.componentRef.setInput('label', options.label);\n if (options.event) fixture.componentRef.setInput('event', options.event);\n if (options.eventArgs) fixture.componentRef.setInput('eventArgs', options.eventArgs);\n if (options.disabled !== undefined) fixture.componentRef.setInput('disabled', options.disabled);\n if (options.hidden !== undefined) fixture.componentRef.setInput('hidden', options.hidden);\n if (options.inputs) {\n for (const [name, value] of Object.entries(options.inputs)) {\n fixture.componentRef.setInput(name, value);\n }\n }\n\n return { fixture, eventBus: TestBed.inject(EventBus) };\n}\n\n/** Provider for `DEFAULT_VALIDATION_MESSAGES` in field tests — supply the default-messages map and the directive resolves it from DI. */\nexport function provideTestValidationMessages(messages: ValidationMessages): Provider {\n return { provide: DEFAULT_VALIDATION_MESSAGES, useValue: signal(messages) };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ng-forge-dynamic-forms-integration.mjs","mappings":";;;;;;;;;;;;;;AA2BE;;AAGE;wBAGE;;AAIE;;AAGF;;;AAGN;;;;AAmBE;AACE;AACA;;AAGF;AACE;;;AAMF;AACE;;AAGF;AACF;AAQM;IAIJ;AACA;;;AAQA;AACF;;ACvFM;AACJ;AACA;;;;AAME;AACE;AACA;;AAGF;AACE;;gBAKF;;AAEA;AACE;;AAGF;AACF;AACF;;AChCA;;AAEI;;AAEF;AACE;;IAGF;AACA;AACF;AAQM;AACJ;AACA;;;QAME;;;AAGA;;;AAGA;;;AAIA;AACF;AACF;;AC5BM;AACJ;AACA;;;QAOE;AAEA;AACF;AACF;;ACrBM;AACJ;AACA;;;QAME;AACE;;AAEF;AACE;;AAEF;AACE;;AAGF;AACF;AACF;;;;ACeE;;;;AAKM;;;;AAID;AACD;;;;AAOE;;;;AAID;AACD;;;;QAIF;AACE;;AAEF;AACE;;;AAIJ;AACF;;;;;;AAmBI;AACE;;;;YAKD;;AAGH;AACF;AAQM;AACJ;AACA;;AAEF;;AC9GM;AAGJ;AACA;;;AAIE;AAEA;AACE;;;AAIA;AACA;;AAEJ;AACF;;;ACcE;AACA;IAGA;;AAGE;AAEI;;IAKN;AACE;;AACA;;;;;;AAMA;;;AAGJ;;AAeE;AACE;AACA,kCACA;;;;AAKA;;AAGF;AACF;AAeM;;AAKN;;AC/BM;AACJ;AACA;AACA;AACA;;;;AAMI;AACA;AACA;AACA;AACA;;AAEJ;AACF;AAMM;AACJ;AACA;AACA;AACA;;;;AAMI;AACA;AACA;AACA;AACA;;AAEJ;AACF;AAMM;AACJ;AACA;AACA;AACA;;;;AAMI;AACA;AACA;AACA;AACA;;AAEJ;AACF;AAUM;AACJ;AACA;AACA;AACA;YAKA;AACA;;;;AAMI;AACA;;;AAGA;;AAEJ;AACF;AAGM;AACJ;AACA;AACA;AACA;;;;AAMI;AACA;;;AAGA;;AAEJ;AACF;AAGM;AACJ;AACA;AACA;AACA;;;;AAMI;AACA;;;AAGA;;AAEJ;AACF;;AC5LM;AACJ;AACA;AACA;AACA;;;;AAME;;;AAMM;;;;;AAKD;;AAGL;AACE;;;;;AAOF;AACE;;AAGF;AACF;AACF;AAOM;AACJ;AACA;AACA;AACA;AACA;;;;AAME;;;;;;;AAQC;AAED;AACE;AACA;;;;AAKF;AACE;;AAGF;AACF;AACF;AAOM;AACJ;AACA;AACA;;;;AAME;AAEA;AACE;AACA;;QAIF;AACE;;;AAIF;AACE;;AAGF;AACF;AACF;;;;;;;;;;QC1GE;AACE;AACA;;;;;;;;AAaI;;;QAOF;AACE;AACA;;QAMN;AACF;AAQA;;;;;AAkBI;AAEI;AAEJ;;;;AAOA,2BACA;;;AAME;;AAGN;;AC9GM;;AAEF;AACA;AAEA;AACF;AACF;;ACgBA;AAKE;AACA;;AAEA;AACA;AACA;AAEA;;YAGI;AACA;AACA;AAEA;;AAEA;;;;;;;;AAgBI;;AAEA;AAEI;;;wBAOR;;;;AAKA;AACE;AACA;;;AAGL;AACH;;;AAeA;;AAkBE;AAGI;AACA;;AAIE;AACA;AACA;AACA;;AAEJ;AAGJ;;AC/HM;;AAOF;;;;;;AAMA;AACF;AACF;;;;;;;;;;;;;ACrBW;AACT;AACE;AACA;AACA;AACD;AACF;;;;;gBC0BkB;IAER;IAEA;;;AAOA;;AACA;;AACA;;AACA;;AACA;;AACA;;YAMQ;YAOR;AAKA;;;;;;;AAUA;AACP;;;;AAIO;AACP;;;AAGO;;;;AAaP;;;AAKF;;;AAMI;;AAEI;;AACA;AACA;;AACA;;;;AAIA;;AAGE;AACF;AACE;;;AAEA;;;AAGL;;;;;;;;AA5GI;AACT;AACE;AACA;AACD;AACF;;AAkHM;AAIP;AAKA;;AAmBE;AACF;;;gBCrJmB;AACR;AACA;;;;;;;AASA;;AACA;;AACA;;AACA;;AACA;;YAMQ;;AAGjB;AAQE;AACA;;AACA;;AAEE;;;AAGF;AACE;AACI;AACA;AACA;AACA;AACD;AACH;AACF;;;;;;;AA3DO;AACT;AACE;AACA;AACD;AACF;;;AAoED;AACA;;AAOE;AACF;;;ICnFmB;;;AAKjB,IAKiB;;AACR;AAEA;;AAEP;;;;AAIO;;;;;;AAIT,IAIS;AACP;;AAEE;AACA;;;AAGF;;;AAGF,IAIiB;AAEjB;AACE;AACE;AACA;AACA;;;4DAQI;AACA;AACA;;AAEE;AACA;;;;AAGJ;;AAEF;AACF;;;;;;;;;AAKG;AAGP;AAEA;;;AA0Ba;;;;AAHF;AACT;AACD;;;AAoBC;AACF;;;;;;;ACrGmB;AACA;;;;;AAKjB,IASiB;AACA;;;;;AAIR;;AACA;;AAET;AACE;AACE;AACA;AACF;;;AAIA;AACA;;;AAGE;AAAc;;AAEhB;;AAGF;AAOE;AACA;AAEA;AACA;AACE;;AAKF;AACE;;;;AAMA;;;AAIF;AACE;;;;;;;AAOQ;AACA;;;;AAMV;AACE;;;;AAOJ;AAME;AACA;AACA;AACA;;;;AAOI;AACA;AACA;;;;;AAKF;AACA;AACA;;;;;;;;;;;AAQN;AAKA;;;AAMa;;;;AAHF;AACT;AACD;;;AASC;AACF;;;;AC7JE;AACE;AACA;;;;;;;;;;;AAcF;AACE;AACA;;;;;;;;;;;;;;;;;AC7BO;AACT;;;AAGC;AACF;;;;;;;;AAQU;AACT;;;AAGC;AACF;;;ACCD;;;;;;;AAOM;AAEN;AAOM;AAIJ;;AAGA;AACA;;;AAGA;AACA;AACE;AAAqB;;AAIjB;;AAGN;AAEA;;AAEqB;;;;AAKrB;AACE;;;;AAKF;AACF;AAuBM;AAIJ;;;;;;;;;AAQA;;AACA;;AACA;AACE;;;;AAKF;AACF;AAGM;AACJ;AACF","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms/integration/src/mappers/value/value-field.mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/checkbox/checkbox-field-mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/datepicker/datepicker-field-mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/select/options-field-mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/slider/slider-field-mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/button/non-field-logic.utils.ts","../../../../packages/dynamic-forms/integration/src/mappers/button/button-field-mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/button/array-button.utils.ts","../../../../packages/dynamic-forms/integration/src/mappers/button/array-button.mapper.ts","../../../../packages/dynamic-forms/integration/src/mappers/button/navigation-button.mapper.ts","../../../../packages/dynamic-forms/integration/src/utils/create-resolved-errors-signal.ts","../../../../packages/dynamic-forms/integration/src/utils/should-show-errors.ts","../../../../packages/dynamic-forms/integration/src/utils/setup-meta-tracking.ts","../../../../packages/dynamic-forms/integration/src/utils/create-aria-described-by.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-field-shell.directive.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-field.directive.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-action.directive.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-addons.directive.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-addon-action.directive.ts","../../../../packages/dynamic-forms/integration/src/directives/ng-forge-controls.ts","../../../../packages/dynamic-forms/integration/src/directives/host-directive-presets.ts","../../../../packages/dynamic-forms/integration/src/testing/create-field-fixture.ts"],"sourcesContent":["import { BaseValueField, FieldMeta } from '@ng-forge/dynamic-forms/internal';\nimport { computed, inject, Signal } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\nimport { buildBaseInputs, DEFAULT_PROPS } from '@ng-forge/dynamic-forms/internal';\nimport { injectFieldSignalContext } from '@ng-forge/dynamic-forms/internal';\nimport { omit } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Context for value field mapping, containing the injected context reference.\n * Used by specialized mappers to avoid duplicate injection.\n */\nexport interface ValueFieldContext {\n /**\n * Gets the field tree for a given key from the form context.\n * This is reactive for array items - the form getter is evaluated each time,\n * allowing the computed to re-run when the root form structure updates.\n */\n getFieldTree: (fieldKey: string) => FieldTree<unknown> | undefined;\n}\n\n/**\n * Creates a value field context from the injected FIELD_SIGNAL_CONTEXT.\n * Must be called within an injection context.\n *\n * @returns The value field context with reactive field tree accessor\n */\nexport function resolveValueFieldContext(): ValueFieldContext {\n const context = injectFieldSignalContext();\n\n return {\n getFieldTree: (fieldKey: string) => {\n // Access context.form here (inside a function that will be called from a computed)\n // This allows the getter to be evaluated reactively\n const formRoot = context.form as Record<string, FieldTree<unknown> | undefined> | undefined;\n\n // Handle case where form is undefined (e.g., newly added array items before root form updates)\n if (!formRoot) {\n return undefined;\n }\n\n return formRoot[fieldKey];\n },\n };\n}\n\n/**\n * Builds the base inputs for a value field.\n * This is a helper function used by valueFieldMapper and specialized mappers.\n *\n * @param fieldDef The value field definition\n * @param ctx The resolved value field context\n * @param defaultProps Default props from the form configuration (may be undefined if not configured)\n * @returns Record of input names to values\n */\nexport function buildValueFieldInputs<TProps, TValue = unknown>(\n fieldDef: BaseValueField<TProps, TValue, FieldMeta, boolean>,\n ctx: ValueFieldContext,\n defaultProps?: Record<string, unknown>,\n): Record<string, unknown> {\n const omittedFields = omit(fieldDef, ['value']);\n const baseInputs = buildBaseInputs(omittedFields, defaultProps);\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n validationMessages: fieldDef.validationMessages ?? {},\n };\n\n if (fieldDef.placeholder !== undefined) {\n inputs['placeholder'] = fieldDef.placeholder;\n }\n\n // Resolve field tree reactively - this call is inside a computed,\n // so it establishes the reactive dependency correctly\n const fieldTree = ctx.getFieldTree(fieldDef.key);\n if (fieldTree !== undefined) {\n inputs['field'] = fieldTree;\n }\n\n return inputs;\n}\n\n/**\n * Maps a value field definition to component inputs.\n *\n * @param fieldDef The value field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function valueFieldMapper<TProps = unknown, TValue = unknown>(\n fieldDef: BaseValueField<TProps, TValue, FieldMeta, boolean>,\n): Signal<Record<string, unknown>> {\n // Get the context once at injection time (captures the FIELD_SIGNAL_CONTEXT reference)\n const ctx = resolveValueFieldContext();\n const defaultProps = inject(DEFAULT_PROPS);\n\n // The computed calls ctx.getFieldTree(fieldDef.key) inside, which:\n // 1. Accesses context.form (triggering the getter for array items)\n // 2. Returns the field tree if available, or undefined if not yet\n // 3. Re-runs when the form structure updates (reactive dependency)\n return computed(() => {\n return buildValueFieldInputs(fieldDef, ctx, defaultProps());\n });\n}\n","import { BaseCheckedField, FieldMeta } from '@ng-forge/dynamic-forms/internal';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { computed, inject, Signal } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\nimport { buildBaseInputs, DEFAULT_PROPS } from '@ng-forge/dynamic-forms/internal';\nimport { injectFieldSignalContext } from '@ng-forge/dynamic-forms/internal';\nimport { omit } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Maps a checkbox/toggle field definition to component inputs.\n *\n * @param fieldDef The checkbox field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function checkboxFieldMapper(fieldDef: BaseCheckedField<unknown, FieldMeta, boolean>): Signal<Record<string, unknown>> {\n const context = injectFieldSignalContext();\n const defaultProps = inject(DEFAULT_PROPS);\n\n return computed(() => {\n const omittedFields = omit(fieldDef, ['value']) as FieldDef<unknown>;\n const baseInputs = buildBaseInputs(omittedFields, defaultProps());\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n validationMessages: fieldDef.validationMessages ?? {},\n };\n\n if (fieldDef.placeholder !== undefined) {\n inputs['placeholder'] = fieldDef.placeholder;\n }\n\n // Access form inside computed for reactivity and to handle cases where\n // form may not be immediately available (e.g., during array item initialization)\n const formRoot = context.form as Record<string, FieldTree<unknown> | undefined> | undefined;\n const fieldTree = formRoot?.[fieldDef.key];\n if (fieldTree !== undefined) {\n inputs['field'] = fieldTree;\n }\n\n return inputs;\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { DEFAULT_PROPS } from '@ng-forge/dynamic-forms/internal';\nimport { DatepickerField } from '../../definitions';\nimport { resolveValueFieldContext, buildValueFieldInputs } from '../value/value-field.mapper';\n\n/**\n * Converts a date value (string, Date, or null) to a Date object or null.\n * This ensures UI libraries that expect Date objects receive the correct type.\n */\nfunction toDate(value: Date | string | null | undefined): Date | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (value instanceof Date) {\n return value;\n }\n // Parse string to Date\n const date = new Date(value);\n return isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Maps a datepicker field to component inputs.\n *\n * @param fieldDef The datepicker field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function datepickerFieldMapper<TProps>(fieldDef: DatepickerField<TProps>): Signal<Record<string, unknown>> {\n const ctx = resolveValueFieldContext();\n const defaultProps = inject(DEFAULT_PROPS);\n\n return computed(() => {\n const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());\n\n // Add datepicker-specific properties, converting strings to Date objects\n if (fieldDef.minDate !== undefined) {\n inputs['minDate'] = toDate(fieldDef.minDate);\n }\n if (fieldDef.maxDate !== undefined) {\n inputs['maxDate'] = toDate(fieldDef.maxDate);\n }\n if (fieldDef.startAt !== undefined) {\n inputs['startAt'] = toDate(fieldDef.startAt);\n }\n\n return inputs;\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { SelectField } from '../../definitions';\nimport { RadioField } from '../../definitions';\nimport { MultiCheckboxField } from '../../definitions';\nimport { BaseValueField, DEFAULT_PROPS, FieldMeta } from '@ng-forge/dynamic-forms/internal';\nimport { resolveValueFieldContext, buildValueFieldInputs } from '../value/value-field.mapper';\n\n/** Field types that have an options property. */\nexport type FieldWithOptions<T = unknown, TProps = unknown> =\n | SelectField<T, TProps>\n | RadioField<T, TProps>\n | MultiCheckboxField<T, TProps>;\n\n/**\n * Maps a field with options (select, radio, multi-checkbox) to component inputs.\n *\n * @param fieldDef The field definition with options\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function optionsFieldMapper<T, TProps>(fieldDef: FieldWithOptions<T, TProps>): Signal<Record<string, unknown>> {\n const ctx = resolveValueFieldContext();\n const defaultProps = inject(DEFAULT_PROPS);\n\n return computed(() => {\n // Cast to BaseValueField - safe because all FieldWithOptions types extend BaseValueField\n const inputs = buildValueFieldInputs(fieldDef as BaseValueField<TProps, unknown, FieldMeta, boolean>, ctx, defaultProps());\n\n // Add options property\n inputs['options'] = fieldDef.options;\n\n return inputs;\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { DEFAULT_PROPS } from '@ng-forge/dynamic-forms/internal';\nimport { SliderField } from '../../definitions';\nimport { resolveValueFieldContext, buildValueFieldInputs } from '../value/value-field.mapper';\n\n/**\n * Maps a slider field to component inputs.\n *\n * @param fieldDef The slider field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function sliderFieldMapper<TProps>(fieldDef: SliderField<TProps>): Signal<Record<string, unknown>> {\n const ctx = resolveValueFieldContext();\n const defaultProps = inject(DEFAULT_PROPS);\n\n return computed(() => {\n const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());\n\n // Add slider-specific properties (these are at field level, not in props)\n if (fieldDef.minValue !== undefined) {\n inputs['minValue'] = fieldDef.minValue;\n }\n if (fieldDef.maxValue !== undefined) {\n inputs['maxValue'] = fieldDef.maxValue;\n }\n if (fieldDef.step !== undefined) {\n inputs['step'] = fieldDef.step;\n }\n\n return inputs;\n });\n}\n","import { inject } from '@angular/core';\nimport {\n EvaluationContext,\n injectNonFieldEvaluationContext,\n LogicConfig,\n NonFieldLogicConfig,\n resolveNonFieldDisabled,\n resolveNonFieldHidden,\n RootFormRegistryService,\n} from '@ng-forge/dynamic-forms/internal';\nimport { FieldTree } from '@angular/forms/signals';\n\n// Single source lives in @ng-forge/dynamic-forms/internal; re-exported so the button\n// mappers keep importing it from this module.\nexport { injectNonFieldEvaluationContext };\n\n/**\n * Field definition with optional hidden/disabled and logic properties.\n * This is the minimal interface needed by applyNonFieldLogic.\n */\nexport interface FieldDefWithLogic {\n key?: string;\n hidden?: boolean;\n disabled?: boolean;\n logic?: NonFieldLogicConfig[] | LogicConfig[];\n}\n\n/**\n * Result of applying non-field logic to a field.\n * Only includes properties that have values to apply.\n */\nexport interface NonFieldLogicResult {\n hidden?: boolean;\n disabled?: boolean;\n}\n\n/**\n * Applies hidden and disabled logic to a non-form-bound field.\n * Must be called inside a computed() context (reads form signals).\n */\nexport function applyNonFieldLogic(\n rootFormRegistry: RootFormRegistryService,\n fieldDef: FieldDefWithLogic,\n evaluationContext?: () => EvaluationContext,\n): NonFieldLogicResult {\n const result: NonFieldLogicResult = {};\n const rootForm = rootFormRegistry.rootForm();\n\n if (rootForm) {\n if (fieldDef.hidden !== undefined || fieldDef.logic?.some((l) => l.type === 'hidden')) {\n const hiddenSignal = resolveNonFieldHidden({\n form: rootForm,\n fieldLogic: fieldDef.logic,\n explicitValue: fieldDef.hidden,\n evaluationContext,\n });\n result.hidden = hiddenSignal();\n }\n\n // Resolve disabled state if explicitly set or has logic\n if (fieldDef.disabled !== undefined || fieldDef.logic?.some((l) => l.type === 'disabled')) {\n // Create the signal and READ IT to establish reactive dependencies\n const disabledSignal = resolveNonFieldDisabled({\n form: rootForm,\n fieldLogic: fieldDef.logic,\n explicitValue: fieldDef.disabled,\n evaluationContext,\n });\n result.disabled = disabledSignal();\n }\n } else {\n // Fallback to static values when rootForm is not available\n if (fieldDef.hidden !== undefined) {\n result.hidden = fieldDef.hidden;\n }\n if (fieldDef.disabled !== undefined) {\n result.disabled = fieldDef.disabled;\n }\n }\n\n return result;\n}\n\n/**\n * Applies hidden logic to a non-form-bound field.\n *\n * @param rootForm The root form FieldTree (can be undefined)\n * @param fieldDef The field definition with optional hidden/logic\n * @returns The resolved hidden boolean value, or undefined if no hidden logic\n */\nexport function resolveHiddenValue(\n rootForm: FieldTree<unknown, string | number> | undefined,\n fieldDef: FieldDefWithLogic,\n evaluationContext?: () => EvaluationContext,\n): boolean | undefined {\n if (!rootForm) {\n return fieldDef.hidden;\n }\n\n if (fieldDef.hidden !== undefined || fieldDef.logic?.some((l) => l.type === 'hidden')) {\n return resolveNonFieldHidden({\n form: rootForm,\n fieldLogic: fieldDef.logic,\n explicitValue: fieldDef.hidden,\n evaluationContext,\n // formValue omitted intentionally - resolver will read form.value() reactively\n })();\n }\n\n return undefined;\n}\n\n/**\n * Injects everything a non-form-bound button mapper needs to evaluate hidden/disabled logic,\n * returning a thunk to invoke inside the mapper's computed. Used by adapter button mappers so a\n * generic `{ type: 'button', logic: [...] }` evaluates its conditions with `externalData` and\n * custom functions in scope, matching the built-in button field types.\n */\nexport function injectNonFieldLogicResolver(fieldDef: FieldDefWithLogic): () => NonFieldLogicResult {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n return () => applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext);\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { FormEvent } from '@ng-forge/dynamic-forms';\nimport { buildBaseInputs, DEFAULT_PROPS } from '@ng-forge/dynamic-forms/internal';\nimport { ButtonField } from '../../definitions';\nimport { injectNonFieldLogicResolver } from './non-field-logic.utils';\n\n/**\n * Maps a button field to component inputs.\n *\n * @param fieldDef The button field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function buttonFieldMapper<TProps, TEvent extends FormEvent>(\n fieldDef: ButtonField<TProps, TEvent>,\n): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const resolveLogic = injectNonFieldLogicResolver(fieldDef);\n\n return computed(() => {\n const inputs = buildBaseInputs(fieldDef, defaultProps());\n inputs['event'] = fieldDef.event;\n\n if (fieldDef.eventArgs !== undefined) {\n inputs['eventArgs'] = fieldDef.eventArgs;\n }\n\n return {\n ...inputs,\n ...resolveLogic(),\n };\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms';\nimport { ARRAY_CONTEXT, ArrayAllowedChildren } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Context for array button mapping, containing resolved array information.\n * Used by array button mappers to build event context.\n */\nexport interface ArrayButtonContext {\n /** The target array key (from explicit arrayKey or ARRAY_CONTEXT) */\n targetArrayKey: string | undefined;\n /** Computed signal that returns the current array index (-1 if not inside array) */\n indexSignal: Signal<number>;\n /** The current form value for token resolution */\n formValue: Record<string, unknown>;\n /** Whether this button is inside an array context */\n isInsideArray: boolean;\n}\n\n/** Event context passed to button components for array operations. */\nexport interface ArrayButtonEventContext {\n /** The button field key */\n key: string;\n /** The current array index (-1 if outside array) */\n index: number;\n /** The target array key */\n arrayKey: string;\n /** The form value for token resolution */\n formValue: Record<string, unknown>;\n /**\n * Optional template for add operations.\n * Can be a single field (primitive item) or array of fields (object item).\n */\n template?: ArrayAllowedChildren | readonly ArrayAllowedChildren[];\n}\n\n/**\n * Resolves array button context for button mappers.\n *\n * @param fieldKey The button field key (for logging)\n * @param buttonType The button type name (for logging)\n * @param explicitArrayKey Optional explicit arrayKey from field definition\n * @returns The array button context with reactive index signal\n */\nexport function resolveArrayButtonContext(fieldKey: string, buttonType: string, explicitArrayKey?: string): ArrayButtonContext {\n const arrayContext = inject(ARRAY_CONTEXT, { optional: true });\n const logger = inject(DynamicFormLogger);\n\n // Priority: explicit arrayKey from fieldDef > arrayKey from context\n const targetArrayKey = explicitArrayKey ?? arrayContext?.arrayKey;\n\n if (!targetArrayKey) {\n logger.warn(\n `${buttonType} button \"${fieldKey}\" has no array context. ` +\n 'Either place it inside an array field, or provide an explicit arrayKey property.',\n );\n }\n\n // Create index signal once at injection time - computed to track arrayContext.index changes\n const indexSignal = computed(() => {\n if (!arrayContext) return -1;\n return arrayContext.index();\n });\n\n return {\n targetArrayKey,\n indexSignal,\n formValue: (arrayContext?.formValue ?? {}) as Record<string, unknown>,\n isInsideArray: arrayContext !== null,\n };\n}\n\n/**\n * Builds the event context passed to the button component.\n *\n * @param fieldKey The button field key\n * @param ctx The resolved array button context\n * @param template Optional template for add operations (single field or array of fields)\n * @returns The event context for the button component\n */\nexport function buildArrayButtonEventContext(\n fieldKey: string,\n ctx: ArrayButtonContext,\n template?: ArrayAllowedChildren | readonly ArrayAllowedChildren[],\n): ArrayButtonEventContext {\n const eventContext: ArrayButtonEventContext = {\n key: fieldKey,\n index: ctx.indexSignal(), // Evaluated inside computed\n arrayKey: ctx.targetArrayKey ?? '',\n formValue: ctx.formValue,\n };\n\n if (template) {\n eventContext.template = template;\n }\n\n return eventContext;\n}\n\n/**\n * Event argument type - supports static values and tokens.\n * Tokens: $key, $index, $arrayKey, $template, formValue\n */\nexport type EventArg = string | number | boolean | null | undefined;\n\n/**\n * Resolves eventArgs with defaults for array buttons.\n *\n * @param fieldEventArgs The eventArgs from field definition (may be undefined)\n * @param defaultEventArgs The default eventArgs to use if not specified\n * @returns The resolved eventArgs array\n */\nexport function resolveArrayButtonEventArgs(\n fieldEventArgs: readonly EventArg[] | undefined,\n defaultEventArgs: readonly EventArg[] = ['$arrayKey', '$template'],\n): readonly EventArg[] {\n return fieldEventArgs ?? defaultEventArgs;\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport {\n AppendArrayItemEvent,\n InsertArrayItemEvent,\n PopArrayItemEvent,\n PrependArrayItemEvent,\n RemoveAtIndexEvent,\n ShiftArrayItemEvent,\n} from '@ng-forge/dynamic-forms';\nimport {\n ArrayAllowedChildren,\n buildBaseInputs,\n DEFAULT_PROPS,\n NonFieldLogicConfig,\n RootFormRegistryService,\n} from '@ng-forge/dynamic-forms/internal';\nimport { buildArrayButtonEventContext, EventArg, resolveArrayButtonContext, resolveArrayButtonEventArgs } from './array-button.utils';\nimport { applyNonFieldLogic, injectNonFieldEvaluationContext } from './non-field-logic.utils';\n\n// =============================================================================\n// Base Interfaces\n// =============================================================================\n\n/**\n * Base interface for array add button fields (append, prepend, insert).\n * Template is required to define the structure for new items.\n */\nexport interface BaseArrayAddButtonField<TProps = unknown> {\n key: string;\n type: string;\n label?: string;\n disabled?: boolean;\n hidden?: boolean;\n className?: string;\n props?: TProps;\n /** Target array key (required when outside array, optional inside) */\n arrayKey?: string;\n /**\n * Template defining the field structure for new items. REQUIRED.\n * - Single field (ArrayAllowedChildren): Creates a primitive item (field's value is extracted directly)\n * - Array of fields (ArrayAllowedChildren[]): Creates an object item (fields merged into object)\n */\n template: ArrayAllowedChildren | readonly ArrayAllowedChildren[];\n /** Optional custom eventArgs */\n eventArgs?: readonly EventArg[];\n /** Logic rules for dynamic hidden/disabled state (only hidden and disabled are supported) */\n logic?: NonFieldLogicConfig[];\n}\n\n/**\n * Base interface for array remove button fields (remove, pop, shift).\n * No template needed - removes existing items.\n */\nexport interface BaseArrayRemoveButtonField<TProps = unknown> {\n key: string;\n type: string;\n label?: string;\n disabled?: boolean;\n hidden?: boolean;\n className?: string;\n props?: TProps;\n /** Target array key (required when outside array, optional inside) */\n arrayKey?: string;\n /** Optional custom eventArgs */\n eventArgs?: readonly EventArg[];\n /** Logic rules for dynamic hidden/disabled state (only hidden and disabled are supported) */\n logic?: NonFieldLogicConfig[];\n}\n\n/**\n * Interface for insert array item button fields.\n * Extends add button with required index.\n */\nexport interface BaseInsertArrayItemButtonField<TProps = unknown> extends BaseArrayAddButtonField<TProps> {\n /** The index at which to insert the new item */\n index: number;\n}\n\n// =============================================================================\n// Add Array Item Mappers\n// =============================================================================\n\n/**\n * Mapper for add array item button — appends new item to end of array.\n * Template is REQUIRED to define the field structure for new items.\n */\nexport function addArrayItemButtonMapper<TProps>(fieldDef: BaseArrayAddButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'addArrayItem', fieldDef.arrayKey);\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: AppendArrayItemEvent,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', '$template']),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n\n/**\n * Mapper for prepend array item button — inserts new item at beginning of array.\n * Template is REQUIRED to define the field structure for new items.\n */\nexport function prependArrayItemButtonMapper<TProps>(fieldDef: BaseArrayAddButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'prependArrayItem', fieldDef.arrayKey);\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: PrependArrayItemEvent,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', '$template']),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n\n/**\n * Mapper for insert array item button — inserts new item at specific index.\n * Template and index are REQUIRED.\n */\nexport function insertArrayItemButtonMapper<TProps>(fieldDef: BaseInsertArrayItemButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'insertArrayItem', fieldDef.arrayKey);\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: InsertArrayItemEvent,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey', fieldDef.index, '$template']),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx, fieldDef.template),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n\n// =============================================================================\n// Remove Array Item Mappers\n// =============================================================================\n\n/**\n * Mapper for remove array item button.\n * Inside array: RemoveAtIndexEvent. Outside array: PopArrayItemEvent.\n */\nexport function removeArrayItemButtonMapper<TProps>(fieldDef: BaseArrayRemoveButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'removeArrayItem', fieldDef.arrayKey);\n\n // Choose event type based on context:\n // - Inside array: RemoveAtIndexEvent (remove specific item)\n // - Outside array: PopArrayItemEvent (remove last item)\n const eventType = ctx.isInsideArray ? RemoveAtIndexEvent : PopArrayItemEvent;\n const defaultEventArgs = ctx.isInsideArray ? ['$arrayKey', '$index'] : ['$arrayKey'];\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: eventType,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, defaultEventArgs),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n\n/** Mapper for pop array item button — removes the last item from array. */\nexport function popArrayItemButtonMapper<TProps>(fieldDef: BaseArrayRemoveButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'popArrayItem', fieldDef.arrayKey);\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: PopArrayItemEvent,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey']),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n\n/** Mapper for shift array item button — removes the first item from array. */\nexport function shiftArrayItemButtonMapper<TProps>(fieldDef: BaseArrayRemoveButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n const ctx = resolveArrayButtonContext(fieldDef.key, 'shiftArrayItem', fieldDef.arrayKey);\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps?.());\n\n return {\n ...baseInputs,\n event: ShiftArrayItemEvent,\n eventArgs: resolveArrayButtonEventArgs(fieldDef.eventArgs, ['$arrayKey']),\n eventContext: buildArrayButtonEventContext(fieldDef.key, ctx),\n ...applyNonFieldLogic(rootFormRegistry, fieldDef, evaluationContext),\n };\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { FieldWithValidation, FormSubmitEvent, NextPageEvent, PreviousPageEvent } from '@ng-forge/dynamic-forms';\nimport {\n buildBaseInputs,\n DEFAULT_PROPS,\n FieldDef,\n injectFieldSignalContext,\n FORM_OPTIONS,\n resolveNextButtonDisabled,\n resolveSubmitButtonDisabled,\n RootFormRegistryService,\n} from '@ng-forge/dynamic-forms/internal';\nimport { ButtonField } from '../../definitions';\nimport { injectNonFieldEvaluationContext, resolveHiddenValue } from './non-field-logic.utils';\n\n// =============================================================================\n// Base Interfaces\n// =============================================================================\n\n/** Base interface for navigation button fields (submit, next, previous). */\nexport type BaseNavigationButtonField<TProps = unknown> = ButtonField<TProps, FormSubmitEvent | NextPageEvent | PreviousPageEvent> & {\n type: string;\n /** Logic rules for dynamic disabled state */\n logic?: FieldWithValidation['logic'];\n};\n\n// =============================================================================\n// Submit Button Mapper\n// =============================================================================\n\n/**\n * Mapper for submit button — configures native form submission via type=\"submit\".\n * Triggers native form submission rather than dispatching events directly.\n */\nexport function submitButtonFieldMapper<TProps>(fieldDef: BaseNavigationButtonField<TProps>): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const defaultProps = inject(DEFAULT_PROPS);\n const formOptions = inject(FORM_OPTIONS);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n\n const fieldWithLogic = fieldDef as FieldDef<Record<string, unknown>> & Partial<FieldWithValidation>;\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n const rootForm = rootFormRegistry.rootForm();\n\n // Use rootForm (not fieldSignalContext.form) — submit needs root form validity (#157).\n // Before the root form is available, fall back to the explicit disabled value.\n const disabled = rootForm\n ? resolveSubmitButtonDisabled({\n form: rootForm,\n formOptions: formOptions(),\n fieldLogic: fieldWithLogic.logic,\n explicitlyDisabled: fieldDef.disabled,\n evaluationContext,\n })()\n : (fieldDef.disabled ?? false);\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n // type=\"submit\" triggers native form submission (no event dispatch needed)\n props: { ...(fieldDef.props as Record<string, unknown>), type: 'submit' },\n disabled,\n };\n\n const hidden = resolveHiddenValue(rootForm, fieldWithLogic, evaluationContext);\n if (hidden !== undefined) {\n inputs['hidden'] = hidden;\n }\n\n return inputs;\n });\n}\n\n// =============================================================================\n// Next Page Button Mapper\n// =============================================================================\n\n/** Mapper for next page button — preconfigures NextPageEvent. */\nexport function nextButtonFieldMapper<TProps>(fieldDef: BaseNavigationButtonField<TProps>): Signal<Record<string, unknown>> {\n const fieldSignalContext = injectFieldSignalContext();\n const rootFormRegistry = inject(RootFormRegistryService);\n const defaultProps = inject(DEFAULT_PROPS);\n const formOptions = inject(FORM_OPTIONS);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n\n const fieldWithLogic = fieldDef as FieldDef<Record<string, unknown>> & Partial<FieldWithValidation>;\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n const rootForm = rootFormRegistry.rootForm();\n\n const disabled = resolveNextButtonDisabled({\n form: fieldSignalContext.form,\n formOptions: formOptions(),\n fieldLogic: fieldWithLogic.logic,\n currentPageValid: fieldSignalContext.currentPageValid,\n evaluationContext,\n })();\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n event: NextPageEvent,\n disabled,\n };\n\n const hidden = resolveHiddenValue(rootForm, fieldWithLogic, evaluationContext);\n if (hidden !== undefined) {\n inputs['hidden'] = hidden;\n }\n\n return inputs;\n });\n}\n\n// =============================================================================\n// Previous Page Button Mapper\n// =============================================================================\n\n/** Mapper for previous page button — preconfigures PreviousPageEvent. */\nexport function previousButtonFieldMapper<TProps>(fieldDef: BaseNavigationButtonField<TProps>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef);\n\n const fieldWithLogic = fieldDef as FieldDef<Record<string, unknown>> & Partial<FieldWithValidation>;\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n const rootForm = rootFormRegistry.rootForm();\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n event: PreviousPageEvent,\n };\n\n // Add disabled binding only if explicitly set by user\n if (fieldDef.disabled !== undefined) {\n inputs['disabled'] = fieldDef.disabled;\n }\n\n const hidden = resolveHiddenValue(rootForm, fieldWithLogic, evaluationContext);\n if (hidden !== undefined) {\n inputs['hidden'] = hidden;\n }\n\n return inputs;\n });\n}\n","import { computed, inject, Injector, signal, Signal } from '@angular/core';\nimport { toObservable, toSignal } from '@angular/core/rxjs-interop';\nimport { FieldTree, ValidationError } from '@angular/forms/signals';\nimport { combineLatest, Observable, of } from 'rxjs';\nimport { map, switchMap } from 'rxjs/operators';\nimport { ValidationMessages } from '@ng-forge/dynamic-forms/internal';\nimport { dynamicTextToObservable } from '@ng-forge/dynamic-forms/internal';\nimport { interpolateParams } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms';\nimport type { Logger } from '@ng-forge/dynamic-forms';\n\n/** Resolved validation error with interpolated message */\nexport interface ResolvedError {\n kind: string;\n message: string;\n}\n\n/**\n * Factory function that creates a signal of resolved validation errors\n * Handles async resolution of DynamicText validation messages\n *\n * @param field - Signal containing FieldTree\n * @param validationMessages - Signal containing custom field-level validation messages\n * @param defaultValidationMessages - Signal containing default validation messages (fallback)\n * @param injector - Optional injector for DynamicText resolution\n * @returns Signal<ResolvedError[]> - Reactively resolved error messages\n */\nexport function createResolvedErrorsSignal<T>(\n field: Signal<FieldTree<T>>,\n validationMessages: Signal<ValidationMessages | undefined>,\n defaultValidationMessages: Signal<ValidationMessages | undefined> = signal(undefined),\n injector = inject(Injector),\n): Signal<ResolvedError[]> {\n // Get logger from injector\n const logger = injector.get(DynamicFormLogger);\n\n // Ensure validationMessages is never undefined (mappers pass {} if not defined)\n const messages = computed(() => validationMessages() ?? {});\n const defaultMessages = computed(() => defaultValidationMessages() ?? {});\n\n // Create a computed signal that reads the actual errors from the field\n // This ensures the signal tracks changes to field().errors(), not just the field reference\n const errors = computed(() => {\n const control = field()();\n return control.errors();\n });\n\n // Convert signals to observables using toObservable with injector\n const errors$ = toObservable(errors, { injector });\n const messages$ = toObservable(messages, { injector });\n const defaultMessages$ = toObservable(defaultMessages, { injector });\n\n // Combine observables and process errors\n const resolvedErrors$ = combineLatest([errors$, messages$, defaultMessages$]).pipe(\n switchMap(([currentErrors, msgs, defaultMsgs]) => {\n // No errors - return empty array\n if (!currentErrors || currentErrors.length === 0) {\n return of([]);\n }\n\n // Create observable for each error's resolved message\n const errorResolvers = currentErrors.map((error: ValidationError) => resolveErrorMessage(error, msgs, defaultMsgs, injector, logger));\n\n // Combine all error message observables into single array emission, filtering out nulls\n return errorResolvers.length > 0\n ? combineLatest(errorResolvers).pipe(map((errors) => errors.filter((e): e is ResolvedError => e !== null)))\n : of([]);\n }),\n );\n\n // Convert observable back to signal using toSignal with injector\n // toSignal properly handles the injection context and manages subscriptions\n return toSignal(resolvedErrors$, { initialValue: [] as ResolvedError[], injector });\n}\n\n/**\n * Resolves a single error message from DynamicText sources with fallback logic\n * Priority: field-level message → default message → error.message → no message (logs warning)\n *\n * @internal\n */\nfunction resolveErrorMessage(\n error: ValidationError,\n fieldMessages: ValidationMessages,\n defaultMessages: ValidationMessages,\n injector: Injector,\n logger: Logger,\n): Observable<ResolvedError | null> {\n // Check for field-level custom message first\n const fieldMessage = fieldMessages[error.kind];\n\n // Fall back to default message if no field-level message exists\n const defaultMessage = defaultMessages[error.kind];\n\n // Determine which message to use: field-level → default → error.message (from schema validators)\n const messageToUse = fieldMessage ?? defaultMessage ?? error.message;\n\n // If no message found, log warning and return null (will be filtered out)\n if (!messageToUse) {\n logger.warn(\n `No validation message found for error kind \"${error.kind}\". ` +\n `Please provide a message via field-level validationMessages or form-level defaultValidationMessages.`,\n );\n return of(null);\n }\n\n // If using error.message directly (from schema validators), wrap in of() instead of dynamicTextToObservable\n // error.message is always a plain string, not DynamicText\n const isSchemaMessage = fieldMessage === undefined && defaultMessage === undefined;\n const messageObservable = isSchemaMessage\n ? of(messageToUse as string) // error.message is always string\n : dynamicTextToObservable(messageToUse, injector);\n\n // Apply parameter interpolation to support {{param}} syntax\n return messageObservable.pipe(\n map((msg) => ({\n kind: error.kind,\n message: interpolateParams(msg, error),\n })),\n );\n}\n","import { Signal, computed } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\n\n/**\n * Computed signal that determines if errors should be displayed.\n * Based on field's invalid, touched, and error count.\n *\n * @param field - Signal containing FieldTree\n * @returns Signal<boolean> - True if errors should be displayed\n */\nexport function shouldShowErrors<T>(field: Signal<FieldTree<T>>): Signal<boolean> {\n return computed(() => {\n const fieldTree = field();\n const control = fieldTree();\n\n return control.invalid() && control.touched() && control.errors().length > 0;\n });\n}\n","import { afterRenderEffect, ElementRef, isDevMode, Signal } from '@angular/core';\nimport { applyMetaToElement, FieldMeta, isEqual } from '@ng-forge/dynamic-forms/internal';\n\n/** Configuration options for setupMetaTracking. */\nexport interface MetaTrackingOptions {\n /**\n * CSS selector for target elements within the host.\n * If not provided, meta attributes are applied directly to the host element.\n */\n selector?: string;\n\n /**\n * Additional signals to track that should trigger re-application of meta.\n * Useful for components where DOM elements are dynamically created (e.g., radio options).\n */\n dependents?: Signal<unknown>[];\n}\n\n// Shared options: selector may be string or accessor; computeAttrs is the\n// merged attribute record (meta + optional parent aria).\ninterface ApplyAttrsOptions {\n readonly selector?: string | (() => string | undefined);\n readonly dependents?: readonly Signal<unknown>[];\n}\n\n/**\n * Resolve target set, compute combined attrs, apply via applyMetaToElement.\n * Target resolution is cached by selector identity — re-resolves only on\n * selector change or `dependents` change. afterRenderEffect is signal-tracked,\n * so the callback re-runs only when computeAttrs / dependents differ.\n *\n * @internal\n */\nfunction applyTrackedAttrsToTargets(\n elementRef: ElementRef<HTMLElement>,\n computeAttrs: () => FieldMeta | undefined,\n options: ApplyAttrsOptions = {},\n): void {\n const appliedByElement = new WeakMap<Element, Set<string>>();\n let cachedSelector: string | undefined;\n let cachedTargets: Element[] = [];\n let previousAttrs: FieldMeta | undefined;\n let previousDeps: readonly unknown[] | undefined;\n const warnedSelectors = isDevMode() ? new Set<string>() : undefined;\n\n afterRenderEffect({\n write: () => {\n // Read dependents first so afterRenderEffect tracks them.\n const currentDeps = options.dependents?.map((d) => d());\n const attrs = computeAttrs();\n const selector = (typeof options.selector === 'function' ? options.selector() : options.selector)?.trim() || undefined;\n\n const selectorChanged = selector !== cachedSelector;\n const prevDeps = previousDeps;\n const depsChanged =\n !!currentDeps && (!prevDeps || currentDeps.length !== prevDeps.length || currentDeps.some((d, i) => d !== prevDeps[i]));\n\n if (selectorChanged || depsChanged || cachedTargets.length === 0) {\n cachedSelector = selector;\n cachedTargets = selector ? Array.from(elementRef.nativeElement.querySelectorAll(selector)) : [elementRef.nativeElement];\n }\n\n previousDeps = currentDeps;\n\n // Dev-mode safety net: selector typo or wrong descendant query leaves\n // meta + aria unapplied. Warn the first time we'd apply non-empty attrs\n // to zero matches for a given selector. Re-warns only when the selector\n // string changes (so authors fixing one typo see the next).\n if (warnedSelectors && selector && cachedTargets.length === 0 && attrs && Object.keys(attrs).length > 0) {\n if (!warnedSelectors.has(selector)) {\n warnedSelectors.add(selector);\n const tag = elementRef.nativeElement.tagName.toLowerCase();\n console.warn(\n `[Dynamic Forms] ngForgeControl selector \"${selector}\" matched no descendants of <${tag}> — meta + aria attributes will not be applied. ` +\n `Check the selector for typos, or supply 'dependents' if the target is rendered after a signal emits.`,\n );\n }\n }\n\n // Bail when attrs unchanged AND nothing structural moved — keeps repeated\n // effect runs (driven by an unrelated tracked-signal flicker) cheap.\n if (!selectorChanged && !depsChanged && isEqual(attrs, previousAttrs)) {\n return;\n }\n previousAttrs = attrs;\n\n for (const el of cachedTargets) {\n const applied = appliedByElement.get(el) ?? new Set<string>();\n appliedByElement.set(el, applyMetaToElement(el as HTMLElement, attrs, applied));\n }\n },\n });\n}\n\n/**\n * Sets up automatic meta attribute tracking and application for wrapped components.\n *\n * @param elementRef - Reference to the host element\n * @param meta - Signal containing meta attributes to apply\n * @param options - Configuration options\n */\nexport function setupMetaTracking(\n elementRef: ElementRef<HTMLElement>,\n meta: Signal<FieldMeta | undefined>,\n options?: MetaTrackingOptions,\n): void {\n applyTrackedAttrsToTargets(elementRef, () => meta(), { selector: options?.selector, dependents: options?.dependents });\n}\n\n/**\n * Marker-directive variant: writes parent meta + aria to resolved targets.\n * Parent aria wins on key collision. Internal to NgForgeControl/HostControl.\n *\n * @internal\n */\nexport function setupMarkerAttrTracking(\n elementRef: ElementRef<HTMLElement>,\n parent: {\n readonly meta: Signal<FieldMeta | undefined>;\n readonly ariaInvalid: Signal<boolean>;\n readonly ariaRequired: Signal<true | null>;\n readonly ariaDescribedBy: Signal<string | null>;\n },\n selector: () => string | undefined,\n): void {\n applyTrackedAttrsToTargets(\n elementRef,\n () => {\n const ariaRequired = parent.ariaRequired();\n const ariaDescribedBy = parent.ariaDescribedBy();\n // Skip null aria-required/describedby so applyMetaToElement removes the\n // attr (rather than serializing \"null\") on transitions.\n return {\n ...(parent.meta() ?? {}),\n 'aria-invalid': parent.ariaInvalid(),\n ...(ariaRequired === null ? {} : { 'aria-required': ariaRequired }),\n ...(ariaDescribedBy === null ? {} : { 'aria-describedby': ariaDescribedBy }),\n };\n },\n { selector },\n );\n}\n","import { computed, Signal } from '@angular/core';\nimport { ResolvedError } from './create-resolved-errors-signal';\n\n/**\n * Creates a signal that computes the aria-describedby value based on errors and hint state.\n * Errors take precedence over hints - when errors are displayed, the hint is hidden.\n * Only the first error is displayed (single error ID, not indexed).\n *\n * @param errorsToDisplay Signal containing the array of errors currently being displayed\n * @param errorId Signal containing the ID for the error element (single error only)\n * @param hintId Signal containing the ID for the hint element\n * @param hasHint Function that returns true if a hint is configured\n * @returns Signal containing the aria-describedby value or null\n */\nexport function createAriaDescribedBySignal(\n errorsToDisplay: Signal<ResolvedError[]>,\n errorId: Signal<string>,\n hintId: Signal<string>,\n hasHint: () => boolean,\n): Signal<string | null> {\n return computed(() => {\n if (errorsToDisplay().length > 0) {\n return errorId();\n }\n if (hasHint()) {\n return hintId();\n }\n return null;\n });\n}\n","import { Directive, input } from '@angular/core';\n\n/**\n * Base directive composed into every ng-forge component via `hostDirectives`.\n * Owns the universal identity inputs (`key`, `className`) and the host\n * bindings that every field shape — value-bearing, action, future container —\n * shares: `[id]`, `[attr.data-testid]`, `[class]`.\n */\n@Directive({\n host: {\n '[id]': 'key()',\n '[attr.data-testid]': 'key()',\n '[class]': 'className()',\n },\n})\nexport class NgForgeFieldShell {\n readonly key = input.required<string>();\n readonly className = input<string>('');\n}\n\n/** Input names forwarded onto `NgForgeFieldShell` via `hostDirectives`. */\nexport const NG_FORGE_FIELD_SHELL_INPUTS = ['key', 'className'] as const;\n","import { afterRenderEffect, computed, Directive, inject, input, isDevMode, Signal, signal, untracked } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms';\nimport { DEFAULT_VALIDATION_MESSAGES, DynamicText, FieldMeta, ValidationMessages } from '@ng-forge/dynamic-forms/internal';\nimport { createAriaDescribedBySignal } from '../utils/create-aria-described-by';\nimport { createResolvedErrorsSignal, ResolvedError } from '../utils/create-resolved-errors-signal';\nimport { shouldShowErrors } from '../utils/should-show-errors';\nimport type { AssertTupleLockstep } from './assert-input-lockstep';\nimport { NgForgeFieldShell } from './ng-forge-field-shell.directive';\n\n/**\n * Value-bearing add-on directive — composed alongside `NgForgeFieldShell` via\n * the `NG_FORGE_FIELD` preset. Owns the value/validation/aria plumbing every\n * value-bearing field needs.\n *\n * @if (ngf.errorsToDisplay()[0]; as e) {\n * <span [id]=\"ngf.errorId()\">{{ e.message }}</span>\n * }\n * `,\n * })\n * export class MyInputField {\n * protected readonly ngf = injectNgForgeField<string>();\n * }\n * ```\n *\n * Selectorless — usage is exclusively via `hostDirectives`. Prefer the\n * `NG_FORGE_FIELD` preset over composing this directive manually.\n */\n@Directive({\n host: {\n '[attr.hidden]': 'field()().hidden() || null',\n '[attr.aria-disabled]': 'field()().disabled() || null',\n },\n})\nexport class NgForgeField {\n // ───────────────────────────────────────────────────────────────────────────\n // Shell-owned identity — re-exported so component templates keep using\n // `ngf.key()` / `ngf.className()` without reaching for the Shell directly.\n // ───────────────────────────────────────────────────────────────────────────\n\n private readonly shell = inject(NgForgeFieldShell);\n /** Shell-owned key, re-exported as plain `Signal<string>` so it doesn't get flagged by the host-directive input-lockstep assertion. */\n readonly key: Signal<string> = this.shell.key;\n /** Shell-owned className, re-exported as plain `Signal<string>` for the same reason. */\n readonly className: Signal<string> = this.shell.className;\n\n // ───────────────────────────────────────────────────────────────────────────\n // Forwarded inputs — the value-field-specific subset of mapper output.\n // ───────────────────────────────────────────────────────────────────────────\n\n readonly field = input.required<FieldTree<unknown>>();\n readonly label = input<DynamicText>();\n readonly placeholder = input<DynamicText>();\n readonly tabIndex = input<number>();\n readonly props = input<unknown>();\n readonly meta = input<FieldMeta>();\n readonly validationMessages = input<ValidationMessages>();\n\n // ───────────────────────────────────────────────────────────────────────────\n // Wiring\n // ───────────────────────────────────────────────────────────────────────────\n\n private readonly defaultValidationMessages: Signal<ValidationMessages | undefined> =\n inject(DEFAULT_VALIDATION_MESSAGES, { optional: true }) ?? signal(undefined);\n\n // ───────────────────────────────────────────────────────────────────────────\n // Error display\n // ───────────────────────────────────────────────────────────────────────────\n\n readonly errors: Signal<ResolvedError[]> = createResolvedErrorsSignal(\n this.field,\n this.validationMessages,\n this.defaultValidationMessages,\n );\n readonly showErrors: Signal<boolean> = shouldShowErrors(this.field);\n readonly errorsToDisplay: Signal<ResolvedError[]> = computed(() => (this.showErrors() ? this.errors() : []));\n\n // ───────────────────────────────────────────────────────────────────────────\n // Accessibility helpers\n // ───────────────────────────────────────────────────────────────────────────\n\n readonly errorId: Signal<string> = computed(() => `${this.key()}-error`);\n readonly hintId: Signal<string> = computed(() => `${this.key()}-hint`);\n\n readonly ariaInvalid: Signal<boolean> = computed(() => {\n const state = this.field()();\n return state.invalid() && state.touched();\n });\n\n readonly ariaRequired: Signal<true | null> = computed(() => {\n return this.field()().required?.() === true ? true : null;\n });\n\n readonly ariaDescribedBy: Signal<string | null> = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () =>\n Boolean((this.props() as { hint?: unknown } | undefined)?.hint),\n );\n\n // ───────────────────────────────────────────────────────────────────────────\n // Claim tracking — markers and ambient sub-components flip this to true so\n // dev-mode can warn when meta() is non-empty but nothing consumed it.\n // ───────────────────────────────────────────────────────────────────────────\n\n private readonly _claimed = signal(false);\n\n /** @internal Marker directives + ambient sub-components call this on construction. */\n markClaimed(): void {\n this._claimed.set(true);\n }\n\n private readonly logger = inject(DynamicFormLogger, { optional: true });\n\n constructor() {\n // Dev-mode safety net: a mapper or template-author binds meta() but no\n // NgForgeControl / NgForgeHostControl / ambient sub-component is in\n // place to consume it. Fires at most once per directive instance.\n if (isDevMode()) {\n let warned = false;\n afterRenderEffect({\n write: () => {\n if (warned) return;\n const meta = this.meta();\n if (!meta || Object.keys(meta).length === 0) return;\n if (untracked(this._claimed)) return;\n warned = true;\n // `untracked` safe — afterRenderEffect.write runs after first CD, so Shell's required `key` is bound (no NG0950).\n const key = untracked(this.key);\n const message =\n `NgForgeField - meta() provided for field \"${key}\" but no NgForgeControl / NgForgeHostControl / ` +\n `ambient sub-component claimed it. Meta will not be applied. ` +\n `See https://ng-forge.dev/building-an-adapter#meta-forwarding`;\n if (this.logger) {\n this.logger.warn(message);\n } else {\n console.warn(message);\n }\n },\n });\n }\n }\n}\n\n/**\n * Inputs forwarded onto `NgForgeField` via `hostDirectives`. Excludes\n * `key` / `className` (forwarded to `NgForgeFieldShell` separately via\n * `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this\n * directly — use the `NG_FORGE_FIELD` preset.\n */\nexport const NG_FORGE_VALUE_FIELD_INPUTS = ['field', 'label', 'placeholder', 'tabIndex', 'props', 'meta', 'validationMessages'] as const;\n\n// Compile-time lockstep: NG_FORGE_VALUE_FIELD_INPUTS must equal the declared\n// `input()` properties on NgForgeField. Drift in either direction fails the build.\nconst _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP: AssertTupleLockstep<\n NgForgeField,\n typeof NG_FORGE_VALUE_FIELD_INPUTS,\n 'NG_FORGE_VALUE_FIELD_INPUTS'\n> = true;\nvoid _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP;\n\n/**\n * `NgForgeField` typed for a specific value type. The directive itself stores\n * `field` as `Signal<FieldTree<unknown>>` because Angular's `hostDirectives`\n * doesn't propagate generics — `injectNgForgeField<T>()` narrows it at the\n * inject site so consumers don't have to declare a separate cast computed.\n */\nexport type TypedNgForgeField<T> = Omit<NgForgeField, 'field'> & {\n readonly field: Signal<FieldTree<T>>;\n};\n\n/**\n * Typed wrapper around `inject(NgForgeField)`. The generic narrows the\n * `field` signal to `Signal<FieldTree<T>>` for the calling component's value\n * type. The cast is unchecked — the runtime contract is that mappers only\n * forward a `field` whose value type matches the field-type definition.\n */\nexport function injectNgForgeField<T = unknown>(): TypedNgForgeField<T> {\n return inject(NgForgeField) as unknown as TypedNgForgeField<T>;\n}\n","import { Directive, inject, input, Signal } from '@angular/core';\nimport { type ArrayItemContext, type FormEvent, type FormEventConstructor } from '@ng-forge/dynamic-forms';\nimport { EventBus, resolveTokens } from '@ng-forge/dynamic-forms/internal';\nimport { ARRAY_CONTEXT, type DynamicText } from '@ng-forge/dynamic-forms/internal';\nimport type { EventArgs } from '../definitions/button-field';\nimport type { AssertTupleLockstep } from './assert-input-lockstep';\nimport { NgForgeFieldShell } from './ng-forge-field-shell.directive';\n\n/**\n * Action add-on directive — composed alongside `NgForgeFieldShell` via the\n * `NG_FORGE_ACTION` preset. Owns the inputs + dispatch logic every button /\n * action component needs (label, disabled, event, eventArgs, …) plus the\n * `[attr.hidden]` / `[attr.aria-disabled]` host bindings driven by its own\n * inputs (no field tree).\n */\n@Directive({\n host: {\n '[attr.hidden]': 'hidden() || null',\n '[attr.aria-disabled]': 'disabled() || null',\n },\n})\nexport class NgForgeAction<TEvent extends FormEvent = FormEvent> {\n // ───────────────────────────────────────────────────────────────────────────\n // Shell-owned identity — re-exported as plain `Signal<T>` so they don't get\n // flagged by the lockstep assertion below.\n // ───────────────────────────────────────────────────────────────────────────\n\n private readonly shell = inject(NgForgeFieldShell);\n readonly key: Signal<string> = this.shell.key;\n readonly className: Signal<string> = this.shell.className;\n\n // ───────────────────────────────────────────────────────────────────────────\n // Forwarded inputs — the action-specific subset of mapper output.\n // ───────────────────────────────────────────────────────────────────────────\n\n readonly label = input.required<DynamicText>();\n readonly disabled = input<boolean>(false);\n readonly hidden = input<boolean>(false);\n readonly tabIndex = input<number>();\n readonly event = input<FormEventConstructor<TEvent>>();\n readonly eventArgs = input<EventArgs>();\n readonly eventContext = input<ArrayItemContext>();\n readonly props = input<unknown>();\n\n // ───────────────────────────────────────────────────────────────────────────\n // Dispatch\n // ───────────────────────────────────────────────────────────────────────────\n\n private readonly eventBus = inject(EventBus);\n private readonly arrayContext = inject(ARRAY_CONTEXT, { optional: true });\n\n /**\n * Dispatch the configured event through the EventBus. Resolves `eventArgs`\n * tokens (`$key`, `$index`, `$arrayKey`, `formValue`, …) against the\n * ambient array context if present, falling back to a static `eventContext`\n * input or the field's own `key` otherwise. No-op when `event` is unset\n * (e.g. submit buttons handled by the native form lifecycle).\n */\n dispatch(): void {\n const event = this.event();\n if (!event) return;\n const args = this.eventArgs();\n if (!args || args.length === 0) {\n this.eventBus.dispatch(event);\n return;\n }\n const context: ArrayItemContext = this.arrayContext\n ? {\n key: this.key(),\n index: this.arrayContext.index(),\n arrayKey: this.arrayContext.arrayKey,\n formValue: this.arrayContext.formValue,\n }\n : (this.eventContext() ?? { key: this.key() });\n this.eventBus.dispatch(event, ...resolveTokens(args, context));\n }\n}\n\n/**\n * Inputs forwarded onto `NgForgeAction` via `hostDirectives`. Excludes\n * `key` / `className` (forwarded to `NgForgeFieldShell` separately via\n * `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this\n * directly — use the `NG_FORGE_ACTION` preset.\n */\nexport const NG_FORGE_ACTION_INPUTS = ['label', 'disabled', 'hidden', 'tabIndex', 'event', 'eventArgs', 'eventContext', 'props'] as const;\n\n// Compile-time lockstep: NG_FORGE_ACTION_INPUTS must equal the declared\n// `input()` properties on NgForgeAction. Drift fails the build.\nconst _NG_FORGE_ACTION_INPUTS_LOCKSTEP: AssertTupleLockstep<NgForgeAction, typeof NG_FORGE_ACTION_INPUTS, 'NG_FORGE_ACTION_INPUTS'> = true;\nvoid _NG_FORGE_ACTION_INPUTS_LOCKSTEP;\n\n/**\n * Typed wrapper around `inject(NgForgeAction)`. The generic narrows the\n * `event` input to a specific FormEvent subclass.\n */\nexport function injectNgForgeAction<TEvent extends FormEvent = FormEvent>(): NgForgeAction<TEvent> {\n return inject(NgForgeAction) as unknown as NgForgeAction<TEvent>;\n}\n","import { computed, Directive, inject, Injector, input, Signal, signal } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { AnyAddon, resolveDynamicValue } from '@ng-forge/dynamic-forms/internal';\nimport type { AssertTupleLockstep } from './assert-input-lockstep';\n\n/**\n * Universal addon-array plumbing: signal-cached `hidden` resolution and\n * slot bucketing. Used internally by the `NgForgeAddons` host wrapper —\n * consumers should compose `NgForgeAddons` directly and inject the\n * directive instance via `injectNgForgeAddons<TAddon>()`.\n */\n@Directive({})\nexport class NgForgeAddonsBase {\n /** Capture the host's injector so `toSignal` (inside `resolveDynamicValue`) ties to the host's `DestroyRef`. */\n private readonly hostInjector = inject(Injector);\n\n /** Addon array mapped from `FieldDef.addons` by `buildBaseInputs`. */\n readonly addons = input<ReadonlyArray<AnyAddon> | undefined>(undefined);\n\n /**\n * Per-addon `hidden` signal cache. Built via `explicitEffect` rather\n * than `computed` because `toSignal` (inside `resolveDynamicValue` for\n * Observable inputs) is disallowed in reactive contexts (NG0602).\n */\n private readonly _hiddenSignalCache = signal<ReadonlyMap<AnyAddon, Signal<boolean>>>(new Map());\n readonly hiddenSignalCache: Signal<ReadonlyMap<AnyAddon, Signal<boolean>>> = this._hiddenSignalCache.asReadonly();\n\n readonly visibleAddons: Signal<ReadonlyArray<AnyAddon>> = computed(() => {\n const list = this.addons() ?? [];\n const cache = this.hiddenSignalCache();\n return list.filter((a) => !(cache.get(a)?.() ?? false));\n });\n\n readonly hasAddons: Signal<boolean> = computed(() => this.visibleAddons().length > 0);\n readonly prefixAddons: Signal<ReadonlyArray<AnyAddon>> = computed(() => this.visibleAddons().filter((a) => a.slot === 'prefix'));\n readonly suffixAddons: Signal<ReadonlyArray<AnyAddon>> = computed(() => this.visibleAddons().filter((a) => a.slot === 'suffix'));\n\n /**\n * Visible addons grouped by their `slot` string. Convenience view for\n * adapters with non-universal slots (e.g., Ionic `start` / `end`).\n */\n readonly addonsBySlot: Signal<ReadonlyMap<string, ReadonlyArray<AnyAddon>>> = computed(() => {\n const map = new Map<string, AnyAddon[]>();\n for (const a of this.visibleAddons()) {\n const bucket = map.get(a.slot) ?? [];\n bucket.push(a);\n map.set(a.slot, bucket);\n }\n return map as ReadonlyMap<string, ReadonlyArray<AnyAddon>>;\n });\n\n /** Secondary index keyed by `hidden` value identity. Catches the common case\n * where a FormConfig re-emit produces structurally-different-but-content-\n * identical addon objects — without this, every re-emit would spawn a new\n * `toSignal` subscription for each addon's hidden axis. */\n private readonly _hiddenByValue = new WeakMap<object, Signal<boolean>>();\n\n constructor() {\n explicitEffect([this.addons], ([list]) => {\n const prev = this._hiddenSignalCache();\n const next = new Map<AnyAddon, Signal<boolean>>();\n for (const a of list ?? []) {\n // Tier 1: reference identity — the cheapest hit when the same addon\n // object is reused across emissions.\n let resolved = prev.get(a);\n if (!resolved) {\n // Tier 2: hidden-value identity — when `hidden` is an object\n // (Signal / Observable / function), the same instance can be passed\n // through multiple new addon objects on FormConfig re-emit.\n const h = a.hidden;\n const valueKey = typeof h === 'object' && h !== null ? h : null;\n resolved = valueKey ? this._hiddenByValue.get(valueKey) : undefined;\n if (!resolved) {\n resolved = resolveDynamicValue(a.hidden, false, this.hostInjector);\n if (valueKey) this._hiddenByValue.set(valueKey, resolved);\n }\n }\n next.set(a, resolved);\n }\n this._hiddenSignalCache.set(next);\n });\n }\n}\n\n/** Forwarded onto `NgForgeAddonsBase` via the `NgForgeAddons` wrapper. */\nexport const NG_FORGE_ADDONS_INPUTS = ['addons'] as const;\n\n// Compile-time lockstep — drift between the tuple and the declared inputs fails the build.\nconst _NG_FORGE_ADDONS_INPUTS_LOCKSTEP: AssertTupleLockstep<NgForgeAddonsBase, typeof NG_FORGE_ADDONS_INPUTS, 'NG_FORGE_ADDONS_INPUTS'> =\n true;\nvoid _NG_FORGE_ADDONS_INPUTS_LOCKSTEP;\n\n/**\n * Host wrapper for `NgForgeAddonsBase`. Consumers compose this directly:\n *\n * @if (ngfa.hasAddons()) {\n * <p-inputgroup>\n * @for (a of ngfa.prefixAddons(); track $index) {\n * <df-addon-slot [addon]=\"a\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n * }\n * <input ngForgeControl ... />\n * </p-inputgroup>\n * }\n * `,\n * })\n * export class MyInputField {\n * protected readonly ngfa = injectNgForgeAddons<MyInputAddon>();\n * }\n * ```\n *\n * The wrapper exists so `hostDirectives: [NgForgeAddons]` resolves\n * cross-package — same constraint that drives `NgForgeFieldHost`.\n */\n@Directive({\n hostDirectives: [{ directive: NgForgeAddonsBase, inputs: [...NG_FORGE_ADDONS_INPUTS] }],\n})\nexport class NgForgeAddons {}\n\n/** Narrowed view of `NgForgeAddonsBase` returned by `injectNgForgeAddons<TAddon>()`. */\nexport type TypedNgForgeAddons<TAddon extends AnyAddon = AnyAddon> = Omit<\n NgForgeAddonsBase,\n 'addons' | 'visibleAddons' | 'prefixAddons' | 'suffixAddons'\n> & {\n readonly addons: Signal<ReadonlyArray<TAddon> | undefined>;\n readonly visibleAddons: Signal<ReadonlyArray<TAddon>>;\n readonly prefixAddons: Signal<ReadonlyArray<TAddon>>;\n readonly suffixAddons: Signal<ReadonlyArray<TAddon>>;\n};\n\n/**\n * Typed inject helper. Always resolves to the underlying\n * `NgForgeAddonsBase` instance — the wrapper class has no instance\n * fields of its own.\n */\nexport function injectNgForgeAddons<TAddon extends AnyAddon = AnyAddon>(): TypedNgForgeAddons<TAddon> {\n return inject(NgForgeAddonsBase) as unknown as TypedNgForgeAddons<TAddon>;\n}\n","import { computed, Directive, inject, Injector, input, Signal, signal } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { BaseAddon, DynamicFormLogger, DynamicValue } from '@ng-forge/dynamic-forms';\nimport { ADDON_ACTION_REGISTRY, AddonActionHandler, WrapperFieldInputs } from '@ng-forge/dynamic-forms/internal';\nimport { AddonActionContext, resolveDynamicValue, writeToFieldValue } from '@ng-forge/dynamic-forms/internal';\nimport { ADDON_PRESET_HANDLER } from './addon-preset-handler.token';\nimport type { AssertTupleLockstep } from './assert-input-lockstep';\n\n/**\n * Permissive shape the directive reads from `addon()`. Adapter-specific\n * addon types (e.g., `PrimeButtonAddon`) narrow this at the inject site via\n * `injectNgForgeAddonAction<T>()`.\n */\ninterface ActionAddonShape extends BaseAddon {\n readonly preset?: string;\n readonly actionRef?: string;\n readonly action?: AddonActionHandler;\n readonly disabled?: DynamicValue<boolean>;\n readonly loading?: DynamicValue<boolean>;\n}\n\n/**\n * Universal click-dispatch plumbing for action-style addons (icon buttons,\n * labelled buttons, etc.): `disabled` / `loading` resolution from\n * `DynamicValue<boolean>`, action context construction, and the\n * `preset / actionRef / action` precedence dispatcher.\n */\n@Directive({})\nexport class NgForgeAddonActionBase {\n // The action registry is form-scoped. `optional: true` matches the\n // preset handler so addons rendered outside a form that called\n // `withAddonActions(...)` degrade gracefully instead of throwing —\n // `actionRef` dispatches then log an actionable warning and no-op.\n private readonly actionRegistry = inject(ADDON_ACTION_REGISTRY, { optional: true });\n private readonly presetHandler = inject(ADDON_PRESET_HANDLER, { optional: true });\n private readonly logger = inject(DynamicFormLogger);\n private readonly hostInjector = inject(Injector);\n\n readonly addon = input.required<ActionAddonShape>();\n readonly fieldInputs = input<WrapperFieldInputs | undefined>(undefined);\n\n /**\n * Per-value caches for `disabled` / `loading` signal resolution. Keyed by\n * the `DynamicValue` identity so a single `toSignal` subscription is\n * shared across addon-reference swaps that pass the same Observable.\n * Mirrors `NgForgeAddonsBase._hiddenByValue` — `resolveDynamicValue`\n * cannot run inside `computed`/`linkedSignal.computation` because\n * `toSignal` throws in reactive contexts (NG0602), and re-running it on\n * every recomputation would leak subscriptions on each cycle.\n */\n private readonly _disabledByValue = new WeakMap<object, Signal<boolean>>();\n private readonly _loadingByValue = new WeakMap<object, Signal<boolean>>();\n private readonly _resolvedDisabled = signal<Signal<boolean> | undefined>(undefined);\n private readonly _resolvedLoading = signal<Signal<boolean> | undefined>(undefined);\n\n readonly disabled: Signal<boolean> = computed(() => this._resolvedDisabled()?.() ?? false);\n readonly loading: Signal<boolean> = computed(() => this._resolvedLoading()?.() ?? false);\n\n constructor() {\n explicitEffect([this.addon], ([addon]) => {\n this._resolvedDisabled.set(this._resolveAxis(addon.disabled, this._disabledByValue));\n this._resolvedLoading.set(this._resolveAxis(addon.loading, this._loadingByValue));\n });\n }\n\n private _resolveAxis(value: DynamicValue<boolean> | undefined, cache: WeakMap<object, Signal<boolean>>): Signal<boolean> {\n const valueKey = typeof value === 'object' && value !== null ? value : null;\n let resolved = valueKey ? cache.get(valueKey) : undefined;\n if (!resolved) {\n resolved = resolveDynamicValue(value, false, this.hostInjector);\n if (valueKey) cache.set(valueKey, resolved);\n }\n return resolved;\n }\n\n /**\n * Resolve the click handler from the addon's three-axis click variant and\n * invoke it. Precedence on multi-set: `preset > actionRef > action`. The\n * validator drops multi-set configs at config-init time; this is\n * defence-in-depth + a logged warning so the misconfiguration is loud.\n */\n dispatch(): void {\n const addon = this.addon();\n const ctx = this.buildActionContext();\n\n const variantsSet = [addon.preset, addon.actionRef, addon.action].filter((v) => v !== undefined).length;\n if (variantsSet > 1) {\n this.logger.warn(\n `Addon button: more than one of preset/actionRef/action configured — dispatching by precedence (preset > actionRef > action). Ship at most one to silence this.`,\n );\n }\n\n if (addon.preset !== undefined) {\n if (!this.presetHandler) {\n this.logger.warn(\n `Addon button: preset '${addon.preset}' is configured but no ADDON_PRESET_HANDLER is registered in this scope. Did the adapter forget to provide one at the host field?`,\n );\n return;\n }\n void this.presetHandler.run(addon.preset, ctx);\n return;\n }\n\n if (addon.actionRef !== undefined) {\n const handler = this.actionRegistry?.get(addon.actionRef);\n if (handler) {\n handler(ctx);\n } else {\n this.logger.warn(\n `Addon button: actionRef '${addon.actionRef}' is not registered. ` +\n (this.actionRegistry\n ? `Did you call withAddonActions({ ${addon.actionRef}: ... })?`\n : `No withAddonActions(...) registry is reachable in this scope — was the addon rendered outside a form?`),\n );\n }\n return;\n }\n\n if (addon.action !== undefined) {\n addon.action(ctx);\n return;\n }\n\n // Decorative — no handler configured.\n }\n\n /**\n * Build an `AddonActionContext` for handlers from the wrapper-style\n * `fieldInputs` bag. Returns the field-bound variant when a host field\n * is reachable, or the orphan variant otherwise.\n */\n buildActionContext(): AddonActionContext {\n const inputs = this.fieldInputs();\n const key = inputs?.key ?? '';\n const type = inputs?.type ?? '';\n const tree = inputs?.field;\n if (tree) {\n // `writeToFieldValue` centralises the `Signal → WritableSignal` cast and\n // warns at runtime if Signal Forms ever stops storing a writable signal\n // here (the alternative — a silent no-op preset — is much worse).\n const setValue = inputs?.setValue ?? ((next: unknown) => writeToFieldValue(tree.value, next, this.logger));\n return {\n field: { key, type },\n form: tree,\n value: tree.value() ?? undefined,\n setValue,\n };\n }\n return {\n field: { key, type },\n form: null,\n value: undefined,\n };\n }\n}\n\n/** Forwarded onto `NgForgeAddonActionBase` via the `NgForgeAddonAction` wrapper. */\nexport const NG_FORGE_ADDON_ACTION_INPUTS = ['addon', 'fieldInputs'] as const;\n\nconst _NG_FORGE_ADDON_ACTION_INPUTS_LOCKSTEP: AssertTupleLockstep<\n NgForgeAddonActionBase,\n typeof NG_FORGE_ADDON_ACTION_INPUTS,\n 'NG_FORGE_ADDON_ACTION_INPUTS'\n> = true;\nvoid _NG_FORGE_ADDON_ACTION_INPUTS_LOCKSTEP;\n\n/** Host wrapper for `NgForgeAddonActionBase`. Consumers compose this directly: */\n@Directive({\n hostDirectives: [{ directive: NgForgeAddonActionBase, inputs: [...NG_FORGE_ADDON_ACTION_INPUTS] }],\n})\nexport class NgForgeAddonAction {}\n\n/** Narrowed view of `NgForgeAddonActionBase` returned by `injectNgForgeAddonAction<T>()`. */\nexport type TypedNgForgeAddonAction<TAddon extends ActionAddonShape = ActionAddonShape> = Omit<NgForgeAddonActionBase, 'addon'> & {\n readonly addon: Signal<TAddon>;\n};\n\nexport function injectNgForgeAddonAction<TAddon extends ActionAddonShape = ActionAddonShape>(): TypedNgForgeAddonAction<TAddon> {\n return inject(NgForgeAddonActionBase) as unknown as TypedNgForgeAddonAction<TAddon>;\n}\n","import { Directive, ElementRef, inject, input } from '@angular/core';\nimport { setupMarkerAttrTracking } from '../utils/setup-meta-tracking';\nimport { NgForgeField } from './ng-forge-field.directive';\n\n/**\n * Marker directive applied to the canonical control element of an\n * `NgForgeField`-using component. Forwards the field's `meta` AND\n * parent-derived aria signals (`aria-invalid`, `aria-required`,\n * `aria-describedby`) onto a target element.\n *\n * @for (option of options(); track option.value) {\n * <input type=\"radio\" ngForgeControl [value]=\"option.value\" />\n * }\n * ```\n *\n * Each `@for` iteration creates its own directive instance; Angular's\n * structural lifecycle handles add/remove naturally.\n */\n@Directive({ selector: '[ngForgeControl]' })\nexport class NgForgeControl {\n readonly target = input<string | undefined>(undefined, { alias: 'ngForgeControl' });\n\n constructor() {\n const parent = inject(NgForgeField);\n setupMarkerAttrTracking(inject(ElementRef), parent, () => this.target());\n parent.markClaimed();\n }\n}\n\n/**\n * Selectorless companion directive for shadow-DOM-encapsulated components\n * (PrimeNG `p-select`, Ionic web components, etc.) where no light-DOM child\n * is the canonical control. Added to the component's `hostDirectives` so the\n * field's `meta` + parent aria land on the component's own host element.\n */\n// Empty `@Directive({})` is intentional — selectorless; used only via `hostDirectives`.\n@Directive({})\nexport class NgForgeHostControl {\n constructor() {\n const parent = inject(NgForgeField);\n setupMarkerAttrTracking(inject(ElementRef), parent, () => undefined);\n parent.markClaimed();\n }\n}\n","import { Directive } from '@angular/core';\nimport { NgForgeAction, NG_FORGE_ACTION_INPUTS } from './ng-forge-action.directive';\nimport { NgForgeFieldShell, NG_FORGE_FIELD_SHELL_INPUTS } from './ng-forge-field-shell.directive';\nimport { NgForgeField, NG_FORGE_VALUE_FIELD_INPUTS } from './ng-forge-field.directive';\n\n/**\n * Value-bearing field host wrapper. Composes `NgForgeFieldShell` (universal\n * identity) with `NgForgeField` (value + validation + aria + meta-tracking)\n * in a single host-directive entry, so consumer components compose just one\n * directive instead of two.\n */\n@Directive({\n hostDirectives: [\n { directive: NgForgeFieldShell, inputs: [...NG_FORGE_FIELD_SHELL_INPUTS] },\n { directive: NgForgeField, inputs: [...NG_FORGE_VALUE_FIELD_INPUTS] },\n ],\n})\nexport class NgForgeFieldHost {}\n\n/**\n * Action / button host wrapper. Composes `NgForgeFieldShell` with\n * `NgForgeAction` (event dispatch + aria-disabled) for button-shaped\n * components.\n */\n@Directive({\n hostDirectives: [\n { directive: NgForgeFieldShell, inputs: [...NG_FORGE_FIELD_SHELL_INPUTS] },\n { directive: NgForgeAction, inputs: [...NG_FORGE_ACTION_INPUTS] },\n ],\n})\nexport class NgForgeActionHost {}\n","import { EnvironmentInjector, Provider, runInInjectionContext, signal, type Type, type WritableSignal } from '@angular/core';\nimport { ComponentFixture, getTestBed, TestBed } from '@angular/core/testing';\nimport { type FieldTree, form, schema, type SchemaPath } from '@angular/forms/signals';\nimport { type FormEvent, type FormEventConstructor } from '@ng-forge/dynamic-forms';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { DEFAULT_VALIDATION_MESSAGES, type DynamicText, type ValidationMessages } from '@ng-forge/dynamic-forms/internal';\nimport type { EventArgs } from '../definitions/button-field';\n\nexport interface CreateNgForgeFieldFixtureOptions<TValue = unknown> {\n /** Field key. Becomes the only property on the wrapping form. */\n readonly key: string;\n /** Initial value for the field. */\n readonly value?: TValue;\n /** Optional schema callback that receives the field's `SchemaPath`. Use it to attach validators (`required`, `minLength`, etc.). */\n readonly schema?: (fieldPath: SchemaPath<TValue>) => void;\n /** Mark the field as touched after creation (useful for invalid-state assertions). */\n readonly touched?: boolean;\n /** Extra inputs to set on the component before `detectChanges()`. */\n readonly inputs?: Record<string, unknown>;\n /** Extra TestBed providers. */\n readonly providers?: readonly Provider[];\n}\n\nexport interface NgForgeFieldFixture<C, TValue = unknown> {\n readonly fixture: ComponentFixture<C>;\n readonly field: FieldTree<TValue>;\n readonly rootValue: WritableSignal<Record<string, TValue>>;\n}\n\n/** Guards against a second, uninitialized @angular/core/testing instance (Vitest externalization). */\nfunction assertTestEnvironmentInitialized(): void {\n if (getTestBed().platform) return;\n throw new Error(\n '[Dynamic Forms] The Angular test environment was never initialized for the @angular/core/testing instance this fixture resolved. ' +\n 'Under Vitest this typically means your specs are inlined but @ng-forge/dynamic-forms is externalized, creating a second testing instance. ' +\n \"Fix: add the package to Vitest's server.deps.inline:\\n\" +\n \"test: { server: { deps: { inline: ['@ng-forge/dynamic-forms'] } } }\\n\" +\n 'On Jest or Karma this instead means initTestEnvironment was never called; add it to your test setup file (e.g. getTestBed().initTestEnvironment(...)).',\n );\n}\n\n/**\n * Builds a `ComponentFixture` for an `NgForgeField`-composing component with\n * `field` + `key` bound BEFORE `detectChanges()` — sidesteps the NG0950 that\n * Shell's required `key` and Field's required `field` would otherwise throw.\n */\nexport function createNgForgeFieldFixture<C, TValue = unknown>(\n component: Type<C>,\n options: CreateNgForgeFieldFixtureOptions<TValue>,\n): NgForgeFieldFixture<C, TValue> {\n assertTestEnvironmentInitialized();\n TestBed.configureTestingModule({ imports: [component], providers: [...(options.providers ?? [])] });\n\n const key = options.key;\n const rootValue = signal<Record<string, TValue>>({ [key]: options.value as TValue });\n\n const injector = TestBed.inject(EnvironmentInjector);\n const schemaCallback = options.schema;\n const root = runInInjectionContext(injector, () => {\n if (!schemaCallback) return form(rootValue);\n return form(\n rootValue,\n schema<Record<string, TValue>>((path) => {\n schemaCallback(path[key] as SchemaPath<TValue>);\n }),\n );\n });\n\n const field = (root as unknown as Record<string, FieldTree<TValue>>)[key];\n\n if (options.touched) field().markAsTouched();\n\n const fixture = TestBed.createComponent(component);\n fixture.componentRef.setInput('field', field);\n fixture.componentRef.setInput('key', key);\n if (options.inputs) {\n for (const [name, value] of Object.entries(options.inputs)) {\n fixture.componentRef.setInput(name, value);\n }\n }\n\n return { fixture, field, rootValue };\n}\n\nexport interface CreateNgForgeActionFixtureOptions<TEvent extends FormEvent = FormEvent> {\n readonly key: string;\n readonly label: DynamicText;\n readonly event?: FormEventConstructor<TEvent>;\n readonly eventArgs?: EventArgs;\n readonly disabled?: boolean;\n readonly hidden?: boolean;\n readonly inputs?: Record<string, unknown>;\n readonly providers?: readonly Provider[];\n}\n\nexport interface NgForgeActionFixture<C> {\n readonly fixture: ComponentFixture<C>;\n readonly eventBus: EventBus;\n}\n\n/**\n * Builds a `ComponentFixture` for an `NgForgeAction`-composing component with\n * `key` + `label` bound and `EventBus` provided. The caller calls\n * `detectChanges()`; the harness handles the NG0950-avoidance bindings.\n */\nexport function createNgForgeActionFixture<C, TEvent extends FormEvent = FormEvent>(\n component: Type<C>,\n options: CreateNgForgeActionFixtureOptions<TEvent>,\n): NgForgeActionFixture<C> {\n assertTestEnvironmentInitialized();\n TestBed.configureTestingModule({ imports: [component], providers: [EventBus, ...(options.providers ?? [])] });\n\n const fixture = TestBed.createComponent(component);\n fixture.componentRef.setInput('key', options.key);\n fixture.componentRef.setInput('label', options.label);\n if (options.event) fixture.componentRef.setInput('event', options.event);\n if (options.eventArgs) fixture.componentRef.setInput('eventArgs', options.eventArgs);\n if (options.disabled !== undefined) fixture.componentRef.setInput('disabled', options.disabled);\n if (options.hidden !== undefined) fixture.componentRef.setInput('hidden', options.hidden);\n if (options.inputs) {\n for (const [name, value] of Object.entries(options.inputs)) {\n fixture.componentRef.setInput(name, value);\n }\n }\n\n return { fixture, eventBus: TestBed.inject(EventBus) };\n}\n\n/** Provider for `DEFAULT_VALIDATION_MESSAGES` in field tests — supply the default-messages map and the directive resolves it from DI. */\nexport function provideTestValidationMessages(messages: ValidationMessages): Provider {\n return { provide: DEFAULT_VALIDATION_MESSAGES, useValue: signal(messages) };\n}\n"]}
|
|
@@ -2185,6 +2185,18 @@ function validateNoNestedHttpConditions(expression) {
|
|
|
2185
2185
|
}
|
|
2186
2186
|
}
|
|
2187
2187
|
}
|
|
2188
|
+
function containsInlineFunction(expression) {
|
|
2189
|
+
if (expression.type === 'and' || expression.type === 'or') {
|
|
2190
|
+
return expression.conditions.some(containsInlineFunction);
|
|
2191
|
+
}
|
|
2192
|
+
if (expression.type === 'custom') {
|
|
2193
|
+
return typeof expression.fn === 'function';
|
|
2194
|
+
}
|
|
2195
|
+
if (expression.type === 'async') {
|
|
2196
|
+
return typeof expression.asyncFn === 'function';
|
|
2197
|
+
}
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2188
2200
|
function createLogicFunction(expression) {
|
|
2189
2201
|
if (expression?.type === 'async') {
|
|
2190
2202
|
return createAsyncConditionLogicFunction(expression);
|
|
@@ -2198,16 +2210,21 @@ function createLogicFunction(expression) {
|
|
|
2198
2210
|
const functionRegistry = inject(FunctionRegistryService);
|
|
2199
2211
|
const fieldContextRegistry = inject(FieldContextRegistryService);
|
|
2200
2212
|
const cacheService = inject(LogicFunctionCacheService);
|
|
2213
|
+
const cacheable = !expression || !containsInlineFunction(expression);
|
|
2201
2214
|
const cacheKey = stableStringify(expression);
|
|
2215
|
+
if (cacheable) {
|
|
2202
2216
|
const cached = cacheService.logicFunctionCache.get(cacheKey);
|
|
2203
|
-
|
|
2204
|
-
|
|
2217
|
+
if (cached) {
|
|
2218
|
+
return cached;
|
|
2219
|
+
}
|
|
2205
2220
|
}
|
|
2206
2221
|
const fn = (ctx) => {
|
|
2207
2222
|
const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());
|
|
2208
2223
|
return evaluateCondition(expression, evaluationContext);
|
|
2209
2224
|
};
|
|
2225
|
+
if (cacheable) {
|
|
2210
2226
|
cacheService.logicFunctionCache.set(cacheKey, fn);
|
|
2227
|
+
}
|
|
2211
2228
|
return fn;
|
|
2212
2229
|
}
|
|
2213
2230
|
function createDebouncedLogicFunction(expression, debounceMs) {
|
|
@@ -2224,10 +2241,13 @@ function createDebouncedLogicFunction(expression, debounceMs) {
|
|
|
2224
2241
|
const fieldContextRegistry = inject(FieldContextRegistryService);
|
|
2225
2242
|
const injector = inject(Injector);
|
|
2226
2243
|
const cacheService = inject(LogicFunctionCacheService);
|
|
2244
|
+
const cacheable = !expression || !containsInlineFunction(expression);
|
|
2227
2245
|
const cacheKey = `${stableStringify(expression)}:${debounceMs}`;
|
|
2246
|
+
if (cacheable) {
|
|
2228
2247
|
const cached = cacheService.debouncedLogicFunctionCache.get(cacheKey);
|
|
2229
|
-
|
|
2230
|
-
|
|
2248
|
+
if (cached) {
|
|
2249
|
+
return cached;
|
|
2250
|
+
}
|
|
2231
2251
|
}
|
|
2232
2252
|
const fn = (ctx) => {
|
|
2233
2253
|
const contextKey = safeReadPathKeys(ctx).join('.');
|
|
@@ -2252,7 +2272,9 @@ function createDebouncedLogicFunction(expression, debounceMs) {
|
|
|
2252
2272
|
});
|
|
2253
2273
|
return debouncedValue() ?? false;
|
|
2254
2274
|
};
|
|
2275
|
+
if (cacheable) {
|
|
2255
2276
|
cacheService.debouncedLogicFunctionCache.set(cacheKey, fn);
|
|
2277
|
+
}
|
|
2256
2278
|
return fn;
|
|
2257
2279
|
}
|
|
2258
2280
|
|
|
@@ -3287,16 +3309,23 @@ function isDisplayOnlyField(field) {
|
|
|
3287
3309
|
}
|
|
3288
3310
|
|
|
3289
3311
|
|
|
3312
|
+
function dynamicTextToObservable(value, injector) {
|
|
3313
|
+
if (value === undefined) {
|
|
3314
|
+
return of('');
|
|
3315
|
+
}
|
|
3316
|
+
if (isObservable(value)) {
|
|
3317
|
+
return value;
|
|
3318
|
+
}
|
|
3319
|
+
if (isSignal(value)) {
|
|
3320
|
+
return untracked(() => toObservable(value, { injector }));
|
|
3321
|
+
}
|
|
3322
|
+
return of(String(value));
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3290
3325
|
class DynamicTextPipe {
|
|
3291
3326
|
injector = inject(Injector);
|
|
3292
3327
|
transform(value) {
|
|
3293
|
-
|
|
3294
|
-
return value;
|
|
3295
|
-
}
|
|
3296
|
-
if (isSignal(value)) {
|
|
3297
|
-
return toObservable(value, { injector: this.injector });
|
|
3298
|
-
}
|
|
3299
|
-
return of(value || '');
|
|
3328
|
+
return dynamicTextToObservable(value ?? undefined, this.injector);
|
|
3300
3329
|
}
|
|
3301
3330
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
3302
3331
|
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
|
|
@@ -3326,19 +3355,6 @@ function applyMetaToElement(element, meta, previouslyApplied) {
|
|
|
3326
3355
|
return newApplied;
|
|
3327
3356
|
}
|
|
3328
3357
|
|
|
3329
|
-
function dynamicTextToObservable(value, injector) {
|
|
3330
|
-
if (value === undefined) {
|
|
3331
|
-
return of('');
|
|
3332
|
-
}
|
|
3333
|
-
if (isObservable(value)) {
|
|
3334
|
-
return value;
|
|
3335
|
-
}
|
|
3336
|
-
if (isSignal(value)) {
|
|
3337
|
-
return toObservable(value, { injector });
|
|
3338
|
-
}
|
|
3339
|
-
return of(String(value));
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
3358
|
const INITIALIZATION_TIMEOUT_MS = new InjectionToken('INITIALIZATION_TIMEOUT_MS', {
|
|
3343
3359
|
providedIn: 'root',
|
|
3344
3360
|
factory: () => 10_000,
|