@ng-forge/dynamic-forms 0.9.0-next.9 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -0
  2. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-us7mQtVQ.mjs → ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs} +171 -73
  3. package/fesm2022/ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs.map +1 -0
  4. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-BCIQ6_Mm.mjs → ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs} +12 -8
  5. package/fesm2022/ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs.map +1 -0
  6. package/fesm2022/{ng-forge-dynamic-forms-container-utils-CzZlFNA0.mjs → ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs} +2 -2
  7. package/fesm2022/{ng-forge-dynamic-forms-container-utils-CzZlFNA0.mjs.map → ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs.map} +1 -1
  8. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-Dhlf6UxN.mjs → ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs} +5 -5
  9. package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs.map +1 -0
  10. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DqQ8BA4W.mjs → ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs} +2 -2
  11. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DqQ8BA4W.mjs.map → ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs.map} +1 -1
  12. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-DKcoco0G.mjs → ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs} +36 -12
  13. package/fesm2022/ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs.map +1 -0
  14. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +936 -54
  15. package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
  16. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-CfbmSRM_.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs} +2230 -445
  17. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs.map +1 -0
  18. package/fesm2022/ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs +114 -0
  19. package/fesm2022/ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs.map +1 -0
  20. package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs +34 -0
  21. package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs.map +1 -0
  22. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-8C1-oyU_.mjs → ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs} +5 -5
  23. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-8C1-oyU_.mjs.map → ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs.map} +1 -1
  24. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  25. package/package.json +2 -2
  26. package/types/ng-forge-dynamic-forms-integration.d.ts +587 -11
  27. package/types/ng-forge-dynamic-forms.d.ts +3841 -2428
  28. package/fesm2022/ng-forge-dynamic-forms-array-field.component-us7mQtVQ.mjs.map +0 -1
  29. package/fesm2022/ng-forge-dynamic-forms-container-field.component-BCIQ6_Mm.mjs.map +0 -1
  30. package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-Dhlf6UxN.mjs.map +0 -1
  31. package/fesm2022/ng-forge-dynamic-forms-group-field.component-DKcoco0G.mjs.map +0 -1
  32. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CfbmSRM_.mjs.map +0 -1
  33. package/fesm2022/ng-forge-dynamic-forms-page-field.component-DqWAcwhG.mjs +0 -110
  34. package/fesm2022/ng-forge-dynamic-forms-page-field.component-DqWAcwhG.mjs.map +0 -1
  35. package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs +0 -34
  36. package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs.map +0 -1
@@ -1,9 +1,13 @@
1
- import { inject, computed, signal, Injector, afterRenderEffect } from '@angular/core';
2
- import { FIELD_SIGNAL_CONTEXT, omit, buildBaseInputs, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, resolveNonFieldHidden, resolveNonFieldDisabled, RootFormRegistryService, ARRAY_CONTEXT, DynamicFormLogger, AppendArrayItemEvent, PrependArrayItemEvent, InsertArrayItemEvent, RemoveAtIndexEvent, PopArrayItemEvent, ShiftArrayItemEvent, FORM_OPTIONS, resolveSubmitButtonDisabled, resolveNextButtonDisabled, NextPageEvent, PreviousPageEvent, dynamicTextToObservable, interpolateParams, isEqual, applyMetaToElement } from '@ng-forge/dynamic-forms';
3
- export { isEqual, resolveNonFieldDisabled, resolveNonFieldHidden } from '@ng-forge/dynamic-forms';
1
+ import * as i0 from '@angular/core';
2
+ import { inject, computed, signal, Injector, isDevMode, afterRenderEffect, input, Directive, untracked, InjectionToken, ElementRef, EnvironmentInjector, runInInjectionContext } from '@angular/core';
3
+ import { injectFieldSignalContext, omit, buildBaseInputs, DEFAULT_PROPS, resolveNonFieldHidden, resolveNonFieldDisabled, RootFormRegistryService, ARRAY_CONTEXT, DynamicFormLogger, AppendArrayItemEvent, PrependArrayItemEvent, InsertArrayItemEvent, RemoveAtIndexEvent, PopArrayItemEvent, ShiftArrayItemEvent, FORM_OPTIONS, resolveSubmitButtonDisabled, resolveNextButtonDisabled, NextPageEvent, PreviousPageEvent, dynamicTextToObservable, interpolateParams, isEqual, applyMetaToElement, DEFAULT_VALIDATION_MESSAGES, EventBus, resolveTokens, resolveDynamicValue, ADDON_ACTION_REGISTRY, writeToFieldValue } from '@ng-forge/dynamic-forms';
4
+ export { createWrappers, isEqual, isWrappersBundle, resolveNonFieldDisabled, resolveNonFieldHidden, wrapperProps } from '@ng-forge/dynamic-forms';
4
5
  import { toObservable, toSignal } from '@angular/core/rxjs-interop';
5
6
  import { combineLatest, of } from 'rxjs';
6
7
  import { switchMap, map } from 'rxjs/operators';
8
+ import { explicitEffect } from 'ngxtension/explicit-effect';
9
+ import { TestBed } from '@angular/core/testing';
10
+ import { form, schema } from '@angular/forms/signals';
7
11
 
8
12
  /**
9
13
  * Creates a value field context from the injected FIELD_SIGNAL_CONTEXT.
@@ -20,7 +24,7 @@ import { switchMap, map } from 'rxjs/operators';
20
24
  * @returns The value field context with reactive field tree accessor
21
25
  */
22
26
  function resolveValueFieldContext() {
23
- const context = inject(FIELD_SIGNAL_CONTEXT);
27
+ const context = injectFieldSignalContext();
24
28
  return {
25
29
  getFieldTree: (fieldKey) => {
26
30
  // Access context.form here (inside a function that will be called from a computed)
@@ -44,10 +48,9 @@ function resolveValueFieldContext() {
44
48
  * @param fieldDef The value field definition
45
49
  * @param ctx The resolved value field context
46
50
  * @param defaultProps Default props from the form configuration (may be undefined if not configured)
47
- * @param defaultValidationMessages Default validation messages from the form configuration (may be undefined if not configured)
48
51
  * @returns Record of input names to values
49
52
  */
50
- function buildValueFieldInputs(fieldDef, ctx, defaultProps, defaultValidationMessages) {
53
+ function buildValueFieldInputs(fieldDef, ctx, defaultProps) {
51
54
  const omittedFields = omit(fieldDef, ['value']);
52
55
  const baseInputs = buildBaseInputs(omittedFields, defaultProps);
53
56
  const inputs = {
@@ -57,9 +60,6 @@ function buildValueFieldInputs(fieldDef, ctx, defaultProps, defaultValidationMes
57
60
  if (fieldDef.placeholder !== undefined) {
58
61
  inputs['placeholder'] = fieldDef.placeholder;
59
62
  }
60
- if (defaultValidationMessages !== undefined) {
61
- inputs['defaultValidationMessages'] = defaultValidationMessages;
62
- }
63
63
  // Resolve field tree reactively - this call is inside a computed,
64
64
  // so it establishes the reactive dependency correctly
65
65
  const fieldTree = ctx.getFieldTree(fieldDef.key);
@@ -92,13 +92,12 @@ function valueFieldMapper(fieldDef) {
92
92
  // Get the context once at injection time (captures the FIELD_SIGNAL_CONTEXT reference)
93
93
  const ctx = resolveValueFieldContext();
94
94
  const defaultProps = inject(DEFAULT_PROPS);
95
- const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
96
95
  // The computed calls ctx.getFieldTree(fieldDef.key) inside, which:
97
96
  // 1. Accesses context.form (triggering the getter for array items)
98
97
  // 2. Returns the field tree if available, or undefined if not yet
99
98
  // 3. Re-runs when the form structure updates (reactive dependency)
100
99
  return computed(() => {
101
- return buildValueFieldInputs(fieldDef, ctx, defaultProps(), defaultValidationMessages());
100
+ return buildValueFieldInputs(fieldDef, ctx, defaultProps());
102
101
  });
103
102
  }
104
103
 
@@ -112,13 +111,11 @@ function valueFieldMapper(fieldDef) {
112
111
  * @returns Signal containing Record of input names to values for ngComponentOutlet
113
112
  */
114
113
  function checkboxFieldMapper(fieldDef) {
115
- const context = inject(FIELD_SIGNAL_CONTEXT);
114
+ const context = injectFieldSignalContext();
116
115
  const defaultProps = inject(DEFAULT_PROPS);
117
- const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
118
116
  return computed(() => {
119
117
  const omittedFields = omit(fieldDef, ['value']);
120
118
  const baseInputs = buildBaseInputs(omittedFields, defaultProps());
121
- const validationMessages = defaultValidationMessages();
122
119
  const inputs = {
123
120
  ...baseInputs,
124
121
  validationMessages: fieldDef.validationMessages ?? {},
@@ -126,9 +123,6 @@ function checkboxFieldMapper(fieldDef) {
126
123
  if (fieldDef.placeholder !== undefined) {
127
124
  inputs['placeholder'] = fieldDef.placeholder;
128
125
  }
129
- if (validationMessages !== undefined) {
130
- inputs['defaultValidationMessages'] = validationMessages;
131
- }
132
126
  // Access form inside computed for reactivity and to handle cases where
133
127
  // form may not be immediately available (e.g., during array item initialization)
134
128
  const formRoot = context.form;
@@ -172,9 +166,8 @@ function toDate(value) {
172
166
  function datepickerFieldMapper(fieldDef) {
173
167
  const ctx = resolveValueFieldContext();
174
168
  const defaultProps = inject(DEFAULT_PROPS);
175
- const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
176
169
  return computed(() => {
177
- const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps(), defaultValidationMessages());
170
+ const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
178
171
  // Add datepicker-specific properties, converting strings to Date objects
179
172
  if (fieldDef.minDate !== undefined) {
180
173
  inputs['minDate'] = toDate(fieldDef.minDate);
@@ -200,10 +193,9 @@ function datepickerFieldMapper(fieldDef) {
200
193
  function optionsFieldMapper(fieldDef) {
201
194
  const ctx = resolveValueFieldContext();
202
195
  const defaultProps = inject(DEFAULT_PROPS);
203
- const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
204
196
  return computed(() => {
205
197
  // Cast to BaseValueField - safe because all FieldWithOptions types extend BaseValueField
206
- const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps(), defaultValidationMessages());
198
+ const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
207
199
  // Add options property
208
200
  inputs['options'] = fieldDef.options;
209
201
  return inputs;
@@ -224,9 +216,8 @@ function optionsFieldMapper(fieldDef) {
224
216
  function sliderFieldMapper(fieldDef) {
225
217
  const ctx = resolveValueFieldContext();
226
218
  const defaultProps = inject(DEFAULT_PROPS);
227
- const defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES);
228
219
  return computed(() => {
229
- const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps(), defaultValidationMessages());
220
+ const inputs = buildValueFieldInputs(fieldDef, ctx, defaultProps());
230
221
  // Add slider-specific properties (these are at field level, not in props)
231
222
  if (fieldDef.minValue !== undefined) {
232
223
  inputs['minValue'] = fieldDef.minValue;
@@ -570,7 +561,7 @@ function submitButtonFieldMapper(fieldDef) {
570
561
  // =============================================================================
571
562
  /** Mapper for next page button — preconfigures NextPageEvent. */
572
563
  function nextButtonFieldMapper(fieldDef) {
573
- const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
564
+ const fieldSignalContext = injectFieldSignalContext();
574
565
  const rootFormRegistry = inject(RootFormRegistryService);
575
566
  const defaultProps = inject(DEFAULT_PROPS);
576
567
  const formOptions = inject(FORM_OPTIONS);
@@ -730,6 +721,59 @@ function shouldShowErrors(field) {
730
721
  });
731
722
  }
732
723
 
724
+ /**
725
+ * Resolve target set, compute combined attrs, apply via applyMetaToElement.
726
+ * Target resolution is cached by selector identity — re-resolves only on
727
+ * selector change or `dependents` change. afterRenderEffect is signal-tracked,
728
+ * so the callback re-runs only when computeAttrs / dependents differ.
729
+ * @internal
730
+ */
731
+ function applyTrackedAttrsToTargets(elementRef, computeAttrs, options = {}) {
732
+ const appliedByElement = new WeakMap();
733
+ let cachedSelector;
734
+ let cachedTargets = [];
735
+ let previousAttrs;
736
+ let previousDeps;
737
+ const warnedSelectors = isDevMode() ? new Set() : undefined;
738
+ afterRenderEffect({
739
+ write: () => {
740
+ // Read dependents first so afterRenderEffect tracks them.
741
+ const currentDeps = options.dependents?.map((d) => d());
742
+ const attrs = computeAttrs();
743
+ const selector = (typeof options.selector === 'function' ? options.selector() : options.selector)?.trim() || undefined;
744
+ const selectorChanged = selector !== cachedSelector;
745
+ const depsChanged = !!currentDeps &&
746
+ (!previousDeps || currentDeps.length !== previousDeps.length || currentDeps.some((d, i) => d !== previousDeps[i]));
747
+ if (selectorChanged || depsChanged || cachedTargets.length === 0) {
748
+ cachedSelector = selector;
749
+ cachedTargets = selector ? Array.from(elementRef.nativeElement.querySelectorAll(selector)) : [elementRef.nativeElement];
750
+ }
751
+ previousDeps = currentDeps;
752
+ // Dev-mode safety net: selector typo or wrong descendant query leaves
753
+ // meta + aria unapplied. Warn the first time we'd apply non-empty attrs
754
+ // to zero matches for a given selector. Re-warns only when the selector
755
+ // string changes (so authors fixing one typo see the next).
756
+ if (warnedSelectors && selector && cachedTargets.length === 0 && attrs && Object.keys(attrs).length > 0) {
757
+ if (!warnedSelectors.has(selector)) {
758
+ warnedSelectors.add(selector);
759
+ const tag = elementRef.nativeElement.tagName.toLowerCase();
760
+ console.warn(`[Dynamic Forms] ngForgeControl selector "${selector}" matched no descendants of <${tag}> — meta + aria attributes will not be applied. ` +
761
+ `Check the selector for typos, or supply 'dependents' if the target is rendered after a signal emits.`);
762
+ }
763
+ }
764
+ // Bail when attrs unchanged AND nothing structural moved — keeps repeated
765
+ // effect runs (driven by an unrelated tracked-signal flicker) cheap.
766
+ if (!selectorChanged && !depsChanged && isEqual(attrs, previousAttrs)) {
767
+ return;
768
+ }
769
+ previousAttrs = attrs;
770
+ for (const el of cachedTargets) {
771
+ const applied = appliedByElement.get(el) ?? new Set();
772
+ appliedByElement.set(el, applyMetaToElement(el, attrs, applied));
773
+ }
774
+ },
775
+ });
776
+ }
733
777
  /**
734
778
  * Sets up automatic meta attribute tracking and application for wrapped components.
735
779
  *
@@ -768,42 +812,880 @@ function shouldShowErrors(field) {
768
812
  * ```
769
813
  */
770
814
  function setupMetaTracking(elementRef, meta, options) {
771
- let appliedAttributes = new Set();
772
- let previousMeta;
773
- let previousDeps;
774
- afterRenderEffect({
775
- write: () => {
776
- // Read dependents to establish signal tracking and collect their values
777
- const currentDeps = options?.dependents?.map((dep) => dep());
778
- const currentMeta = meta();
779
- const hostElement = elementRef.nativeElement;
780
- // Early exit if nothing changed (using deep equality for meta)
781
- const metaChanged = !isEqual(currentMeta, previousMeta);
782
- const depsChanged = !previousDeps || currentDeps?.length !== previousDeps.length || currentDeps?.some((d, i) => d !== previousDeps[i]);
783
- if (!metaChanged && !depsChanged) {
815
+ applyTrackedAttrsToTargets(elementRef, () => meta(), { selector: options?.selector, dependents: options?.dependents });
816
+ }
817
+ /**
818
+ * Marker-directive variant: writes parent meta + aria to resolved targets.
819
+ * Parent aria wins on key collision. Internal to NgForgeControl/HostControl.
820
+ * @internal
821
+ */
822
+ function setupMarkerAttrTracking(elementRef, parent, selector) {
823
+ applyTrackedAttrsToTargets(elementRef, () => {
824
+ const ariaRequired = parent.ariaRequired();
825
+ const ariaDescribedBy = parent.ariaDescribedBy();
826
+ // Skip null aria-required/describedby so applyMetaToElement removes the
827
+ // attr (rather than serializing "null") on transitions.
828
+ return {
829
+ ...(parent.meta() ?? {}),
830
+ 'aria-invalid': parent.ariaInvalid(),
831
+ ...(ariaRequired === null ? {} : { 'aria-required': ariaRequired }),
832
+ ...(ariaDescribedBy === null ? {} : { 'aria-describedby': ariaDescribedBy }),
833
+ };
834
+ }, { selector });
835
+ }
836
+
837
+ /**
838
+ * Creates a signal that computes the aria-describedby value based on errors and hint state.
839
+ * Errors take precedence over hints - when errors are displayed, the hint is hidden.
840
+ * Only the first error is displayed (single error ID, not indexed).
841
+ *
842
+ * @param errorsToDisplay Signal containing the array of errors currently being displayed
843
+ * @param errorId Signal containing the ID for the error element (single error only)
844
+ * @param hintId Signal containing the ID for the hint element
845
+ * @param hasHint Function that returns true if a hint is configured
846
+ * @returns Signal containing the aria-describedby value or null
847
+ */
848
+ function createAriaDescribedBySignal(errorsToDisplay, errorId, hintId, hasHint) {
849
+ return computed(() => {
850
+ if (errorsToDisplay().length > 0) {
851
+ return errorId();
852
+ }
853
+ if (hasHint()) {
854
+ return hintId();
855
+ }
856
+ return null;
857
+ });
858
+ }
859
+
860
+ // Error display utilities for field components
861
+
862
+ /**
863
+ * Base directive composed into every ng-forge component via `hostDirectives`.
864
+ * Owns the universal identity inputs (`key`, `className`) and the host
865
+ * bindings that every field shape — value-bearing, action, future container —
866
+ * shares: `[id]`, `[attr.data-testid]`, `[class]`.
867
+ *
868
+ * Shape-specific add-ons (`NgForgeField` for value fields, `NgForgeAction`
869
+ * for actions) inject this directive and read `key()` / `className()` from
870
+ * it. Source-of-truth concerns like `hidden` / `disabled` live on the add-on
871
+ * because they vary by shape (value fields derive from `field()()`; actions
872
+ * receive direct inputs).
873
+ *
874
+ * Selectorless — usage is exclusively via `hostDirectives`. Prefer the
875
+ * `NG_FORGE_FIELD` / `NG_FORGE_ACTION` presets rather than composing Shell
876
+ * directly.
877
+ */
878
+ class NgForgeFieldShell {
879
+ key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
880
+ className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
881
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeFieldShell, deps: [], target: i0.ɵɵFactoryTarget.Directive });
882
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeFieldShell, isStandalone: true, inputs: { key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "key()", "attr.data-testid": "key()", "class": "className()" } }, ngImport: i0 });
883
+ }
884
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeFieldShell, decorators: [{
885
+ type: Directive,
886
+ args: [{
887
+ host: {
888
+ '[id]': 'key()',
889
+ '[attr.data-testid]': 'key()',
890
+ '[class]': 'className()',
891
+ },
892
+ }]
893
+ }], propDecorators: { key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }] } });
894
+ /** Input names forwarded onto `NgForgeFieldShell` via `hostDirectives`. */
895
+ const NG_FORGE_FIELD_SHELL_INPUTS = ['key', 'className'];
896
+
897
+ /**
898
+ * Value-bearing add-on directive — composed alongside `NgForgeFieldShell` via
899
+ * the `NG_FORGE_FIELD` preset. Owns the value/validation/aria plumbing every
900
+ * value-bearing field needs.
901
+ *
902
+ * The Shell owns universal identity (`key`, `className`, `[id]`,
903
+ * `[attr.data-testid]`, `[class]`); this directive injects Shell and adds
904
+ * field-driven bindings (`[attr.hidden]`, `[attr.aria-disabled]`), the
905
+ * standard value-field inputs (`field`, `label`, `placeholder`, `tabIndex`,
906
+ * `props`, `meta`, `validationMessages`), the derived error/aria signals,
907
+ * and the meta-tracking claim contract.
908
+ *
909
+ * @example
910
+ * ```ts
911
+ * \@Component({
912
+ * hostDirectives: NG_FORGE_FIELD,
913
+ * imports: [NgForgeControl, FormField],
914
+ * template: `
915
+ * <label [for]="ngf.key() + '-input'">{{ ngf.label() | dynamicText | async }}</label>
916
+ * <input ngForgeControl [id]="ngf.key() + '-input'" [formField]="ngf.field()" />
917
+ * @if (ngf.errorsToDisplay()[0]; as e) {
918
+ * <span [id]="ngf.errorId()">{{ e.message }}</span>
919
+ * }
920
+ * `,
921
+ * })
922
+ * export class MyInputField {
923
+ * protected readonly ngf = injectNgForgeField<string>();
924
+ * }
925
+ * ```
926
+ *
927
+ * Selectorless — usage is exclusively via `hostDirectives`. Prefer the
928
+ * `NG_FORGE_FIELD` preset over composing this directive manually.
929
+ */
930
+ class NgForgeField {
931
+ // ───────────────────────────────────────────────────────────────────────────
932
+ // Shell-owned identity — re-exported so component templates keep using
933
+ // `ngf.key()` / `ngf.className()` without reaching for the Shell directly.
934
+ // ───────────────────────────────────────────────────────────────────────────
935
+ shell = inject(NgForgeFieldShell);
936
+ /** Shell-owned key, re-exported as plain `Signal<string>` so it doesn't get flagged by the host-directive input-lockstep assertion. */
937
+ key = this.shell.key;
938
+ /** Shell-owned className, re-exported as plain `Signal<string>` for the same reason. */
939
+ className = this.shell.className;
940
+ // ───────────────────────────────────────────────────────────────────────────
941
+ // Forwarded inputs — the value-field-specific subset of mapper output.
942
+ // ───────────────────────────────────────────────────────────────────────────
943
+ field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
944
+ label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
945
+ placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
946
+ tabIndex = input(...(ngDevMode ? [undefined, { debugName: "tabIndex" }] : /* istanbul ignore next */ []));
947
+ props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : /* istanbul ignore next */ []));
948
+ meta = input(...(ngDevMode ? [undefined, { debugName: "meta" }] : /* istanbul ignore next */ []));
949
+ validationMessages = input(...(ngDevMode ? [undefined, { debugName: "validationMessages" }] : /* istanbul ignore next */ []));
950
+ // ───────────────────────────────────────────────────────────────────────────
951
+ // Wiring
952
+ // ───────────────────────────────────────────────────────────────────────────
953
+ defaultValidationMessages = inject(DEFAULT_VALIDATION_MESSAGES, { optional: true }) ?? signal(undefined);
954
+ // ───────────────────────────────────────────────────────────────────────────
955
+ // Error display
956
+ // ───────────────────────────────────────────────────────────────────────────
957
+ errors = createResolvedErrorsSignal(this.field, this.validationMessages, this.defaultValidationMessages);
958
+ showErrors = shouldShowErrors(this.field);
959
+ errorsToDisplay = computed(() => (this.showErrors() ? this.errors() : []), ...(ngDevMode ? [{ debugName: "errorsToDisplay" }] : /* istanbul ignore next */ []));
960
+ // ───────────────────────────────────────────────────────────────────────────
961
+ // Accessibility helpers
962
+ // ───────────────────────────────────────────────────────────────────────────
963
+ errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
964
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
965
+ ariaInvalid = computed(() => {
966
+ const state = this.field()();
967
+ return state.invalid() && state.touched();
968
+ }, ...(ngDevMode ? [{ debugName: "ariaInvalid" }] : /* istanbul ignore next */ []));
969
+ ariaRequired = computed(() => {
970
+ return this.field()().required?.() === true ? true : null;
971
+ }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : /* istanbul ignore next */ []));
972
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => Boolean(this.props()?.hint));
973
+ // ───────────────────────────────────────────────────────────────────────────
974
+ // Claim tracking — markers and ambient sub-components flip this to true so
975
+ // dev-mode can warn when meta() is non-empty but nothing consumed it.
976
+ // ───────────────────────────────────────────────────────────────────────────
977
+ _claimed = signal(false, ...(ngDevMode ? [{ debugName: "_claimed" }] : /* istanbul ignore next */ []));
978
+ /** @internal Marker directives + ambient sub-components call this on construction. */
979
+ markClaimed() {
980
+ this._claimed.set(true);
981
+ }
982
+ logger = inject(DynamicFormLogger, { optional: true });
983
+ constructor() {
984
+ // Dev-mode safety net: a mapper or template-author binds meta() but no
985
+ // NgForgeControl / NgForgeHostControl / ambient sub-component is in
986
+ // place to consume it. Fires at most once per directive instance.
987
+ if (isDevMode()) {
988
+ let warned = false;
989
+ afterRenderEffect({
990
+ write: () => {
991
+ if (warned)
992
+ return;
993
+ const meta = this.meta();
994
+ if (!meta || Object.keys(meta).length === 0)
995
+ return;
996
+ if (untracked(this._claimed))
997
+ return;
998
+ warned = true;
999
+ // `untracked` safe — afterRenderEffect.write runs after first CD, so Shell's required `key` is bound (no NG0950).
1000
+ const key = untracked(this.key);
1001
+ const message = `NgForgeField - meta() provided for field "${key}" but no NgForgeControl / NgForgeHostControl / ` +
1002
+ `ambient sub-component claimed it. Meta will not be applied. ` +
1003
+ `See https://ng-forge.dev/building-an-adapter#meta-forwarding`;
1004
+ if (this.logger) {
1005
+ this.logger.warn(message);
1006
+ }
1007
+ else {
1008
+ console.warn(message);
1009
+ }
1010
+ },
1011
+ });
1012
+ }
1013
+ }
1014
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1015
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeField, isStandalone: true, inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.hidden": "field()().hidden() || null", "attr.aria-disabled": "field()().disabled() || null" } }, ngImport: i0 });
1016
+ }
1017
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeField, decorators: [{
1018
+ type: Directive,
1019
+ args: [{
1020
+ host: {
1021
+ '[attr.hidden]': 'field()().hidden() || null',
1022
+ '[attr.aria-disabled]': 'field()().disabled() || null',
1023
+ },
1024
+ }]
1025
+ }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], validationMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationMessages", required: false }] }] } });
1026
+ /**
1027
+ * Inputs forwarded onto `NgForgeField` via `hostDirectives`. Excludes
1028
+ * `key` / `className` (forwarded to `NgForgeFieldShell` separately via
1029
+ * `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this
1030
+ * directly — use the `NG_FORGE_FIELD` preset.
1031
+ */
1032
+ const NG_FORGE_VALUE_FIELD_INPUTS = ['field', 'label', 'placeholder', 'tabIndex', 'props', 'meta', 'validationMessages'];
1033
+ // Compile-time lockstep: NG_FORGE_VALUE_FIELD_INPUTS must equal the declared
1034
+ // `input()` properties on NgForgeField. Drift in either direction fails the build.
1035
+ const _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP = true;
1036
+ void _NG_FORGE_VALUE_FIELD_INPUTS_LOCKSTEP;
1037
+ /**
1038
+ * Typed wrapper around `inject(NgForgeField)`. The generic narrows the
1039
+ * `field` signal to `Signal<FieldTree<T>>` for the calling component's value
1040
+ * type. The cast is unchecked — the runtime contract is that mappers only
1041
+ * forward a `field` whose value type matches the field-type definition.
1042
+ *
1043
+ * @example
1044
+ * ```ts
1045
+ * protected readonly ngf = injectNgForgeField<string>();
1046
+ * // ngf.field() is Signal<FieldTree<string>>; ngf.errors() etc. unchanged
1047
+ * ```
1048
+ */
1049
+ function injectNgForgeField() {
1050
+ return inject(NgForgeField);
1051
+ }
1052
+
1053
+ /**
1054
+ * Action add-on directive — composed alongside `NgForgeFieldShell` via the
1055
+ * `NG_FORGE_ACTION` preset. Owns the inputs + dispatch logic every button /
1056
+ * action component needs (label, disabled, event, eventArgs, …) plus the
1057
+ * `[attr.hidden]` / `[attr.aria-disabled]` host bindings driven by its own
1058
+ * inputs (no field tree).
1059
+ *
1060
+ * Components call `action.dispatch()` from their click handler; this
1061
+ * directive resolves any `eventArgs` tokens via the optional `ARRAY_CONTEXT`
1062
+ * and dispatches the configured `event` through the `EventBus`.
1063
+ *
1064
+ * @example
1065
+ * ```ts
1066
+ * \@Component({
1067
+ * hostDirectives: NG_FORGE_ACTION,
1068
+ * template: `<button (click)="action.dispatch()" [disabled]="action.disabled()">{{ action.label() }}</button>`,
1069
+ * })
1070
+ * export class MyButton {
1071
+ * protected readonly action = inject(NgForgeAction);
1072
+ * }
1073
+ * ```
1074
+ *
1075
+ * Selectorless — usage is exclusively via `hostDirectives`. Prefer the
1076
+ * `NG_FORGE_ACTION` preset over composing this directive manually.
1077
+ */
1078
+ class NgForgeAction {
1079
+ // ───────────────────────────────────────────────────────────────────────────
1080
+ // Shell-owned identity — re-exported as plain `Signal<T>` so they don't get
1081
+ // flagged by the lockstep assertion below.
1082
+ // ───────────────────────────────────────────────────────────────────────────
1083
+ shell = inject(NgForgeFieldShell);
1084
+ key = this.shell.key;
1085
+ className = this.shell.className;
1086
+ // ───────────────────────────────────────────────────────────────────────────
1087
+ // Forwarded inputs — the action-specific subset of mapper output.
1088
+ // ───────────────────────────────────────────────────────────────────────────
1089
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1090
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1091
+ hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
1092
+ tabIndex = input(...(ngDevMode ? [undefined, { debugName: "tabIndex" }] : /* istanbul ignore next */ []));
1093
+ event = input(...(ngDevMode ? [undefined, { debugName: "event" }] : /* istanbul ignore next */ []));
1094
+ eventArgs = input(...(ngDevMode ? [undefined, { debugName: "eventArgs" }] : /* istanbul ignore next */ []));
1095
+ eventContext = input(...(ngDevMode ? [undefined, { debugName: "eventContext" }] : /* istanbul ignore next */ []));
1096
+ props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : /* istanbul ignore next */ []));
1097
+ // ───────────────────────────────────────────────────────────────────────────
1098
+ // Dispatch
1099
+ // ───────────────────────────────────────────────────────────────────────────
1100
+ eventBus = inject(EventBus);
1101
+ arrayContext = inject(ARRAY_CONTEXT, { optional: true });
1102
+ /**
1103
+ * Dispatch the configured event through the EventBus. Resolves `eventArgs`
1104
+ * tokens (`$key`, `$index`, `$arrayKey`, `formValue`, …) against the
1105
+ * ambient array context if present, falling back to a static `eventContext`
1106
+ * input or the field's own `key` otherwise. No-op when `event` is unset
1107
+ * (e.g. submit buttons handled by the native form lifecycle).
1108
+ */
1109
+ dispatch() {
1110
+ const event = this.event();
1111
+ if (!event)
1112
+ return;
1113
+ const args = this.eventArgs();
1114
+ if (!args || args.length === 0) {
1115
+ this.eventBus.dispatch(event);
1116
+ return;
1117
+ }
1118
+ const context = this.arrayContext
1119
+ ? {
1120
+ key: this.key(),
1121
+ index: this.arrayContext.index(),
1122
+ arrayKey: this.arrayContext.arrayKey,
1123
+ formValue: this.arrayContext.formValue,
1124
+ }
1125
+ : (this.eventContext() ?? { key: this.key() });
1126
+ this.eventBus.dispatch(event, ...resolveTokens(args, context));
1127
+ }
1128
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1129
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeAction, isStandalone: true, inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, event: { classPropertyName: "event", publicName: "event", isSignal: true, isRequired: false, transformFunction: null }, eventArgs: { classPropertyName: "eventArgs", publicName: "eventArgs", isSignal: true, isRequired: false, transformFunction: null }, eventContext: { classPropertyName: "eventContext", publicName: "eventContext", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.hidden": "hidden() || null", "attr.aria-disabled": "disabled() || null" } }, ngImport: i0 });
1130
+ }
1131
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAction, decorators: [{
1132
+ type: Directive,
1133
+ args: [{
1134
+ host: {
1135
+ '[attr.hidden]': 'hidden() || null',
1136
+ '[attr.aria-disabled]': 'disabled() || null',
1137
+ },
1138
+ }]
1139
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], event: [{ type: i0.Input, args: [{ isSignal: true, alias: "event", required: false }] }], eventArgs: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventArgs", required: false }] }], eventContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventContext", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "props", required: false }] }] } });
1140
+ /**
1141
+ * Inputs forwarded onto `NgForgeAction` via `hostDirectives`. Excludes
1142
+ * `key` / `className` (forwarded to `NgForgeFieldShell` separately via
1143
+ * `NG_FORGE_FIELD_SHELL_INPUTS`). Most consumers shouldn't reference this
1144
+ * directly — use the `NG_FORGE_ACTION` preset.
1145
+ */
1146
+ const NG_FORGE_ACTION_INPUTS = ['label', 'disabled', 'hidden', 'tabIndex', 'event', 'eventArgs', 'eventContext', 'props'];
1147
+ // Compile-time lockstep: NG_FORGE_ACTION_INPUTS must equal the declared
1148
+ // `input()` properties on NgForgeAction. Drift fails the build.
1149
+ const _NG_FORGE_ACTION_INPUTS_LOCKSTEP = true;
1150
+ void _NG_FORGE_ACTION_INPUTS_LOCKSTEP;
1151
+ /**
1152
+ * Typed wrapper around `inject(NgForgeAction)`. The generic narrows the
1153
+ * `event` input to a specific FormEvent subclass.
1154
+ *
1155
+ * @example
1156
+ * ```ts
1157
+ * protected readonly action = injectNgForgeAction<SubmitEvent>();
1158
+ * ```
1159
+ */
1160
+ function injectNgForgeAction() {
1161
+ return inject(NgForgeAction);
1162
+ }
1163
+
1164
+ /**
1165
+ * Universal addon-array plumbing: signal-cached `hidden` resolution and
1166
+ * slot bucketing. Used internally by the `NgForgeAddons` host wrapper —
1167
+ * consumers should compose `NgForgeAddons` directly and inject the
1168
+ * directive instance via `injectNgForgeAddons<TAddon>()`.
1169
+ */
1170
+ class NgForgeAddonsBase {
1171
+ /** Capture the host's injector so `toSignal` (inside `resolveDynamicValue`) ties to the host's `DestroyRef`. */
1172
+ hostInjector = inject(Injector);
1173
+ /** Addon array mapped from `FieldDef.addons` by `buildBaseInputs`. */
1174
+ addons = input(undefined, ...(ngDevMode ? [{ debugName: "addons" }] : /* istanbul ignore next */ []));
1175
+ /**
1176
+ * Per-addon `hidden` signal cache. Built via `explicitEffect` rather
1177
+ * than `computed` because `toSignal` (inside `resolveDynamicValue` for
1178
+ * Observable inputs) is disallowed in reactive contexts (NG0602).
1179
+ */
1180
+ _hiddenSignalCache = signal(new Map(), ...(ngDevMode ? [{ debugName: "_hiddenSignalCache" }] : /* istanbul ignore next */ []));
1181
+ hiddenSignalCache = this._hiddenSignalCache.asReadonly();
1182
+ visibleAddons = computed(() => {
1183
+ const list = this.addons() ?? [];
1184
+ const cache = this.hiddenSignalCache();
1185
+ return list.filter((a) => !(cache.get(a)?.() ?? false));
1186
+ }, ...(ngDevMode ? [{ debugName: "visibleAddons" }] : /* istanbul ignore next */ []));
1187
+ hasAddons = computed(() => this.visibleAddons().length > 0, ...(ngDevMode ? [{ debugName: "hasAddons" }] : /* istanbul ignore next */ []));
1188
+ prefixAddons = computed(() => this.visibleAddons().filter((a) => a.slot === 'prefix'), ...(ngDevMode ? [{ debugName: "prefixAddons" }] : /* istanbul ignore next */ []));
1189
+ suffixAddons = computed(() => this.visibleAddons().filter((a) => a.slot === 'suffix'), ...(ngDevMode ? [{ debugName: "suffixAddons" }] : /* istanbul ignore next */ []));
1190
+ /**
1191
+ * Visible addons grouped by their `slot` string. Convenience view for
1192
+ * adapters with non-universal slots (e.g., Ionic `start` / `end`).
1193
+ */
1194
+ addonsBySlot = computed(() => {
1195
+ const map = new Map();
1196
+ for (const a of this.visibleAddons()) {
1197
+ const bucket = map.get(a.slot) ?? [];
1198
+ bucket.push(a);
1199
+ map.set(a.slot, bucket);
1200
+ }
1201
+ return map;
1202
+ }, ...(ngDevMode ? [{ debugName: "addonsBySlot" }] : /* istanbul ignore next */ []));
1203
+ /** Secondary index keyed by `hidden` value identity. Catches the common case
1204
+ * where a FormConfig re-emit produces structurally-different-but-content-
1205
+ * identical addon objects — without this, every re-emit would spawn a new
1206
+ * `toSignal` subscription for each addon's hidden axis. */
1207
+ _hiddenByValue = new WeakMap();
1208
+ constructor() {
1209
+ explicitEffect([this.addons], ([list]) => {
1210
+ const prev = this._hiddenSignalCache();
1211
+ const next = new Map();
1212
+ for (const a of list ?? []) {
1213
+ // Tier 1: reference identity — the cheapest hit when the same addon
1214
+ // object is reused across emissions.
1215
+ let resolved = prev.get(a);
1216
+ if (!resolved) {
1217
+ // Tier 2: hidden-value identity — when `hidden` is an object
1218
+ // (Signal / Observable / function), the same instance can be passed
1219
+ // through multiple new addon objects on FormConfig re-emit.
1220
+ const h = a.hidden;
1221
+ const valueKey = typeof h === 'object' && h !== null ? h : null;
1222
+ resolved = valueKey ? this._hiddenByValue.get(valueKey) : undefined;
1223
+ if (!resolved) {
1224
+ resolved = resolveDynamicValue(a.hidden, false, this.hostInjector);
1225
+ if (valueKey)
1226
+ this._hiddenByValue.set(valueKey, resolved);
1227
+ }
1228
+ }
1229
+ next.set(a, resolved);
1230
+ }
1231
+ this._hiddenSignalCache.set(next);
1232
+ });
1233
+ }
1234
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonsBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1235
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeAddonsBase, isStandalone: true, inputs: { addons: { classPropertyName: "addons", publicName: "addons", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
1236
+ }
1237
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonsBase, decorators: [{
1238
+ type: Directive,
1239
+ args: [{}]
1240
+ }], ctorParameters: () => [], propDecorators: { addons: [{ type: i0.Input, args: [{ isSignal: true, alias: "addons", required: false }] }] } });
1241
+ /** Forwarded onto `NgForgeAddonsBase` via the `NgForgeAddons` wrapper. */
1242
+ const NG_FORGE_ADDONS_INPUTS = ['addons'];
1243
+ // Compile-time lockstep — drift between the tuple and the declared inputs fails the build.
1244
+ const _NG_FORGE_ADDONS_INPUTS_LOCKSTEP = true;
1245
+ void _NG_FORGE_ADDONS_INPUTS_LOCKSTEP;
1246
+ /**
1247
+ * Host wrapper for `NgForgeAddonsBase`. Consumers compose this directly:
1248
+ *
1249
+ * ```ts
1250
+ * \@Component({
1251
+ * hostDirectives: [NgForgeFieldHost, NgForgeAddons],
1252
+ * template: `
1253
+ * @if (ngfa.hasAddons()) {
1254
+ * <p-inputgroup>
1255
+ * @for (a of ngfa.prefixAddons(); track $index) {
1256
+ * <df-addon-slot [addon]="a" [hidden]="ngfa.hiddenSignalCache().get(a)" />
1257
+ * }
1258
+ * <input ngForgeControl ... />
1259
+ * </p-inputgroup>
1260
+ * }
1261
+ * `,
1262
+ * })
1263
+ * export class MyInputField {
1264
+ * protected readonly ngfa = injectNgForgeAddons<MyInputAddon>();
1265
+ * }
1266
+ * ```
1267
+ *
1268
+ * The wrapper exists so `hostDirectives: [NgForgeAddons]` resolves
1269
+ * cross-package — same constraint that drives `NgForgeFieldHost`.
1270
+ */
1271
+ class NgForgeAddons {
1272
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddons, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1273
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeAddons, isStandalone: true, hostDirectives: [{ directive: NgForgeAddonsBase, inputs: ["addons", "addons"] }], ngImport: i0 });
1274
+ }
1275
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddons, decorators: [{
1276
+ type: Directive,
1277
+ args: [{
1278
+ hostDirectives: [{ directive: NgForgeAddonsBase, inputs: [...NG_FORGE_ADDONS_INPUTS] }],
1279
+ }]
1280
+ }] });
1281
+ /**
1282
+ * Typed inject helper. Always resolves to the underlying
1283
+ * `NgForgeAddonsBase` instance — the wrapper class has no instance
1284
+ * fields of its own.
1285
+ */
1286
+ function injectNgForgeAddons() {
1287
+ return inject(NgForgeAddonsBase);
1288
+ }
1289
+
1290
+ const ADDON_PRESET_HANDLER = new InjectionToken('ADDON_PRESET_HANDLER');
1291
+
1292
+ /**
1293
+ * Universal click-dispatch plumbing for action-style addons (icon buttons,
1294
+ * labelled buttons, etc.): `disabled` / `loading` resolution from
1295
+ * `DynamicValue<boolean>`, action context construction, and the
1296
+ * `preset / actionRef / action` precedence dispatcher.
1297
+ *
1298
+ * Adapter button kinds (e.g., `prime-button`) compose `NgForgeAddonAction`
1299
+ * and read state via `injectNgForgeAddonAction<TAddon>()`. Preset semantics
1300
+ * are adapter-specific — register an `ADDON_PRESET_HANDLER` at the host
1301
+ * field scope to receive preset dispatches.
1302
+ */
1303
+ class NgForgeAddonActionBase {
1304
+ // The action registry is form-scoped. `optional: true` matches the
1305
+ // preset handler so addons rendered outside a form that called
1306
+ // `provideAddonActions(...)` degrade gracefully instead of throwing —
1307
+ // `actionRef` dispatches then log an actionable warning and no-op.
1308
+ actionRegistry = inject(ADDON_ACTION_REGISTRY, { optional: true });
1309
+ presetHandler = inject(ADDON_PRESET_HANDLER, { optional: true });
1310
+ logger = inject(DynamicFormLogger);
1311
+ hostInjector = inject(Injector);
1312
+ addon = input.required(...(ngDevMode ? [{ debugName: "addon" }] : /* istanbul ignore next */ []));
1313
+ fieldInputs = input(undefined, ...(ngDevMode ? [{ debugName: "fieldInputs" }] : /* istanbul ignore next */ []));
1314
+ /**
1315
+ * Per-value caches for `disabled` / `loading` signal resolution. Keyed by
1316
+ * the `DynamicValue` identity so a single `toSignal` subscription is
1317
+ * shared across addon-reference swaps that pass the same Observable.
1318
+ * Mirrors `NgForgeAddonsBase._hiddenByValue` — `resolveDynamicValue`
1319
+ * cannot run inside `computed`/`linkedSignal.computation` because
1320
+ * `toSignal` throws in reactive contexts (NG0602), and re-running it on
1321
+ * every recomputation would leak subscriptions on each cycle.
1322
+ */
1323
+ _disabledByValue = new WeakMap();
1324
+ _loadingByValue = new WeakMap();
1325
+ _resolvedDisabled = signal(undefined, ...(ngDevMode ? [{ debugName: "_resolvedDisabled" }] : /* istanbul ignore next */ []));
1326
+ _resolvedLoading = signal(undefined, ...(ngDevMode ? [{ debugName: "_resolvedLoading" }] : /* istanbul ignore next */ []));
1327
+ disabled = computed(() => this._resolvedDisabled()?.() ?? false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1328
+ loading = computed(() => this._resolvedLoading()?.() ?? false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1329
+ constructor() {
1330
+ explicitEffect([this.addon], ([addon]) => {
1331
+ this._resolvedDisabled.set(this._resolveAxis(addon.disabled, this._disabledByValue));
1332
+ this._resolvedLoading.set(this._resolveAxis(addon.loading, this._loadingByValue));
1333
+ });
1334
+ }
1335
+ _resolveAxis(value, cache) {
1336
+ const valueKey = typeof value === 'object' && value !== null ? value : null;
1337
+ let resolved = valueKey ? cache.get(valueKey) : undefined;
1338
+ if (!resolved) {
1339
+ resolved = resolveDynamicValue(value, false, this.hostInjector);
1340
+ if (valueKey)
1341
+ cache.set(valueKey, resolved);
1342
+ }
1343
+ return resolved;
1344
+ }
1345
+ /**
1346
+ * Resolve the click handler from the addon's three-axis click variant and
1347
+ * invoke it. Precedence on multi-set: `preset > actionRef > action`. The
1348
+ * validator drops multi-set configs at config-init time; this is
1349
+ * defence-in-depth + a logged warning so the misconfiguration is loud.
1350
+ */
1351
+ dispatch() {
1352
+ const addon = this.addon();
1353
+ const ctx = this.buildActionContext();
1354
+ const variantsSet = [addon.preset, addon.actionRef, addon.action].filter((v) => v !== undefined).length;
1355
+ if (variantsSet > 1) {
1356
+ this.logger.warn(`Addon button: more than one of preset/actionRef/action configured — dispatching by precedence (preset > actionRef > action). Ship at most one to silence this.`);
1357
+ }
1358
+ if (addon.preset !== undefined) {
1359
+ if (!this.presetHandler) {
1360
+ this.logger.warn(`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?`);
784
1361
  return;
785
1362
  }
786
- previousMeta = currentMeta;
787
- previousDeps = currentDeps;
788
- if (options?.selector) {
789
- // Apply to elements matching selector
790
- const elements = hostElement.querySelectorAll(options.selector);
791
- const allApplied = new Set();
792
- elements.forEach((el) => {
793
- const applied = applyMetaToElement(el, currentMeta, appliedAttributes);
794
- applied.forEach((attr) => allApplied.add(attr));
795
- });
796
- appliedAttributes = allApplied;
1363
+ void this.presetHandler.run(addon.preset, ctx);
1364
+ return;
1365
+ }
1366
+ if (addon.actionRef !== undefined) {
1367
+ const handler = this.actionRegistry?.get(addon.actionRef);
1368
+ if (handler) {
1369
+ handler(ctx);
797
1370
  }
798
1371
  else {
799
- // Apply to host element (for Shadow DOM components)
800
- appliedAttributes = applyMetaToElement(hostElement, currentMeta, appliedAttributes);
1372
+ this.logger.warn(`Addon button: actionRef '${addon.actionRef}' is not registered. ` +
1373
+ (this.actionRegistry
1374
+ ? `Did you call provideAddonActions({ ${addon.actionRef}: ... })?`
1375
+ : `No provideAddonActions(...) registry is reachable in this scope — was the addon rendered outside a form?`));
801
1376
  }
802
- },
803
- });
1377
+ return;
1378
+ }
1379
+ if (addon.action !== undefined) {
1380
+ addon.action(ctx);
1381
+ return;
1382
+ }
1383
+ // Decorative — no handler configured.
1384
+ }
1385
+ /**
1386
+ * Build an `AddonActionContext` for handlers from the wrapper-style
1387
+ * `fieldInputs` bag. Returns the field-bound variant when a host field
1388
+ * is reachable, or the orphan variant otherwise.
1389
+ *
1390
+ * Resolution order for `setValue`:
1391
+ * 1. `inputs.setValue` from the forwarded bag (preferred — populated by
1392
+ * `buildFieldInputs` in `DfFieldOutlet`).
1393
+ * 2. Fallback reconstructed from `inputs.field` itself: although
1394
+ * `ReadonlyFieldTree.value` is type-narrowed to `Signal<T>`, the
1395
+ * underlying runtime object is the same `WritableSignal<T>` created
1396
+ * by Signal Forms (the read-only wrapping is type-level only, see
1397
+ * `toReadonlyFieldTree`). Casting back at the writer boundary
1398
+ * produces a working setValue for ANY field depth — top-level OR
1399
+ * nested under groups. This avoids the previous FSC-key navigation
1400
+ * that broke for grouped fields, where `inputs.key` is DOM-mangled
1401
+ * (`'user_address_street'`) and `fsc.form[key]` returns undefined.
1402
+ *
1403
+ * The orphan variant fires only when the bag carries no field tree at
1404
+ * all — genuine "rendered outside a field" scenario.
1405
+ */
1406
+ buildActionContext() {
1407
+ const inputs = this.fieldInputs();
1408
+ const key = inputs?.key ?? '';
1409
+ const type = inputs?.type ?? '';
1410
+ const tree = inputs?.field;
1411
+ if (tree) {
1412
+ // `writeToFieldValue` centralises the `Signal → WritableSignal` cast and
1413
+ // warns at runtime if Signal Forms ever stops storing a writable signal
1414
+ // here (the alternative — a silent no-op preset — is much worse).
1415
+ const setValue = inputs?.setValue ?? ((next) => writeToFieldValue(tree.value, next, this.logger));
1416
+ return {
1417
+ field: { key, type },
1418
+ form: tree,
1419
+ value: tree.value() ?? undefined,
1420
+ setValue,
1421
+ };
1422
+ }
1423
+ return {
1424
+ field: { key, type },
1425
+ form: null,
1426
+ value: undefined,
1427
+ };
1428
+ }
1429
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonActionBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1430
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeAddonActionBase, isStandalone: true, inputs: { addon: { classPropertyName: "addon", publicName: "addon", isSignal: true, isRequired: true, transformFunction: null }, fieldInputs: { classPropertyName: "fieldInputs", publicName: "fieldInputs", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
1431
+ }
1432
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonActionBase, decorators: [{
1433
+ type: Directive,
1434
+ args: [{}]
1435
+ }], ctorParameters: () => [], propDecorators: { addon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addon", required: true }] }], fieldInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldInputs", required: false }] }] } });
1436
+ /** Forwarded onto `NgForgeAddonActionBase` via the `NgForgeAddonAction` wrapper. */
1437
+ const NG_FORGE_ADDON_ACTION_INPUTS = ['addon', 'fieldInputs'];
1438
+ const _NG_FORGE_ADDON_ACTION_INPUTS_LOCKSTEP = true;
1439
+ void _NG_FORGE_ADDON_ACTION_INPUTS_LOCKSTEP;
1440
+ /**
1441
+ * Host wrapper for `NgForgeAddonActionBase`. Consumers compose this directly:
1442
+ *
1443
+ * ```ts
1444
+ * \@Component({
1445
+ * hostDirectives: [NgForgeAddonAction],
1446
+ * template: `
1447
+ * <p-button [loading]="action.loading()" [disabled]="action.disabled()"
1448
+ * (onClick)="action.dispatch()" />
1449
+ * `,
1450
+ * })
1451
+ * export class MyButtonAddon {
1452
+ * protected readonly action = injectNgForgeAddonAction<MyButtonAddon>();
1453
+ * }
1454
+ * ```
1455
+ */
1456
+ class NgForgeAddonAction {
1457
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1458
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeAddonAction, isStandalone: true, hostDirectives: [{ directive: NgForgeAddonActionBase, inputs: ["addon", "addon", "fieldInputs", "fieldInputs"] }], ngImport: i0 });
1459
+ }
1460
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeAddonAction, decorators: [{
1461
+ type: Directive,
1462
+ args: [{
1463
+ hostDirectives: [{ directive: NgForgeAddonActionBase, inputs: [...NG_FORGE_ADDON_ACTION_INPUTS] }],
1464
+ }]
1465
+ }] });
1466
+ function injectNgForgeAddonAction() {
1467
+ return inject(NgForgeAddonActionBase);
804
1468
  }
805
1469
 
806
- // Error display utilities for field components
1470
+ /**
1471
+ * Marker directive applied to the canonical control element of an
1472
+ * `NgForgeField`-using component. Forwards the field's `meta` AND
1473
+ * parent-derived aria signals (`aria-invalid`, `aria-required`,
1474
+ * `aria-describedby`) onto a target element.
1475
+ *
1476
+ * By default attributes land on the directive's own host. For wrapped
1477
+ * controls (e.g. `<mat-checkbox>`, `<p-toggleSwitch>`) where the canonical
1478
+ * native input is rendered as a descendant inside the wrapper, pass a CSS
1479
+ * selector through the input alias — the directive caches the resolved
1480
+ * descendant set and re-resolves only when the selector changes.
1481
+ *
1482
+ * @example Bare native control
1483
+ * ```html
1484
+ * <input ngForgeControl [formField]="ngf.field()" />
1485
+ * ```
1486
+ *
1487
+ * @example Wrapped control with inner native input
1488
+ * ```html
1489
+ * <mat-checkbox ngForgeControl="input[type='checkbox']" [formField]="ngf.field()">
1490
+ * {{ ngf.label() }}
1491
+ * </mat-checkbox>
1492
+ * ```
1493
+ *
1494
+ * @example Dynamic option list (radios rendered in @for)
1495
+ * ```html
1496
+ * @for (option of options(); track option.value) {
1497
+ * <input type="radio" ngForgeControl [value]="option.value" />
1498
+ * }
1499
+ * ```
1500
+ *
1501
+ * Each `@for` iteration creates its own directive instance; Angular's
1502
+ * structural lifecycle handles add/remove naturally.
1503
+ */
1504
+ class NgForgeControl {
1505
+ target = input(undefined, { ...(ngDevMode ? { debugName: "target" } : /* istanbul ignore next */ {}), alias: 'ngForgeControl' });
1506
+ constructor() {
1507
+ const parent = inject(NgForgeField);
1508
+ setupMarkerAttrTracking(inject(ElementRef), parent, () => this.target());
1509
+ parent.markClaimed();
1510
+ }
1511
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1512
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: NgForgeControl, isStandalone: true, selector: "[ngForgeControl]", inputs: { target: { classPropertyName: "target", publicName: "ngForgeControl", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
1513
+ }
1514
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeControl, decorators: [{
1515
+ type: Directive,
1516
+ args: [{ selector: '[ngForgeControl]' }]
1517
+ }], ctorParameters: () => [], propDecorators: { target: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngForgeControl", required: false }] }] } });
1518
+ /**
1519
+ * Selectorless companion directive for shadow-DOM-encapsulated components
1520
+ * (PrimeNG `p-select`, Ionic web components, etc.) where no light-DOM child
1521
+ * is the canonical control. Added to the component's `hostDirectives` so the
1522
+ * field's `meta` + parent aria land on the component's own host element.
1523
+ *
1524
+ * For split parent/control components (e.g. `df-prime-select` rendering
1525
+ * `df-prime-select-control` internally), place `NgForgeHostControl` on the
1526
+ * INNER control component's `hostDirectives` — the inner element is the
1527
+ * canonical control, and the marker's `inject(NgForgeField)` walks up the
1528
+ * element-injector tree to find the parent's directive instance.
1529
+ *
1530
+ * @example
1531
+ * ```ts
1532
+ * \@Component({
1533
+ * hostDirectives: [
1534
+ * { directive: NgForgeField, inputs: [...NG_FORGE_FIELD_INPUTS] },
1535
+ * NgForgeHostControl,
1536
+ * ],
1537
+ * template: `...`,
1538
+ * })
1539
+ * export class IonicInputField { ... }
1540
+ * ```
1541
+ */
1542
+ // Empty `@Directive({})` is intentional — selectorless; used only via `hostDirectives`.
1543
+ class NgForgeHostControl {
1544
+ constructor() {
1545
+ const parent = inject(NgForgeField);
1546
+ setupMarkerAttrTracking(inject(ElementRef), parent, () => undefined);
1547
+ parent.markClaimed();
1548
+ }
1549
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeHostControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1550
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeHostControl, isStandalone: true, ngImport: i0 });
1551
+ }
1552
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeHostControl, decorators: [{
1553
+ type: Directive,
1554
+ args: [{}]
1555
+ }], ctorParameters: () => [] });
1556
+
1557
+ /**
1558
+ * Value-bearing field host wrapper. Composes `NgForgeFieldShell` (universal
1559
+ * identity) with `NgForgeField` (value + validation + aria + meta-tracking)
1560
+ * in a single host-directive entry, so consumer components compose just one
1561
+ * directive instead of two.
1562
+ *
1563
+ * Why a directive instead of a `const` preset: cross-package partial
1564
+ * compilation can't resolve a const reference inside `hostDirectives:`, so
1565
+ * an exported const array doesn't work for library consumers. A wrapper
1566
+ * directive's own `hostDirectives` IS a literal at compile time (in this
1567
+ * file), so it works everywhere.
1568
+ *
1569
+ * Authoring shape for adapter components:
1570
+ * ```ts
1571
+ * \@Component({
1572
+ * hostDirectives: [NgForgeFieldHost],
1573
+ * ...
1574
+ * })
1575
+ * export class MyInputField {
1576
+ * protected readonly ngf = injectNgForgeField<string>();
1577
+ * }
1578
+ * ```
1579
+ *
1580
+ * Pattern reference: Taiga UI uses the same wrapper-directive technique
1581
+ * for the same reason. See https://www.angularspace.com/host-directives-decomposition-unleashed/
1582
+ */
1583
+ class NgForgeFieldHost {
1584
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeFieldHost, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1585
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeFieldHost, isStandalone: true, hostDirectives: [{ directive: NgForgeFieldShell, inputs: ["key", "key", "className", "className"] }, { directive: NgForgeField, inputs: ["field", "field", "label", "label", "placeholder", "placeholder", "tabIndex", "tabIndex", "props", "props", "meta", "meta", "validationMessages", "validationMessages"] }], ngImport: i0 });
1586
+ }
1587
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeFieldHost, decorators: [{
1588
+ type: Directive,
1589
+ args: [{
1590
+ hostDirectives: [
1591
+ { directive: NgForgeFieldShell, inputs: [...NG_FORGE_FIELD_SHELL_INPUTS] },
1592
+ { directive: NgForgeField, inputs: [...NG_FORGE_VALUE_FIELD_INPUTS] },
1593
+ ],
1594
+ }]
1595
+ }] });
1596
+ /**
1597
+ * Action / button host wrapper. Composes `NgForgeFieldShell` with
1598
+ * `NgForgeAction` (event dispatch + aria-disabled) for button-shaped
1599
+ * components.
1600
+ *
1601
+ * Authoring shape:
1602
+ * ```ts
1603
+ * \@Component({
1604
+ * hostDirectives: [NgForgeActionHost],
1605
+ * ...
1606
+ * })
1607
+ * export class MyButton {
1608
+ * protected readonly action = injectNgForgeAction();
1609
+ * }
1610
+ * ```
1611
+ */
1612
+ class NgForgeActionHost {
1613
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeActionHost, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1614
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NgForgeActionHost, isStandalone: true, hostDirectives: [{ directive: NgForgeFieldShell, inputs: ["key", "key", "className", "className"] }, { directive: NgForgeAction, inputs: ["label", "label", "disabled", "disabled", "hidden", "hidden", "tabIndex", "tabIndex", "event", "event", "eventArgs", "eventArgs", "eventContext", "eventContext", "props", "props"] }], ngImport: i0 });
1615
+ }
1616
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgForgeActionHost, decorators: [{
1617
+ type: Directive,
1618
+ args: [{
1619
+ hostDirectives: [
1620
+ { directive: NgForgeFieldShell, inputs: [...NG_FORGE_FIELD_SHELL_INPUTS] },
1621
+ { directive: NgForgeAction, inputs: [...NG_FORGE_ACTION_INPUTS] },
1622
+ ],
1623
+ }]
1624
+ }] });
1625
+
1626
+ /**
1627
+ * Builds a `ComponentFixture` for an `NgForgeField`-composing component with
1628
+ * `field` + `key` bound BEFORE `detectChanges()` — sidesteps the NG0950 that
1629
+ * Shell's required `key` and Field's required `field` would otherwise throw.
1630
+ *
1631
+ * The caller drives `detectChanges()` themselves (so they can `setInput`
1632
+ * additional component-specific props in between).
1633
+ */
1634
+ function createNgForgeFieldFixture(component, options) {
1635
+ TestBed.configureTestingModule({ imports: [component], providers: [...(options.providers ?? [])] });
1636
+ const key = options.key;
1637
+ const rootValue = signal({ [key]: options.value }, ...(ngDevMode ? [{ debugName: "rootValue" }] : /* istanbul ignore next */ []));
1638
+ const injector = TestBed.inject(EnvironmentInjector);
1639
+ const schemaCallback = options.schema;
1640
+ const root = runInInjectionContext(injector, () => {
1641
+ if (!schemaCallback)
1642
+ return form(rootValue);
1643
+ return form(rootValue, schema((path) => {
1644
+ schemaCallback(path[key]);
1645
+ }));
1646
+ });
1647
+ const field = root[key];
1648
+ if (options.touched)
1649
+ field().markAsTouched();
1650
+ const fixture = TestBed.createComponent(component);
1651
+ fixture.componentRef.setInput('field', field);
1652
+ fixture.componentRef.setInput('key', key);
1653
+ if (options.inputs) {
1654
+ for (const [name, value] of Object.entries(options.inputs)) {
1655
+ fixture.componentRef.setInput(name, value);
1656
+ }
1657
+ }
1658
+ return { fixture, field, rootValue };
1659
+ }
1660
+ /**
1661
+ * Builds a `ComponentFixture` for an `NgForgeAction`-composing component with
1662
+ * `key` + `label` bound and `EventBus` provided. The caller calls
1663
+ * `detectChanges()`; the harness handles the NG0950-avoidance bindings.
1664
+ */
1665
+ function createNgForgeActionFixture(component, options) {
1666
+ TestBed.configureTestingModule({ imports: [component], providers: [EventBus, ...(options.providers ?? [])] });
1667
+ const fixture = TestBed.createComponent(component);
1668
+ fixture.componentRef.setInput('key', options.key);
1669
+ fixture.componentRef.setInput('label', options.label);
1670
+ if (options.event)
1671
+ fixture.componentRef.setInput('event', options.event);
1672
+ if (options.eventArgs)
1673
+ fixture.componentRef.setInput('eventArgs', options.eventArgs);
1674
+ if (options.disabled !== undefined)
1675
+ fixture.componentRef.setInput('disabled', options.disabled);
1676
+ if (options.hidden !== undefined)
1677
+ fixture.componentRef.setInput('hidden', options.hidden);
1678
+ if (options.inputs) {
1679
+ for (const [name, value] of Object.entries(options.inputs)) {
1680
+ fixture.componentRef.setInput(name, value);
1681
+ }
1682
+ }
1683
+ return { fixture, eventBus: TestBed.inject(EventBus) };
1684
+ }
1685
+ /** Provider for `DEFAULT_VALIDATION_MESSAGES` in field tests — supply the default-messages map and the directive resolves it from DI. */
1686
+ function provideTestValidationMessages(messages) {
1687
+ return { provide: DEFAULT_VALIDATION_MESSAGES, useValue: signal(messages) };
1688
+ }
807
1689
 
808
1690
  /**
809
1691
  * @ng-forge/dynamic-forms/integration
@@ -824,5 +1706,5 @@ function setupMetaTracking(elementRef, meta, options) {
824
1706
  * Generated bundle index. Do not edit.
825
1707
  */
826
1708
 
827
- export { addArrayItemButtonMapper, buildArrayButtonEventContext, buildValueFieldInputs, buttonFieldMapper, checkboxFieldMapper, createResolvedErrorsSignal, datepickerFieldMapper, insertArrayItemButtonMapper, nextButtonFieldMapper, optionsFieldMapper, popArrayItemButtonMapper, prependArrayItemButtonMapper, previousButtonFieldMapper, removeArrayItemButtonMapper, resolveArrayButtonContext, resolveArrayButtonEventArgs, resolveValueFieldContext, setupMetaTracking, shiftArrayItemButtonMapper, shouldShowErrors, submitButtonFieldMapper, valueFieldMapper };
1709
+ export { ADDON_PRESET_HANDLER, NG_FORGE_ACTION_INPUTS, NG_FORGE_ADDONS_INPUTS, NG_FORGE_ADDON_ACTION_INPUTS, NG_FORGE_FIELD_SHELL_INPUTS, NG_FORGE_VALUE_FIELD_INPUTS, NgForgeAction, NgForgeActionHost, NgForgeAddonAction, NgForgeAddonActionBase, NgForgeAddons, NgForgeAddonsBase, NgForgeControl, NgForgeField, NgForgeFieldHost, NgForgeFieldShell, NgForgeHostControl, addArrayItemButtonMapper, buildArrayButtonEventContext, buildValueFieldInputs, buttonFieldMapper, checkboxFieldMapper, createAriaDescribedBySignal, createNgForgeActionFixture, createNgForgeFieldFixture, createResolvedErrorsSignal, datepickerFieldMapper, injectNgForgeAction, injectNgForgeAddonAction, injectNgForgeAddons, injectNgForgeField, insertArrayItemButtonMapper, nextButtonFieldMapper, optionsFieldMapper, popArrayItemButtonMapper, prependArrayItemButtonMapper, previousButtonFieldMapper, provideTestValidationMessages, removeArrayItemButtonMapper, resolveArrayButtonContext, resolveArrayButtonEventArgs, resolveValueFieldContext, setupMetaTracking, shiftArrayItemButtonMapper, shouldShowErrors, submitButtonFieldMapper, valueFieldMapper };
828
1710
  //# sourceMappingURL=ng-forge-dynamic-forms-integration.mjs.map