@ng-forge/dynamic-forms 0.9.0-next.12 → 0.9.0-next.14

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 (31) hide show
  1. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs → ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs} +9 -9
  2. package/fesm2022/ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs.map +1 -0
  3. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs → ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs} +6 -6
  4. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-VHaBk_1l.mjs.map → ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs.map} +1 -1
  5. package/fesm2022/{ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs → ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs} +2 -2
  6. package/fesm2022/{ng-forge-dynamic-forms-container-utils-CGJiiKBY.mjs.map → ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs.map} +1 -1
  7. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs → ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs} +5 -5
  8. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-BOdw40ZF.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs.map} +1 -1
  9. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs → ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs} +2 -2
  10. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-D72mQxlX.mjs.map → ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs.map} +1 -1
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs → ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs} +9 -9
  12. package/fesm2022/ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs.map +1 -0
  13. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +334 -27
  14. package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
  15. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs} +1558 -172
  16. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs.map +1 -0
  17. package/fesm2022/ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs +114 -0
  18. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs.map → ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs.map} +1 -1
  19. package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs +34 -0
  20. package/fesm2022/{ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs.map → ng-forge-dynamic-forms-row-wrapper.component-CFtnatYj.mjs.map} +1 -1
  21. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs → ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs} +5 -5
  22. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-Bo4zNJXY.mjs.map → ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs.map} +1 -1
  23. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  24. package/package.json +1 -1
  25. package/types/ng-forge-dynamic-forms-integration.d.ts +244 -40
  26. package/types/ng-forge-dynamic-forms.d.ts +1719 -545
  27. package/fesm2022/ng-forge-dynamic-forms-array-field.component-BgCrTME1.mjs.map +0 -1
  28. package/fesm2022/ng-forge-dynamic-forms-group-field.component-BvTQwoLg.mjs.map +0 -1
  29. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BAPGCjUs.mjs.map +0 -1
  30. package/fesm2022/ng-forge-dynamic-forms-page-field.component-DDzmhJNn.mjs +0 -114
  31. package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs +0 -34
@@ -1,13 +1,155 @@
1
1
  import * as rxjs from 'rxjs';
2
2
  import { Observable } from 'rxjs';
3
3
  import * as _angular_core from '@angular/core';
4
- import { Signal, ResourceRef, Type, ViewContainerRef, InjectionToken, Injector, WritableSignal, EnvironmentInjector, Provider, EnvironmentProviders, Resource, PipeTransform } from '@angular/core';
4
+ import { Signal, Type, ResourceRef, ViewContainerRef, InjectionToken, Injector, WritableSignal, EnvironmentInjector, Provider, EnvironmentProviders, TemplateRef, Resource, PipeTransform } from '@angular/core';
5
5
  import * as _ng_forge_dynamic_forms from '@ng-forge/dynamic-forms';
6
6
  import * as _angular_forms_signals from '@angular/forms/signals';
7
7
  import { FieldContext, TreeValidationResult, ValidationError as ValidationError$1, FieldTree, SchemaPath, SchemaPathTree } from '@angular/forms/signals';
8
8
  import { FormSchema } from '@ng-forge/dynamic-forms/schema';
9
9
  import * as _angular_forms from '@angular/forms';
10
10
 
11
+ type DynamicText = string | Observable<string> | Signal<string>;
12
+
13
+ /**
14
+ * Generic reactive value — a static value, an Angular Signal, or an RxJS Observable.
15
+ *
16
+ * Generalises {@link DynamicText} for arbitrary value types. Used by
17
+ * cross-cutting properties that need to react to form state without forcing
18
+ * every consumer to deal with reactivity (e.g., `hidden` / `disabled` on
19
+ * addons).
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const a: DynamicValue<boolean> = true;
24
+ * const b: DynamicValue<boolean> = signal(false);
25
+ * const c: DynamicValue<boolean> = isValid$;
26
+ * ```
27
+ */
28
+ type DynamicValue<T> = T | Signal<T> | Observable<T>;
29
+
30
+ /**
31
+ * Universal addon slot names supported by every adapter.
32
+ *
33
+ * Adapters render `prefix` to the leading edge of a field and `suffix` to
34
+ * the trailing edge (mirrored in RTL). Additional slots may be contributed
35
+ * via {@link DynamicFormAddonSlotRegistry} module augmentation.
36
+ */
37
+ type CommonAddonSlot = 'prefix' | 'suffix';
38
+ /**
39
+ * Module-augmentable registry of adapter-specific addon slot names.
40
+ *
41
+ * Adapters that expose slots beyond the universal `prefix` / `suffix` (e.g.,
42
+ * an "error overlay" slot specific to one design system) declare them here.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * declare module '@ng-forge/dynamic-forms' {
47
+ * interface DynamicFormAddonSlotRegistry {
48
+ * 'mat-error-overlay': true;
49
+ * }
50
+ * }
51
+ * ```
52
+ */
53
+ interface DynamicFormAddonSlotRegistry {
54
+ }
55
+ /**
56
+ * Union of every valid addon slot — universal slots plus any registered via
57
+ * {@link DynamicFormAddonSlotRegistry} module augmentation.
58
+ */
59
+ type AddonSlot = CommonAddonSlot | keyof DynamicFormAddonSlotRegistry;
60
+
61
+ /**
62
+ * Base shape every addon kind extends.
63
+ *
64
+ * Public contract:
65
+ * - `slot` chooses where the addon renders (prefix / suffix / adapter-specific).
66
+ * - `hidden` / `disabled` are reactive — re-evaluated when their underlying
67
+ * signals or observables emit.
68
+ * - Multiple addons in the same slot render in array order, left-to-right
69
+ * within the slot (LTR; mirrored in RTL).
70
+ *
71
+ * Note: `meta` is intentionally absent — adapters declare addon-specific
72
+ * fields via module augmentation rather than an untyped escape hatch.
73
+ */
74
+ interface BaseAddon<TSlot extends AddonSlot = AddonSlot> {
75
+ /** Slot to render this addon in. */
76
+ readonly slot: TSlot;
77
+ /** CSS class applied to the addon host element. */
78
+ readonly className?: string;
79
+ /** Reactive visibility — when `true`, the addon is removed from layout. */
80
+ readonly hidden?: DynamicValue<boolean>;
81
+ /** Reactive disabled state — primarily relevant for interactive kinds. */
82
+ readonly disabled?: DynamicValue<boolean>;
83
+ }
84
+ /**
85
+ * Module-augmentable registry of addon kinds.
86
+ *
87
+ * Each key is the kind discriminant; each value is the addon's full shape
88
+ * (extending {@link BaseAddon}). Core ships `text`, `template`, and
89
+ * `component`; adapters add their own (e.g., `prime-icon`, `prime-button`).
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * declare module '@ng-forge/dynamic-forms' {
94
+ * interface DynamicFormAddonRegistry {
95
+ * 'prime-icon': PrimeIconAddon;
96
+ * 'prime-button': PrimeButtonAddon;
97
+ * }
98
+ * }
99
+ * ```
100
+ */
101
+ interface DynamicFormAddonRegistry {
102
+ text: TextAddon;
103
+ template: TemplateAddon;
104
+ component: ComponentAddon;
105
+ }
106
+ /**
107
+ * Union of every registered addon shape.
108
+ */
109
+ type AnyAddon = DynamicFormAddonRegistry[keyof DynamicFormAddonRegistry];
110
+ /**
111
+ * Renders text content inline.
112
+ *
113
+ * `text` is `DynamicText`, supporting plain strings, observables, and
114
+ * signals — so values like `'$'`, `'.com'`, or interpolated counters
115
+ * (`'42/100'`) all work uniformly.
116
+ */
117
+ interface TextAddon extends BaseAddon {
118
+ readonly kind: 'text';
119
+ readonly text: DynamicText;
120
+ }
121
+ /**
122
+ * Renders a named `<ng-template>` projected into the host `<df-dynamic-form>`.
123
+ *
124
+ * JSON-safe — backends ship the key, FE supplies the template definition.
125
+ * Use the `templateAddon(ref)` helper for rename-safe authoring in code.
126
+ */
127
+ interface TemplateAddon extends BaseAddon {
128
+ readonly kind: 'template';
129
+ readonly templateKey: string;
130
+ }
131
+ /**
132
+ * Renders an arbitrary Angular component as the addon body.
133
+ *
134
+ * The loader returns either a component class directly or a module with a
135
+ * `default` export. For non-default exports, unwrap with `.then(m => m.X)`:
136
+ *
137
+ * ```ts
138
+ * { kind: 'component', component: () => import('./my').then(m => m.MyComponent) }
139
+ * ```
140
+ *
141
+ * @codeOnly The `component` loader is a function and cannot survive
142
+ * `JSON.stringify`/`parse`. The lenient validator drops this kind when the
143
+ * config originated from JSON (set via `sanitizeFormConfig({ source: 'json' })`).
144
+ */
145
+ interface ComponentAddon extends BaseAddon {
146
+ readonly kind: 'component';
147
+ readonly component: () => Promise<Type<unknown> | {
148
+ default: Type<unknown>;
149
+ }>;
150
+ readonly inputs?: Record<string, unknown>;
151
+ }
152
+
11
153
  type WithInputSignals<T> = {
12
154
  readonly [K in keyof T]-?: Signal<T[K]>;
13
155
  };
@@ -725,19 +867,32 @@ type Widen<T, D extends number = 5> = [D] extends [never] ? T : T extends string
725
867
  -readonly [K in keyof T]: Widen<T[K], Depth[D]>;
726
868
  } : T;
727
869
  /**
728
- * Infer value type based on field type and props.
729
- * - Slider fields: always number
730
- * - Input fields with props.type: 'number': number
731
- * - Other fields: widen the literal value type
870
+ * Resolves the "underlying" non-null value type for a field, based on its `type`
871
+ * discriminator and (where it matters) its `value` literal. Used when we need a
872
+ * base type independent of whether the literal happens to be `null` — i.e., when
873
+ * computing the nullable widening for `value: null, nullable: true`.
732
874
  */
733
- type InferValueType<T, V> = T extends {
875
+ type ResolveBaseValueType<T, V> = T extends {
734
876
  type: 'slider';
735
877
  } ? number : T extends {
736
878
  type: 'input';
737
879
  props: {
738
880
  type: 'number';
739
881
  };
740
- } ? number : Widen<V>;
882
+ } ? number : T extends {
883
+ type: 'checkbox' | 'toggle';
884
+ } ? boolean : [V] extends [null] ? string : Widen<V>;
885
+ /**
886
+ * Infer value type based on field type, value literal, and the `nullable` flag.
887
+ * - Slider fields: always number
888
+ * - Input fields with props.type: 'number': number
889
+ * - Checkbox / toggle fields: always boolean
890
+ * - Other fields: widen the literal value type (default to string when value is null)
891
+ * - When `nullable: true` is declared, widens the result to `T | null`.
892
+ */
893
+ type InferValueType<T, V> = T extends {
894
+ nullable: true;
895
+ } ? ResolveBaseValueType<T, V> | null : ResolveBaseValueType<T, V>;
741
896
  /**
742
897
  * Make type optional if field is not required.
743
898
  * Hidden fields are never optional since they require a value property.
@@ -749,9 +904,11 @@ type MaybeOptional<T, V> = T extends {
749
904
  } ? V : V | undefined;
750
905
  /**
751
906
  * Infers the value type from a single template field (for primitive arrays).
752
- * Mirrors InferValueType logic: slider → number, input[number] → number, else string.
907
+ * Mirrors InferValueType logic: slider → number, input[number] → number,
908
+ * checkbox/toggle → boolean, else widen `value` (string fallback). Widens to
909
+ * `T | null` when `nullable: true` is declared on the template field.
753
910
  */
754
- type InferSingleTemplateValue<T, D extends number> = T extends {
911
+ type InferSingleTemplateValueBase<T, D extends number> = T extends {
755
912
  type: 'slider';
756
913
  } ? number : T extends {
757
914
  type: 'input';
@@ -762,7 +919,10 @@ type InferSingleTemplateValue<T, D extends number> = T extends {
762
919
  type: 'checkbox' | 'toggle';
763
920
  } ? boolean : T extends {
764
921
  value: infer V;
765
- } ? Widen<V, Depth[D]> : string;
922
+ } ? [V] extends [null] ? string : Widen<V, Depth[D]> : string;
923
+ type InferSingleTemplateValue<T, D extends number> = T extends {
924
+ nullable: true;
925
+ } ? InferSingleTemplateValueBase<T, D> | null : InferSingleTemplateValueBase<T, D>;
766
926
  /**
767
927
  * Infers the value type for an array field.
768
928
  * Handles four cases:
@@ -810,7 +970,9 @@ type ProcessField<T, D extends number = 5> = [D] extends [never] ? Record<string
810
970
  } : never : T extends {
811
971
  key: infer K;
812
972
  } ? K extends string ? {
813
- [P in K]: MaybeOptional<T, string>;
973
+ [P in K]: MaybeOptional<T, T extends {
974
+ nullable: true;
975
+ } ? string | null : string>;
814
976
  } : never : never;
815
977
  /**
816
978
  * Internal helper with depth tracking
@@ -2098,8 +2260,6 @@ interface SchemaDefinition {
2098
2260
  subSchemas?: SchemaApplicationConfig[];
2099
2261
  }
2100
2262
 
2101
- type DynamicText = string | Observable<string> | Signal<string>;
2102
-
2103
2263
  /**
2104
2264
  * Re-export Angular's ValidationError for consistency
2105
2265
  * This replaces our custom ValidationError interface
@@ -4195,6 +4355,24 @@ interface FieldDef<TProps, TMeta extends FieldMeta = FieldMeta> {
4195
4355
  * and any field-level `wrappers`.
4196
4356
  */
4197
4357
  skipDefaultWrappers?: boolean;
4358
+ /**
4359
+ * Addons rendered inside this field's slots (typically `prefix` / `suffix`).
4360
+ *
4361
+ * Each addon declares its kind, slot, and kind-specific configuration.
4362
+ * Whether a field type accepts addons — and which slots / kinds — is
4363
+ * declared at registration via `FieldTypeDefinition.addons`. Field types
4364
+ * without that registration ("Tier 3", e.g., toggle, radio) drop any
4365
+ * `addons` value with a lenient warning at config init.
4366
+ *
4367
+ * @example
4368
+ * ```typescript
4369
+ * addons: [
4370
+ * { slot: 'prefix', kind: 'prime-icon', icon: 'search' },
4371
+ * { slot: 'suffix', kind: 'prime-button', preset: 'clear', ariaLabel: 'Clear' },
4372
+ * ]
4373
+ * ```
4374
+ */
4375
+ addons?: ReadonlyArray<AnyAddon>;
4198
4376
  }
4199
4377
  type IncludedKeys = 'label' | 'className' | 'hidden' | 'tabIndex';
4200
4378
  /**
@@ -4289,6 +4467,103 @@ interface GroupContext {
4289
4467
  */
4290
4468
  type MapperFn<T extends FieldDef<unknown>> = (input: T) => Signal<Record<string, unknown>>;
4291
4469
 
4470
+ /**
4471
+ * Minimal JSON Schema fragment used by the MCP server to surface an addon
4472
+ * kind's shape to AI form generators. Covers the common subset
4473
+ * (`type`/`properties`/`required`/`enum`/etc.) without pulling in a full JSON
4474
+ * Schema dependency. Extra keys are allowed via the `[key: string]` index so
4475
+ * vendor-specific annotations (`x-foo`, `markdownDescription`) still typecheck.
4476
+ *
4477
+ * Tooling-only: the runtime addon validator does NOT consult `schema` — it
4478
+ * relies on each kind's `validate?(addon, fieldKey)` callback for shape
4479
+ * enforcement. `schema` exists exclusively so the MCP server (and any other
4480
+ * AI-driven form authoring tool) can describe the kind to a model without
4481
+ * hand-maintaining a parallel registry.
4482
+ */
4483
+ interface AddonKindSchema {
4484
+ readonly $ref?: string;
4485
+ readonly $schema?: string;
4486
+ readonly title?: string;
4487
+ readonly description?: string;
4488
+ readonly type?: 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null' | ReadonlyArray<'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null'>;
4489
+ readonly enum?: ReadonlyArray<unknown>;
4490
+ readonly const?: unknown;
4491
+ readonly properties?: Readonly<Record<string, AddonKindSchema>>;
4492
+ readonly required?: ReadonlyArray<string>;
4493
+ readonly additionalProperties?: boolean | AddonKindSchema;
4494
+ readonly items?: AddonKindSchema | ReadonlyArray<AddonKindSchema>;
4495
+ readonly oneOf?: ReadonlyArray<AddonKindSchema>;
4496
+ readonly anyOf?: ReadonlyArray<AddonKindSchema>;
4497
+ readonly allOf?: ReadonlyArray<AddonKindSchema>;
4498
+ readonly not?: AddonKindSchema;
4499
+ readonly default?: unknown;
4500
+ readonly examples?: ReadonlyArray<unknown>;
4501
+ readonly minimum?: number;
4502
+ readonly maximum?: number;
4503
+ readonly minLength?: number;
4504
+ readonly maxLength?: number;
4505
+ readonly pattern?: string;
4506
+ readonly format?: string;
4507
+ /** Vendor extensions and unrecognised keywords pass through untyped. */
4508
+ readonly [key: string]: unknown;
4509
+ }
4510
+ /**
4511
+ * Optional shape validator a kind ships to enforce its required fields at
4512
+ * runtime. Throws are caught by the validator and converted into lenient
4513
+ * warnings; the offending addon is dropped and the form keeps rendering.
4514
+ */
4515
+ type AddonShapeValidator<T extends BaseAddon = BaseAddon> = (addon: T, fieldKey: string) => void;
4516
+ /**
4517
+ * Per-kind registration metadata.
4518
+ *
4519
+ * Registered via `withCustomAddon(...)` features (or adapter feature
4520
+ * helpers like `withPrimeNGAddons()`); resolved at render time by
4521
+ * `<df-addon-slot>`.
4522
+ */
4523
+ interface AddonKindDefinition<T extends BaseAddon = BaseAddon> {
4524
+ /** Discriminant matching `addon.kind`. */
4525
+ readonly kind: string;
4526
+ /** Lazy loader for the kind's renderer component. */
4527
+ readonly loadComponent: LazyComponentLoader;
4528
+ /** Optional shape validator — throw `DynamicFormError` to drop the addon with a warning. */
4529
+ readonly validate?: AddonShapeValidator<T>;
4530
+ /** Optional JSON Schema fragment for MCP / pre-flight tooling. */
4531
+ readonly schema?: AddonKindSchema;
4532
+ /**
4533
+ * Whether the kind survives `JSON.stringify` / `JSON.parse`. Kinds whose
4534
+ * shape includes a function payload (e.g., the built-in `'component'`
4535
+ * loader, inline `prime-button` `action` handlers) declare `jsonSafe: false`
4536
+ * so the validator drops them when the config was loaded from a JSON
4537
+ * source (`sanitizeFormConfig(config, { source: 'json' })`). Defaults to
4538
+ * `true` — kinds are JSON-safe unless explicitly opted out.
4539
+ */
4540
+ readonly jsonSafe?: boolean;
4541
+ }
4542
+ /**
4543
+ * Per-field-type addon-support metadata declared at field registration.
4544
+ *
4545
+ * Source of truth for the runtime validator: which slots are valid for a
4546
+ * field type, which kinds are accepted (optional whitelist).
4547
+ *
4548
+ * Field types that do not declare `addons` on their `FieldTypeDefinition`
4549
+ * are treated as Tier 3 — addons configured on such fields are dropped
4550
+ * with a warning at config init.
4551
+ */
4552
+ interface FieldAddonSupport {
4553
+ /** Slots a field of this type accepts. */
4554
+ readonly slots: readonly AddonSlot[];
4555
+ /** Optional whitelist of allowed addon kinds. Omitted = any registered kind. */
4556
+ readonly allowedKinds?: readonly string[];
4557
+ }
4558
+ /**
4559
+ * Global registry of addon kinds.
4560
+ *
4561
+ * Populated by `withCustomAddon(...)` features and adapter feature helpers
4562
+ * (e.g., `withPrimeNGAddons()`). Read by `<df-addon-slot>` to resolve a
4563
+ * kind discriminant to its renderer component.
4564
+ */
4565
+ declare const ADDON_KIND_REGISTRY: InjectionToken<Map<string, AddonKindDefinition<BaseAddon<AddonSlot>>>>;
4566
+
4292
4567
  /**
4293
4568
  * Defines how a field type handles form values and data collection.
4294
4569
  *
@@ -4382,6 +4657,23 @@ interface FieldTypeDefinition<T extends FieldDef<any> = any> {
4382
4657
  scope?: FieldScope | FieldScope[];
4383
4658
  /** Mapped component inputs that must exist before the renderer instantiates the component */
4384
4659
  renderReadyWhen?: readonly RenderReadyInput[];
4660
+ /**
4661
+ * Declares which addon slots this field accepts and (optionally) the
4662
+ * subset of kinds allowed.
4663
+ *
4664
+ * Source of truth for the runtime addon validator. Field types that
4665
+ * omit this property are treated as Tier 3 — any `addons` configured
4666
+ * on such a field are dropped with a warning at config init.
4667
+ *
4668
+ * @example
4669
+ * ```typescript
4670
+ * {
4671
+ * name: 'prime-input',
4672
+ * addons: { slots: ['prefix', 'suffix'] },
4673
+ * }
4674
+ * ```
4675
+ */
4676
+ addons?: FieldAddonSupport;
4385
4677
  }
4386
4678
  /**
4387
4679
  * Injection token for the global field type registry.
@@ -4626,12 +4918,22 @@ declare class DynamicForm<TFields extends RegisteredFieldTypes[] = RegisteredFie
4626
4918
  formOptions: _angular_core.InputSignal<FormOptions | undefined>;
4627
4919
  /** Form values for two-way data binding. */
4628
4920
  value: _angular_core.ModelSignal<Partial<TModel> | undefined>;
4921
+ /**
4922
+ * How lenient the addon validator is on `config.fields[...].addons`.
4923
+ * `'inline'` (default) keeps everything; `'json'` drops code-only kinds
4924
+ * with a warning. Read at form init and on every config swap — change
4925
+ * `source` AND the config to force re-validation mid-flight.
4926
+ */
4927
+ source: _angular_core.InputSignal<"json" | "inline">;
4629
4928
  private destroyRef;
4630
4929
  private injector;
4631
4930
  protected environmentInjector: EnvironmentInjector;
4632
4931
  private eventBus;
4633
4932
  private logger;
4634
4933
  private dispatcher;
4934
+ private templateRegistry;
4935
+ /** All `<ng-template dfTemplate="...">` instances projected into this form. */
4936
+ private readonly _projectedTemplates;
4635
4937
  /** State manager that owns all form state. Initialized via connectDeps() to guarantee
4636
4938
  * FORM_STATE_DEPS is populated before FormStateManager is injected. */
4637
4939
  private readonly stateManager;
@@ -4745,7 +5047,7 @@ declare class DynamicForm<TFields extends RegisteredFieldTypes[] = RegisteredFie
4745
5047
  private connectDeps;
4746
5048
  private setupEventHandlers;
4747
5049
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<DynamicForm<any, any>, never>;
4748
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<DynamicForm<any, any>, "form[dynamic-form]", never, { "config": { "alias": "dynamic-form"; "required": true; "isSignal": true; }; "formOptions": { "alias": "formOptions"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "validityChange": "validityChange"; "dirtyChange": "dirtyChange"; "submitted": "submitted"; "reset": "reset"; "cleared": "cleared"; "events": "events"; "initialized": "initialized"; "onPageChange": "onPageChange"; "onPageNavigationStateChange": "onPageNavigationStateChange"; }, never, never, true, never>;
5050
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<DynamicForm<any, any>, "form[dynamic-form]", never, { "config": { "alias": "dynamic-form"; "required": true; "isSignal": true; }; "formOptions": { "alias": "formOptions"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "source": { "alias": "source"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "validityChange": "validityChange"; "dirtyChange": "dirtyChange"; "submitted": "submitted"; "reset": "reset"; "cleared": "cleared"; "events": "events"; "initialized": "initialized"; "onPageChange": "onPageChange"; "onPageNavigationStateChange": "onPageNavigationStateChange"; }, ["_projectedTemplates"], never, true, never>;
4749
5051
  }
4750
5052
 
4751
5053
  declare class EventBus {
@@ -4920,178 +5222,99 @@ interface DynamicFormFeature<TKind extends string = string> {
4920
5222
  ɵproviders: Provider[];
4921
5223
  }
4922
5224
 
4923
- interface FieldOption<T = unknown> {
4924
- label: DynamicText;
4925
- value: T;
4926
- disabled?: boolean;
4927
- [key: string]: unknown;
4928
- }
4929
-
4930
5225
  /**
4931
- * Injection token for form-level default wrappers.
4932
- *
4933
- * Provides a Signal of the `defaultWrappers` array from FormConfig. Consumed by
4934
- * `DfFieldOutlet` / `ContainerFieldComponent` to merge into each field's
4935
- * effective wrapper chain (lowest priority after auto-associations, higher
4936
- * than field-level `wrappers`).
5226
+ * User-facing wrapper registration shape used with `createWrappers(...)`.
4937
5227
  *
4938
- * Provided once at the DynamicForm level and inherited via Angular's
4939
- * hierarchical injector.
5228
+ * Extends `WrapperTypeDefinition` with an optional `props` field that
5229
+ * carries the wrapper's config type (via `wrapperProps`) for later
5230
+ * inference by `InferWrapperRegistry`.
4940
5231
  */
4941
- declare const DEFAULT_WRAPPERS: InjectionToken<Signal<readonly WrapperConfig[] | undefined>>;
5232
+ interface WrapperRegistration<TName extends string = string, TConfig = unknown> {
5233
+ /** Unique identifier for the wrapper type */
5234
+ readonly wrapperName: TName;
5235
+ /** Lazy-loader for the wrapper component */
5236
+ readonly loadComponent: LazyComponentLoader;
5237
+ /** Field types this wrapper auto-applies to (merged lowest-priority during wrapper resolution) */
5238
+ readonly types?: readonly string[];
5239
+ /**
5240
+ * Type carrier only — runtime value is always `undefined`.
5241
+ * Use `wrapperProps<YourConfig>()` to thread the config type.
5242
+ */
5243
+ readonly props?: TConfig;
5244
+ }
4942
5245
  /**
4943
- * Injection token for providing field signal context to mappers and components.
4944
- *
4945
- * The field signal context is the "nervous system" of the dynamic form library,
4946
- * providing access to:
4947
- * - The form instance and structure
4948
- * - Current form values (as signals)
4949
- * - Default values
4950
- * - Validation messages
4951
- * - The injector for creating components
5246
+ * Branded bundle returned by `createWrappers(...)`.
4952
5247
  *
4953
- * This token is provided by the DynamicForm component and can be scoped
4954
- * for nested forms (Groups, Arrays) via child injectors.
5248
+ * Carries:
5249
+ * - `ɵkind: 'wrappers'` — discriminant recognised by `provideDynamicForm(...)`
5250
+ * - `ɵregistrations` — the original registrations (type-level use for `InferWrapperRegistry`)
5251
+ * - `ɵdefinitions` — the `WrapperTypeDefinition[]` extracted for `WRAPPER_REGISTRY`
5252
+ */
5253
+ interface WrappersBundle<T extends readonly WrapperRegistration[] = readonly WrapperRegistration[]> {
5254
+ readonly ɵkind: 'wrappers';
5255
+ readonly ɵregistrations: T;
5256
+ readonly ɵdefinitions: readonly WrapperTypeDefinition[];
5257
+ }
5258
+ /**
5259
+ * Bundle wrapper registrations into a single object that can be passed to
5260
+ * `provideDynamicForm(...)`.
4955
5261
  *
4956
5262
  * @example
4957
5263
  * ```typescript
4958
- * // In a mapper function
4959
- * export function mapValueField(fieldDef: BaseValueField<any, any>): Binding[] {
4960
- * const context = inject(FIELD_SIGNAL_CONTEXT);
4961
- * const form = context.form();
4962
- * // ... use context
4963
- * }
5264
+ * const appWrappers = createWrappers(
5265
+ * {
5266
+ * wrapperName: 'section',
5267
+ * loadComponent: () => import('./section-wrapper'),
5268
+ * props: wrapperProps<SectionWrapper>(),
5269
+ * },
5270
+ * {
5271
+ * wrapperName: 'highlight',
5272
+ * loadComponent: () => import('./highlight-wrapper'),
5273
+ * types: ['input', 'select'],
5274
+ * props: wrapperProps<HighlightWrapper>(),
5275
+ * },
5276
+ * );
4964
5277
  *
4965
- * // In a component
4966
- * export class MyFieldComponent {
4967
- * private context = inject(FIELD_SIGNAL_CONTEXT);
5278
+ * declare module '@ng-forge/dynamic-forms' {
5279
+ * interface FieldRegistryWrappers extends InferWrapperRegistry<typeof appWrappers> {}
4968
5280
  * }
5281
+ *
5282
+ * bootstrapApplication(AppComponent, {
5283
+ * providers: [provideDynamicForm(appWrappers)],
5284
+ * });
4969
5285
  * ```
4970
5286
  */
4971
- declare const FIELD_SIGNAL_CONTEXT: InjectionToken<FieldSignalContext<Record<string, unknown>>>;
5287
+ declare function createWrappers<const T extends readonly WrapperRegistration[]>(...registrations: T): WrappersBundle<T>;
5288
+ /** Type guard for a `WrappersBundle`. */
5289
+ declare function isWrappersBundle(value: unknown): value is WrappersBundle;
4972
5290
  /**
4973
- * Injection token for providing array context metadata to mappers and components.
4974
- *
4975
- * This token is optionally provided by ArrayFieldComponent when creating injectors
4976
- * for array items. It contains metadata about the array item's position and parent.
5291
+ * Derive the `FieldRegistryWrappers` augmentation shape from a wrapper bundle.
4977
5292
  *
4978
- * Mappers can inject this token with {optional: true} to access array context:
5293
+ * Maps each registration's `wrapperName` to its `props` type (the config carried
5294
+ * by `wrapperProps<T>()`). If a registration has no `props`, falls back to the
5295
+ * minimal discriminant shape `{ readonly type: wrapperName }`.
4979
5296
  *
4980
5297
  * @example
4981
5298
  * ```typescript
4982
- * // In a mapper function
4983
- * export function buttonMapper(fieldDef: FieldDef<any>): Binding[] {
4984
- * const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
4985
- * if (arrayContext) {
4986
- * // Use arrayContext.index, arrayContext.arrayKey, etc.
4987
- * }
5299
+ * declare module '@ng-forge/dynamic-forms' {
5300
+ * interface FieldRegistryWrappers extends InferWrapperRegistry<typeof appWrappers> {}
4988
5301
  * }
4989
5302
  * ```
4990
5303
  */
4991
- declare const ARRAY_CONTEXT: InjectionToken<ArrayContext>;
5304
+ type InferWrapperRegistry<T> = T extends WrappersBundle<infer R> ? {
5305
+ [Reg in R[number] as Reg['wrapperName']]: Reg extends {
5306
+ props: infer P;
5307
+ } ? Exclude<P, undefined> : {
5308
+ readonly type: Reg['wrapperName'];
5309
+ };
5310
+ } : never;
5311
+
4992
5312
  /**
4993
- * Injection token for providing group ancestry to mappers and components.
4994
- *
4995
- * Re-provided in two places:
4996
- * 1. `GroupFieldComponent` provides a context whose `groupPath()` is the
4997
- * parent path extended by its own key (e.g. `'address'` or `'user.address'`).
4998
- * 2. `createArrayItemInjector` provides a sentinel with an empty `groupPath`,
4999
- * resetting the chain at array boundaries — descendants of an array item
5000
- * that re-enter a group compose paths INSIDE the array item, matching the
5001
- * property-derivation collector's keying.
5002
- *
5003
- * `mapFieldToInputs` reads this to scope DOM IDs and override-store keys so
5004
- * the same leaf key can appear in different groups without colliding (#401).
5005
- *
5006
- * Inject with `{ optional: true }` because top-level fields with no group
5007
- * ancestor (and no enclosing array) won't have the token provided. The token
5008
- * IS present for fields inside an array even when no inner group exists — the
5009
- * array-boundary sentinel makes injection succeed with an empty `groupPath`.
5313
+ * Type guard to check if a field is a container field (page, row, group, or array)
5314
+ * Container fields have a 'fields' property and don't contribute values directly.
5315
+ * This overload works with RegisteredFieldTypes for full type narrowing.
5010
5316
  */
5011
- declare const GROUP_CONTEXT: InjectionToken<GroupContext>;
5012
- /**
5013
- * Injection token for form-level default props.
5014
- *
5015
- * Default props are form-wide property defaults that are merged with field-level props.
5016
- * Field props take precedence over default props.
5017
- *
5018
- * Unlike FIELD_SIGNAL_CONTEXT which is re-provided by container components (Group, Array),
5019
- * DEFAULT_PROPS is provided ONCE at the DynamicForm level and inherited by all children
5020
- * via Angular's hierarchical injector.
5021
- *
5022
- * The token provides a Signal to enable reactivity - when config changes, mappers
5023
- * that read the signal inside their computed() will automatically update.
5024
- *
5025
- * @example
5026
- * ```typescript
5027
- * // In a mapper function
5028
- * const defaultPropsSignal = inject(DEFAULT_PROPS);
5029
- *
5030
- * return computed(() => {
5031
- * const defaultProps = defaultPropsSignal?.(); // Read inside computed for reactivity
5032
- * const baseInputs = buildBaseInputs(fieldDef, defaultProps);
5033
- * // ...
5034
- * });
5035
- * ```
5036
- */
5037
- declare const DEFAULT_PROPS: InjectionToken<Signal<Record<string, unknown> | undefined>>;
5038
- /**
5039
- * Injection token for form-level default validation messages.
5040
- *
5041
- * Default validation messages act as fallbacks when fields have validation
5042
- * errors but no field-level `validationMessages` defined.
5043
- *
5044
- * Like DEFAULT_PROPS, this token is provided ONCE at the DynamicForm level
5045
- * and inherited by all children via Angular's hierarchical injector.
5046
- *
5047
- * The token provides a Signal to enable reactivity - when config changes, mappers
5048
- * that read the signal inside their computed() will automatically update.
5049
- *
5050
- * @example
5051
- * ```typescript
5052
- * // In a mapper or component
5053
- * const defaultMessagesSignal = inject(DEFAULT_VALIDATION_MESSAGES);
5054
- *
5055
- * return computed(() => {
5056
- * const defaultMessages = defaultMessagesSignal?.(); // Read inside computed for reactivity
5057
- * const message = defaultMessages?.required ?? 'Field is required';
5058
- * // ...
5059
- * });
5060
- * ```
5061
- */
5062
- declare const DEFAULT_VALIDATION_MESSAGES: InjectionToken<Signal<ValidationMessages | undefined>>;
5063
- /**
5064
- * Injection token for form-level options.
5065
- *
5066
- * Form options control form-wide behavior including button disabled states.
5067
- * Used by button mappers to determine default disabled behavior.
5068
- *
5069
- * Like DEFAULT_PROPS, this token is provided ONCE at the DynamicForm level
5070
- * and inherited by all children via Angular's hierarchical injector.
5071
- *
5072
- * The token provides a Signal to enable reactivity - when config changes, mappers
5073
- * that read the signal inside their computed() will automatically update.
5074
- *
5075
- * @example
5076
- * ```typescript
5077
- * // In a button mapper
5078
- * const formOptionsSignal = inject(FORM_OPTIONS);
5079
- *
5080
- * return computed(() => {
5081
- * const formOptions = formOptionsSignal?.(); // Read inside computed for reactivity
5082
- * const disableWhenInvalid = formOptions?.submitButton?.disableWhenInvalid ?? true;
5083
- * // ...
5084
- * });
5085
- * ```
5086
- */
5087
- declare const FORM_OPTIONS: InjectionToken<Signal<FormOptions | undefined>>;
5088
-
5089
- /**
5090
- * Type guard to check if a field is a container field (page, row, group, or array)
5091
- * Container fields have a 'fields' property and don't contribute values directly.
5092
- * This overload works with RegisteredFieldTypes for full type narrowing.
5093
- */
5094
- declare function isContainerField(field: RegisteredFieldTypes): field is ContainerFieldTypes;
5317
+ declare function isContainerField(field: RegisteredFieldTypes): field is ContainerFieldTypes;
5095
5318
  /**
5096
5319
  * Type guard to check if a field is a container field (page, row, group, or array).
5097
5320
  * This overload works with any FieldDef for looser type checking.
@@ -5128,93 +5351,6 @@ type FieldPathAccess<TValue> = {
5128
5351
  [K in keyof TValue]: SchemaPath<TValue[K]> | SchemaPathTree<TValue[K]>;
5129
5352
  };
5130
5353
 
5131
- /**
5132
- * User-facing wrapper registration shape used with `createWrappers(...)`.
5133
- *
5134
- * Extends `WrapperTypeDefinition` with an optional `props` field that
5135
- * carries the wrapper's config type (via `wrapperProps`) for later
5136
- * inference by `InferWrapperRegistry`.
5137
- */
5138
- interface WrapperRegistration<TName extends string = string, TConfig = unknown> {
5139
- /** Unique identifier for the wrapper type */
5140
- readonly wrapperName: TName;
5141
- /** Lazy-loader for the wrapper component */
5142
- readonly loadComponent: LazyComponentLoader;
5143
- /** Field types this wrapper auto-applies to (merged lowest-priority during wrapper resolution) */
5144
- readonly types?: readonly string[];
5145
- /**
5146
- * Type carrier only — runtime value is always `undefined`.
5147
- * Use `wrapperProps<YourConfig>()` to thread the config type.
5148
- */
5149
- readonly props?: TConfig;
5150
- }
5151
- /**
5152
- * Branded bundle returned by `createWrappers(...)`.
5153
- *
5154
- * Carries:
5155
- * - `ɵkind: 'wrappers'` — discriminant recognised by `provideDynamicForm(...)`
5156
- * - `ɵregistrations` — the original registrations (type-level use for `InferWrapperRegistry`)
5157
- * - `ɵdefinitions` — the `WrapperTypeDefinition[]` extracted for `WRAPPER_REGISTRY`
5158
- */
5159
- interface WrappersBundle<T extends readonly WrapperRegistration[] = readonly WrapperRegistration[]> {
5160
- readonly ɵkind: 'wrappers';
5161
- readonly ɵregistrations: T;
5162
- readonly ɵdefinitions: readonly WrapperTypeDefinition[];
5163
- }
5164
- /**
5165
- * Bundle wrapper registrations into a single object that can be passed to
5166
- * `provideDynamicForm(...)`.
5167
- *
5168
- * @example
5169
- * ```typescript
5170
- * const appWrappers = createWrappers(
5171
- * {
5172
- * wrapperName: 'section',
5173
- * loadComponent: () => import('./section-wrapper'),
5174
- * props: wrapperProps<SectionWrapper>(),
5175
- * },
5176
- * {
5177
- * wrapperName: 'highlight',
5178
- * loadComponent: () => import('./highlight-wrapper'),
5179
- * types: ['input', 'select'],
5180
- * props: wrapperProps<HighlightWrapper>(),
5181
- * },
5182
- * );
5183
- *
5184
- * declare module '@ng-forge/dynamic-forms' {
5185
- * interface FieldRegistryWrappers extends InferWrapperRegistry<typeof appWrappers> {}
5186
- * }
5187
- *
5188
- * bootstrapApplication(AppComponent, {
5189
- * providers: [provideDynamicForm(appWrappers)],
5190
- * });
5191
- * ```
5192
- */
5193
- declare function createWrappers<const T extends readonly WrapperRegistration[]>(...registrations: T): WrappersBundle<T>;
5194
- /** Type guard for a `WrappersBundle`. */
5195
- declare function isWrappersBundle(value: unknown): value is WrappersBundle;
5196
- /**
5197
- * Derive the `FieldRegistryWrappers` augmentation shape from a wrapper bundle.
5198
- *
5199
- * Maps each registration's `wrapperName` to its `props` type (the config carried
5200
- * by `wrapperProps<T>()`). If a registration has no `props`, falls back to the
5201
- * minimal discriminant shape `{ readonly type: wrapperName }`.
5202
- *
5203
- * @example
5204
- * ```typescript
5205
- * declare module '@ng-forge/dynamic-forms' {
5206
- * interface FieldRegistryWrappers extends InferWrapperRegistry<typeof appWrappers> {}
5207
- * }
5208
- * ```
5209
- */
5210
- type InferWrapperRegistry<T> = T extends WrappersBundle<infer R> ? {
5211
- [Reg in R[number] as Reg['wrapperName']]: Reg extends {
5212
- props: infer P;
5213
- } ? Exclude<P, undefined> : {
5214
- readonly type: Reg['wrapperName'];
5215
- };
5216
- } : never;
5217
-
5218
5354
  /**
5219
5355
  * Extract FieldDef type from FieldTypeDefinition
5220
5356
  */
@@ -5307,336 +5443,1425 @@ type ExtractFormValue<T> = T extends {
5307
5443
  } ? V : never;
5308
5444
 
5309
5445
  /**
5310
- * Built-in field types provided by the dynamic form library.
5446
+ * Type representing a FieldTree for an array.
5447
+ * Extends FieldTree to add numeric indexing for accessing item FieldTrees.
5448
+ */
5449
+ type ArrayFieldTree<T> = FieldTree<T[]> & {
5450
+ readonly [index: number]: FieldTree<T> | undefined;
5451
+ };
5452
+ /**
5453
+ * Get the length of an array FieldTree.
5454
+ */
5455
+ declare function getArrayLength<T>(arrayFieldTree: ArrayFieldTree<T>): number;
5456
+ /**
5457
+ * Read-only view of a single field's observable state.
5311
5458
  *
5312
- * Each field type is validated at compile time using satisfies, ensuring
5313
- * type safety of the mapper function while allowing the array to be typed
5314
- * as FieldTypeDefinition[] for consumer flexibility.
5459
+ * Whitelisted read signals copied from Angular Signal Forms' `FieldState` so wrappers
5460
+ * can observe a field without being able to mutate it. New write-capable members
5461
+ * added in future Angular versions are excluded by default (the `Pick` list stays
5462
+ * the source of truth).
5463
+ *
5464
+ * `value` is narrowed from `WritableSignal<TValue>` to `Signal<TValue>` so consumers
5465
+ * cannot write through it.
5315
5466
  */
5316
- declare const BUILT_IN_FIELDS: FieldTypeDefinition[];
5467
+ interface ReadonlyFieldTree<TValue = unknown> {
5468
+ readonly value: Signal<TValue>;
5469
+ readonly valid: Signal<boolean>;
5470
+ readonly invalid: Signal<boolean>;
5471
+ readonly touched: Signal<boolean>;
5472
+ readonly dirty: Signal<boolean>;
5473
+ readonly required: Signal<boolean>;
5474
+ readonly disabled: Signal<boolean>;
5475
+ readonly hidden: Signal<boolean>;
5476
+ readonly errors: Signal<readonly unknown[]>;
5477
+ }
5478
+ /**
5479
+ * Build a `ReadonlyFieldTree` by extracting the whitelisted read signals from a
5480
+ * Signal Forms `FieldTree`. Returns a fresh plain object — no casting, no proxying,
5481
+ * so consumers only see the narrow surface and Angular's `WritableSignal` capability
5482
+ * on `value` is hidden.
5483
+ */
5484
+ declare function toReadonlyFieldTree<TValue>(field: FieldTree<TValue>): ReadonlyFieldTree<TValue>;
5485
+ /**
5486
+ * Write a value into a FieldTree's underlying signal. Signal Forms stores
5487
+ * `FieldState.value` as a `WritableSignal`; this helper centralises the cast
5488
+ * and warns if that contract ever changes (vs a silent no-op).
5489
+ */
5490
+ declare function writeToFieldValue<T>(value: Signal<T>, next: T, logger?: {
5491
+ warn: (msg: string) => void;
5492
+ }): boolean;
5317
5493
 
5318
5494
  /**
5319
- * Configuration options for the logger feature.
5320
- *
5321
- * @public
5495
+ * Fields every addon-action context carries, regardless of whether the
5496
+ * addon is bound to a host field or rendered orphaned.
5322
5497
  */
5323
- interface LoggerConfigOptions {
5498
+ interface AddonActionContextBase<TValue> {
5324
5499
  /**
5325
- * Whether general logging is enabled.
5326
- *
5327
- * @default true
5500
+ * Identity of the host field this addon is attached to. Empty strings
5501
+ * indicate the addon is rendered outside an active field context (rare).
5502
+ * Use {@link isFieldBoundContext} or the `form !== null` check to
5503
+ * narrow before relying on field identity.
5328
5504
  */
5329
- enabled?: boolean;
5505
+ readonly field: {
5506
+ readonly key: string;
5507
+ readonly type: string;
5508
+ };
5330
5509
  /**
5331
- * Derivation logging level.
5332
- *
5333
- * - `'none'`: No derivation debug logging (default)
5334
- * - `'summary'`: Log cycle completion with counts
5335
- * - `'verbose'`: Log individual derivation evaluations with details
5336
- *
5337
- * @default 'none'
5510
+ * Current value of the host field at the time the action fires.
5511
+ * `undefined` when the host field has no value yet (initial render before
5512
+ * defaultValue resolution) or when the addon is rendered outside a field.
5338
5513
  */
5339
- derivations?: DerivationLogLevel;
5514
+ readonly value: TValue | undefined;
5340
5515
  }
5341
5516
  /**
5342
- * Configure logging for dynamic forms.
5517
+ * Field-bound variant — the addon is attached to a real field and has a
5518
+ * working value-writer. The common case for `pi-button` / `mat-button` /
5519
+ * `bs-button` / `ion-button` inside an adapter input field.
5343
5520
  *
5344
- * By default, general logging is enabled (ConsoleLogger) and derivation
5345
- * logging is disabled ('none'). Use this feature to enable derivation debugging.
5521
+ * `setValue` is non-optional here; the optional chain (`ctx.setValue?.()`)
5522
+ * is unnecessary noise once you've narrowed via {@link isFieldBoundContext}.
5523
+ */
5524
+ interface FieldBoundAddonActionContext<TValue = unknown> extends AddonActionContextBase<TValue> {
5525
+ /** Read-only view of the host field's tree — same view wrappers receive. */
5526
+ readonly form: ReadonlyFieldTree<TValue>;
5527
+ /** Push a new value into the host field. */
5528
+ readonly setValue: (next: TValue) => void;
5529
+ }
5530
+ /**
5531
+ * Orphan variant — the addon is rendered outside any field component (rare;
5532
+ * happens when consumers compose `<df-addon-slot>` directly without a host
5533
+ * field). No writer is reachable; reading `value` may also yield `undefined`.
5534
+ */
5535
+ interface OrphanAddonActionContext<TValue = unknown> extends AddonActionContextBase<TValue> {
5536
+ readonly form: null;
5537
+ /** Always absent in the orphan variant — distinguishes the union members. */
5538
+ readonly setValue?: undefined;
5539
+ }
5540
+ /**
5541
+ * Context handed to inline addon actions and registered handler functions.
5542
+ *
5543
+ * Discriminated by `form`: field-bound contexts have a non-null `form` and a
5544
+ * required `setValue`; orphan contexts have `form: null` and no `setValue`.
5545
+ * Use {@link isFieldBoundContext} when the handler needs to write back.
5346
5546
  *
5347
5547
  * @example
5348
5548
  * ```typescript
5349
- * // Disable all logging
5350
- * provideDynamicForm(
5351
- * ...withMaterialFields(),
5352
- * withLoggerConfig(false)
5353
- * )
5549
+ * provideAddonActions({
5550
+ * submit: (ctx) => {
5551
+ * if (!isFieldBoundContext(ctx)) return;
5552
+ * // ctx.setValue is now `(next: TValue) => void` — no optional chain.
5553
+ * myService.send(ctx.field.key, ctx.value, ctx.setValue);
5554
+ * },
5555
+ * });
5556
+ * ```
5557
+ */
5558
+ type AddonActionContext<TValue = unknown> = FieldBoundAddonActionContext<TValue> | OrphanAddonActionContext<TValue>;
5559
+ /** Type guard narrowing {@link AddonActionContext} to its field-bound variant. */
5560
+ declare function isFieldBoundContext<TValue>(ctx: AddonActionContext<TValue>): ctx is FieldBoundAddonActionContext<TValue>;
5561
+ /**
5562
+ * Built-in preset actions shared across adapters.
5354
5563
  *
5355
- * // Enable verbose derivation logging
5356
- * provideDynamicForm(
5357
- * ...withMaterialFields(),
5358
- * withLoggerConfig({ derivations: 'verbose' })
5359
- * )
5564
+ * Adapter kinds that accept a `preset` field render the corresponding
5565
+ * behaviour:
5360
5566
  *
5361
- * // Disable derivation logging but keep general logging
5362
- * provideDynamicForm(
5363
- * ...withMaterialFields(),
5364
- * withLoggerConfig({ derivations: 'none' })
5365
- * )
5567
+ * - `'clear'`: empties the field value.
5568
+ * - `'reset'`: restores the field's configured default value (resolved
5569
+ * from the form's `defaultValues` map at click time); falls back to
5570
+ * empty when no default is reachable.
5571
+ * - `'paste'`: reads from the system clipboard and writes the result.
5572
+ * - `'copy'`: writes the field's current value to the system clipboard.
5573
+ * - `'toggle-password-visibility'`: flips a host input's `type` between
5574
+ * `password` and `text` (requires the host field to provide a type
5575
+ * override token, e.g., `PRIME_INPUT_TYPE_OVERRIDE`).
5576
+ *
5577
+ * Form submission is intentionally NOT exposed as a button-addon preset —
5578
+ * use the dedicated `'submit'` field type instead.
5579
+ */
5580
+ type CommonAddonActionPreset = 'clear' | 'reset' | 'paste' | 'copy' | 'toggle-password-visibility';
5581
+ /**
5582
+ * Module-augmentable registry of adapter-specific preset names.
5583
+ *
5584
+ * Adapters add their canonical actions (e.g., Material's
5585
+ * `'mat-datepicker-open'`) without forking the `AddonActionPreset` union.
5586
+ *
5587
+ * @example
5588
+ * ```typescript
5589
+ * declare module '@ng-forge/dynamic-forms' {
5590
+ * interface DynamicFormAddonActionPresetRegistry {
5591
+ * 'mat-datepicker-open': true;
5592
+ * }
5593
+ * }
5594
+ * ```
5595
+ */
5596
+ interface DynamicFormAddonActionPresetRegistry {
5597
+ }
5598
+ /**
5599
+ * Union of every preset name an addon may reference — universal presets
5600
+ * plus any contributed via {@link DynamicFormAddonActionPresetRegistry}.
5601
+ *
5602
+ * The `(string & {})` escape hatch keeps autocomplete on the known +
5603
+ * augmented presets while accepting arbitrary strings for one-off adapter
5604
+ * presets that don't warrant a module-augmentation. The runtime preset
5605
+ * runner already handles unknown names gracefully via its `default:` arm
5606
+ * (logs a warning, no throw). Mirrors `FieldDef.type`'s
5607
+ * `RegisteredFieldTypes['type'] | (string & {})` shape.
5608
+ */
5609
+ type AddonActionPreset = CommonAddonActionPreset | keyof DynamicFormAddonActionPresetRegistry | (string & {});
5610
+ /**
5611
+ * Module-augmentable registry of user-defined named action handlers.
5612
+ *
5613
+ * Users register handlers once at bootstrap with `provideAddonActions({...})`
5614
+ * and reference them from JSON-driven configs via `actionRef: 'handlerName'`.
5615
+ *
5616
+ * @example
5617
+ * ```typescript
5618
+ * declare module '@ng-forge/dynamic-forms' {
5619
+ * interface DynamicFormActionRegistry {
5620
+ * openProfile: true;
5621
+ * runWorkflow: true;
5622
+ * }
5623
+ * }
5624
+ * ```
5625
+ */
5626
+ interface DynamicFormActionRegistry {
5627
+ }
5628
+ /**
5629
+ * Typed string handle to a registered action handler. The empty registry
5630
+ * resolves to `never`; augmenting `DynamicFormActionRegistry` populates the
5631
+ * union and unlocks autocomplete.
5632
+ */
5633
+ type RegisteredActionRef = keyof DynamicFormActionRegistry;
5634
+
5635
+ /**
5636
+ * Map of named `TemplateRef`s collected by `<df-dynamic-form>` from
5637
+ * `<ng-template dfTemplate="...">` children projected into its content.
5638
+ * The directive selector is `dfTemplate` (see `DfTemplate`); the input
5639
+ * value is the name addons reference via `templateKey`.
5640
+ *
5641
+ * The `template` addon kind looks up its `templateKey` in this map at
5642
+ * render time. Exposed as a `Signal` so projections can be added or
5643
+ * removed reactively (e.g., when the host conditionally projects).
5644
+ *
5645
+ * Provided at the form-component level (not root) so multiple
5646
+ * `<df-dynamic-form>` instances on the same page have independent
5647
+ * template scopes.
5648
+ */
5649
+ declare const DF_FIELD_TEMPLATES: InjectionToken<Signal<ReadonlyMap<string, TemplateRef<unknown>>>>;
5650
+
5651
+ interface FieldOption<T = unknown> {
5652
+ label: DynamicText;
5653
+ value: T;
5654
+ disabled?: boolean;
5655
+ [key: string]: unknown;
5656
+ }
5657
+
5658
+ /**
5659
+ * Injection token for form-level default wrappers.
5660
+ *
5661
+ * Provides a Signal of the `defaultWrappers` array from FormConfig. Consumed by
5662
+ * `DfFieldOutlet` / `ContainerFieldComponent` to merge into each field's
5663
+ * effective wrapper chain (lowest priority after auto-associations, higher
5664
+ * than field-level `wrappers`).
5665
+ *
5666
+ * Provided once at the DynamicForm level and inherited via Angular's
5667
+ * hierarchical injector.
5668
+ */
5669
+ declare const DEFAULT_WRAPPERS: InjectionToken<Signal<readonly WrapperConfig[] | undefined>>;
5670
+ /**
5671
+ * Injection token for providing field signal context to mappers and components.
5672
+ *
5673
+ * The field signal context is the "nervous system" of the dynamic form library,
5674
+ * providing access to:
5675
+ * - The form instance and structure
5676
+ * - Current form values (as signals)
5677
+ * - Default values
5678
+ * - Validation messages
5679
+ * - The injector for creating components
5680
+ *
5681
+ * This token is provided by the DynamicForm component and can be scoped
5682
+ * for nested forms (Groups, Arrays) via child injectors.
5683
+ *
5684
+ * @example
5685
+ * ```typescript
5686
+ * // In a mapper function — prefer the helper for required access (descriptive error).
5687
+ * export function mapValueField(fieldDef: BaseValueField<any, any>): Binding[] {
5688
+ * const context = injectFieldSignalContext();
5689
+ * const form = context.form();
5690
+ * // ... use context
5691
+ * }
5692
+ *
5693
+ * // Optional access — token directly, returns null if not provided.
5694
+ * const ctx = inject(FIELD_SIGNAL_CONTEXT, { optional: true });
5695
+ * if (ctx) {
5696
+ * // ...
5697
+ * }
5698
+ * ```
5699
+ */
5700
+ declare const FIELD_SIGNAL_CONTEXT: InjectionToken<FieldSignalContext<Record<string, unknown>>>;
5701
+ /**
5702
+ * Throwing accessor for {@link FIELD_SIGNAL_CONTEXT}.
5703
+ *
5704
+ * Use when the consumer requires the context — group/array containers and
5705
+ * mappers that depend on form structure. Throws a descriptive
5706
+ * `DynamicFormError` instead of Angular's generic `NullInjectorError` when
5707
+ * the token isn't provided.
5708
+ *
5709
+ * Consumers that may legitimately run outside a `DynamicForm` should keep
5710
+ * using `inject(FIELD_SIGNAL_CONTEXT, { optional: true })`.
5711
+ */
5712
+ declare function injectFieldSignalContext<TModel extends Record<string, unknown> = Record<string, unknown>>(): FieldSignalContext<TModel>;
5713
+ /**
5714
+ * Injection token for providing array context metadata to mappers and components.
5715
+ *
5716
+ * This token is optionally provided by ArrayFieldComponent when creating injectors
5717
+ * for array items. It contains metadata about the array item's position and parent.
5718
+ *
5719
+ * Mappers can inject this token with {optional: true} to access array context:
5720
+ *
5721
+ * @example
5722
+ * ```typescript
5723
+ * // In a mapper function
5724
+ * export function buttonMapper(fieldDef: FieldDef<any>): Binding[] {
5725
+ * const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
5726
+ * if (arrayContext) {
5727
+ * // Use arrayContext.index, arrayContext.arrayKey, etc.
5728
+ * }
5729
+ * }
5730
+ * ```
5731
+ */
5732
+ declare const ARRAY_CONTEXT: InjectionToken<ArrayContext>;
5733
+ /**
5734
+ * Injection token for providing group ancestry to mappers and components.
5735
+ *
5736
+ * Re-provided in two places:
5737
+ * 1. `GroupFieldComponent` provides a context whose `groupPath()` is the
5738
+ * parent path extended by its own key (e.g. `'address'` or `'user.address'`).
5739
+ * 2. `createArrayItemInjector` provides a sentinel with an empty `groupPath`,
5740
+ * resetting the chain at array boundaries — descendants of an array item
5741
+ * that re-enter a group compose paths INSIDE the array item, matching the
5742
+ * property-derivation collector's keying.
5743
+ *
5744
+ * `mapFieldToInputs` reads this to scope DOM IDs and override-store keys so
5745
+ * the same leaf key can appear in different groups without colliding (#401).
5746
+ *
5747
+ * Inject with `{ optional: true }` because top-level fields with no group
5748
+ * ancestor (and no enclosing array) won't have the token provided. The token
5749
+ * IS present for fields inside an array even when no inner group exists — the
5750
+ * array-boundary sentinel makes injection succeed with an empty `groupPath`.
5751
+ */
5752
+ declare const GROUP_CONTEXT: InjectionToken<GroupContext>;
5753
+ /**
5754
+ * Injection token for form-level default props.
5755
+ *
5756
+ * Default props are form-wide property defaults that are merged with field-level props.
5757
+ * Field props take precedence over default props.
5758
+ *
5759
+ * Unlike FIELD_SIGNAL_CONTEXT which is re-provided by container components (Group, Array),
5760
+ * DEFAULT_PROPS is provided ONCE at the DynamicForm level and inherited by all children
5761
+ * via Angular's hierarchical injector.
5762
+ *
5763
+ * The token provides a Signal to enable reactivity - when config changes, mappers
5764
+ * that read the signal inside their computed() will automatically update.
5765
+ *
5766
+ * @example
5767
+ * ```typescript
5768
+ * // In a mapper function
5769
+ * const defaultPropsSignal = inject(DEFAULT_PROPS);
5770
+ *
5771
+ * return computed(() => {
5772
+ * const defaultProps = defaultPropsSignal?.(); // Read inside computed for reactivity
5773
+ * const baseInputs = buildBaseInputs(fieldDef, defaultProps);
5774
+ * // ...
5775
+ * });
5776
+ * ```
5777
+ */
5778
+ declare const DEFAULT_PROPS: InjectionToken<Signal<Record<string, unknown> | undefined>>;
5779
+ /**
5780
+ * Injection token for form-level default validation messages.
5781
+ *
5782
+ * Default validation messages act as fallbacks when fields have validation
5783
+ * errors but no field-level `validationMessages` defined.
5784
+ *
5785
+ * Like DEFAULT_PROPS, this token is provided ONCE at the DynamicForm level
5786
+ * and inherited by all children via Angular's hierarchical injector.
5787
+ *
5788
+ * The token provides a Signal to enable reactivity - when config changes, mappers
5789
+ * that read the signal inside their computed() will automatically update.
5790
+ *
5791
+ * @example
5792
+ * ```typescript
5793
+ * // In a mapper or component
5794
+ * const defaultMessagesSignal = inject(DEFAULT_VALIDATION_MESSAGES);
5795
+ *
5796
+ * return computed(() => {
5797
+ * const defaultMessages = defaultMessagesSignal?.(); // Read inside computed for reactivity
5798
+ * const message = defaultMessages?.required ?? 'Field is required';
5799
+ * // ...
5800
+ * });
5801
+ * ```
5802
+ */
5803
+ declare const DEFAULT_VALIDATION_MESSAGES: InjectionToken<Signal<ValidationMessages | undefined>>;
5804
+ /**
5805
+ * Injection token for form-level options.
5806
+ *
5807
+ * Form options control form-wide behavior including button disabled states.
5808
+ * Used by button mappers to determine default disabled behavior.
5809
+ *
5810
+ * Like DEFAULT_PROPS, this token is provided ONCE at the DynamicForm level
5811
+ * and inherited by all children via Angular's hierarchical injector.
5812
+ *
5813
+ * The token provides a Signal to enable reactivity - when config changes, mappers
5814
+ * that read the signal inside their computed() will automatically update.
5815
+ *
5816
+ * @example
5817
+ * ```typescript
5818
+ * // In a button mapper
5819
+ * const formOptionsSignal = inject(FORM_OPTIONS);
5820
+ *
5821
+ * return computed(() => {
5822
+ * const formOptions = formOptionsSignal?.(); // Read inside computed for reactivity
5823
+ * const disableWhenInvalid = formOptions?.submitButton?.disableWhenInvalid ?? true;
5824
+ * // ...
5825
+ * });
5826
+ * ```
5827
+ */
5828
+ declare const FORM_OPTIONS: InjectionToken<Signal<FormOptions | undefined>>;
5829
+
5830
+ /**
5831
+ * Built-in field types provided by the dynamic form library.
5832
+ *
5833
+ * Each field type is validated at compile time using satisfies, ensuring
5834
+ * type safety of the mapper function while allowing the array to be typed
5835
+ * as FieldTypeDefinition[] for consumer flexibility.
5836
+ */
5837
+ declare const BUILT_IN_FIELDS: FieldTypeDefinition[];
5838
+
5839
+ /**
5840
+ * Configuration options for the logger feature.
5841
+ *
5842
+ * @public
5843
+ */
5844
+ interface LoggerConfigOptions {
5845
+ /**
5846
+ * Whether general logging is enabled.
5847
+ *
5848
+ * @default true
5849
+ */
5850
+ enabled?: boolean;
5851
+ /**
5852
+ * Derivation logging level.
5853
+ *
5854
+ * - `'none'`: No derivation debug logging (default)
5855
+ * - `'summary'`: Log cycle completion with counts
5856
+ * - `'verbose'`: Log individual derivation evaluations with details
5857
+ *
5858
+ * @default 'none'
5859
+ */
5860
+ derivations?: DerivationLogLevel;
5861
+ }
5862
+ /**
5863
+ * Configure logging for dynamic forms.
5864
+ *
5865
+ * By default, general logging is enabled (ConsoleLogger) and derivation
5866
+ * logging is disabled ('none'). Use this feature to enable derivation debugging.
5867
+ *
5868
+ * @example
5869
+ * ```typescript
5870
+ * // Disable all logging
5871
+ * provideDynamicForm(
5872
+ * ...withMaterialFields(),
5873
+ * withLoggerConfig(false)
5874
+ * )
5875
+ *
5876
+ * // Enable verbose derivation logging
5877
+ * provideDynamicForm(
5878
+ * ...withMaterialFields(),
5879
+ * withLoggerConfig({ derivations: 'verbose' })
5880
+ * )
5881
+ *
5882
+ * // Disable derivation logging but keep general logging
5883
+ * provideDynamicForm(
5884
+ * ...withMaterialFields(),
5885
+ * withLoggerConfig({ derivations: 'none' })
5886
+ * )
5366
5887
  *
5367
5888
  * // Conditional logging (e.g., only in dev mode)
5368
5889
  * provideDynamicForm(
5369
5890
  * ...withMaterialFields(),
5370
- * withLoggerConfig(() => isDevMode())
5891
+ * withLoggerConfig(() => isDevMode())
5892
+ * )
5893
+ * ```
5894
+ *
5895
+ * @param config - Boolean to enable/disable logging, a function returning boolean, or a config object
5896
+ * @returns A DynamicFormFeature that configures logging
5897
+ *
5898
+ * @public
5899
+ */
5900
+ declare function withLoggerConfig(config?: boolean | (() => boolean) | LoggerConfigOptions): DynamicFormFeature<'logger'>;
5901
+
5902
+ /**
5903
+ * Injection token for the dynamic forms logger.
5904
+ *
5905
+ * Provided by provideDynamicForm() with ConsoleLogger as default.
5906
+ * Override with withLoggerConfig(false) to disable logging.
5907
+ */
5908
+ declare const DynamicFormLogger: InjectionToken<Logger>;
5909
+
5910
+ /**
5911
+ * Console-based logger implementation.
5912
+ */
5913
+ declare class ConsoleLogger implements Logger {
5914
+ private readonly prefix;
5915
+ debug(message: string, ...args: unknown[]): void;
5916
+ info(message: string, ...args: unknown[]): void;
5917
+ warn(message: string, ...args: unknown[]): void;
5918
+ error(message: string, ...args: unknown[]): void;
5919
+ }
5920
+
5921
+ /**
5922
+ * No-operation logger implementation.
5923
+ * All methods are no-ops - used in production by default.
5924
+ */
5925
+ declare class NoopLogger implements Logger {
5926
+ debug(): void;
5927
+ info(): void;
5928
+ warn(): void;
5929
+ error(): void;
5930
+ }
5931
+
5932
+ /**
5933
+ * Enables automatic form value attachment to all events dispatched through the EventBus.
5934
+ *
5935
+ * When this feature is enabled, events dispatched via `eventBus.dispatch()` will include
5936
+ * the current form value in the `formValue` property. This is useful for event handlers
5937
+ * that need access to the complete form state at the time of the event.
5938
+ *
5939
+ * @example Global opt-in
5940
+ * ```typescript
5941
+ * provideDynamicForm(
5942
+ * ...withMaterialFields(),
5943
+ * withEventFormValue()
5944
+ * )
5945
+ * ```
5946
+ *
5947
+ * @example Consumer usage
5948
+ * ```typescript
5949
+ * eventBus.on<SubmitEvent>('submit').subscribe(event => {
5950
+ * if (hasFormValue(event)) {
5951
+ * console.log('Form value:', event.formValue);
5952
+ * }
5953
+ * });
5954
+ * ```
5955
+ *
5956
+ * @example Per-form disable (when globally enabled)
5957
+ * ```typescript
5958
+ * const config: FormConfig = {
5959
+ * fields: [...],
5960
+ * options: { emitFormValueOnEvents: false }
5961
+ * };
5962
+ * ```
5963
+ *
5964
+ * @remarks
5965
+ * **Precedence rules:**
5966
+ * 1. Per-form `false` - Disables for this form, regardless of global setting
5967
+ * 2. Per-form `true` - Enables for this form, regardless of global setting
5968
+ * 3. Per-form `undefined` - Uses global setting
5969
+ * 4. Global `withEventFormValue()` - Enables globally
5970
+ * 5. No global feature - Disabled globally (default)
5971
+ *
5972
+ * @returns A DynamicFormFeature that enables form value attachment to events
5973
+ *
5974
+ * @public
5975
+ */
5976
+ declare function withEventFormValue(): DynamicFormFeature<'event-form-value'>;
5977
+
5978
+ /**
5979
+ * Configuration for excluding field values from form submission output
5980
+ * based on their reactive state (hidden, disabled, readonly).
5981
+ *
5982
+ * Supports a 3-tier configuration hierarchy: Field > Form > Global.
5983
+ * The most specific level wins for each property.
5984
+ *
5985
+ * @public
5986
+ */
5987
+ interface ValueExclusionConfig {
5988
+ /**
5989
+ * Whether to exclude the value of hidden fields from submission output.
5990
+ *
5991
+ * When `true`, fields whose `hidden()` state is `true` will have their
5992
+ * values omitted from the submitted form value.
5993
+ *
5994
+ * @remarks
5995
+ * This does NOT affect HiddenField (`type: 'hidden'`) — those fields
5996
+ * store values without UI and their `hidden()` state is always `false`.
5997
+ */
5998
+ readonly excludeValueIfHidden?: boolean;
5999
+ /**
6000
+ * Whether to exclude the value of disabled fields from submission output.
6001
+ *
6002
+ * When `true`, fields whose `disabled()` state is `true` will have their
6003
+ * values omitted from the submitted form value.
6004
+ */
6005
+ readonly excludeValueIfDisabled?: boolean;
6006
+ /**
6007
+ * Whether to exclude the value of readonly fields from submission output.
6008
+ *
6009
+ * When `true`, fields whose `readonly()` state is `true` will have their
6010
+ * values omitted from the submitted form value.
6011
+ */
6012
+ readonly excludeValueIfReadonly?: boolean;
6013
+ }
6014
+ /**
6015
+ * Fully resolved exclusion config with all properties required.
6016
+ * Produced by the 3-tier resolution: `field ?? form ?? global`.
6017
+ *
6018
+ * @public
6019
+ */
6020
+ type ResolvedValueExclusionConfig = Required<ValueExclusionConfig>;
6021
+
6022
+ /**
6023
+ * Configures global value exclusion defaults for form submission output.
6024
+ *
6025
+ * Value exclusion is **enabled by default** — field values are excluded from the
6026
+ * `(submitted)` output based on their reactive state. Use this feature to
6027
+ * override those defaults. This does NOT affect two-way binding (`value` model /
6028
+ * `entity`) — fields retain their values internally.
6029
+ *
6030
+ * @example Default behavior (all exclusions enabled)
6031
+ * ```typescript
6032
+ * provideDynamicForm(
6033
+ * ...withMaterialFields(),
6034
+ * withValueExclusionDefaults()
6035
+ * )
6036
+ * ```
6037
+ *
6038
+ * @example Disable specific exclusions
6039
+ * ```typescript
6040
+ * provideDynamicForm(
6041
+ * ...withMaterialFields(),
6042
+ * withValueExclusionDefaults({ excludeValueIfReadonly: false })
6043
+ * )
6044
+ * ```
6045
+ *
6046
+ * @example Disable all exclusions (restore pre-v1 behavior)
6047
+ * ```typescript
6048
+ * provideDynamicForm(
6049
+ * ...withMaterialFields(),
6050
+ * withValueExclusionDefaults({
6051
+ * excludeValueIfHidden: false,
6052
+ * excludeValueIfDisabled: false,
6053
+ * excludeValueIfReadonly: false,
6054
+ * })
6055
+ * )
6056
+ * ```
6057
+ *
6058
+ * @remarks
6059
+ * **Precedence rules:**
6060
+ * 1. Per-field `excludeValueIf*` on `FieldDef` — wins for that field
6061
+ * 2. Per-form `excludeValueIf*` on `FormOptions` — wins for all fields in that form
6062
+ * 3. Global `withValueExclusionDefaults()` — baseline default
6063
+ * 4. No global feature — uses token default (all enabled)
6064
+ *
6065
+ * @param config - Partial override of exclusion rules. Unspecified properties default to `true`.
6066
+ * @returns A DynamicFormFeature that configures value exclusion defaults
6067
+ *
6068
+ * @public
6069
+ */
6070
+ declare function withValueExclusionDefaults(config?: Partial<ValueExclusionConfig>): DynamicFormFeature<'value-exclusion'>;
6071
+
6072
+ /**
6073
+ * Configuration for when validation runs based on a field's reactive state.
6074
+ *
6075
+ * Currently controls whether hidden fields are validated. Resolved through a
6076
+ * tree-aware hierarchy: per-field overrides inherit down to descendants, with
6077
+ * per-form `FormOptions` and global `withValidationExecutionDefaults()` providing
6078
+ * the root inherited value.
6079
+ *
6080
+ * @public
6081
+ */
6082
+ interface ValidationExecutionConfig {
6083
+ /**
6084
+ * Whether validators run while a field is hidden (statically, via `hidden`
6085
+ * logic, or because any ancestor is hidden).
6086
+ *
6087
+ * When `false`, all validators on a hidden field are skipped — the field is
6088
+ * effectively inert until shown. State logic (hidden/disabled/readonly) and
6089
+ * value derivations continue to apply.
6090
+ */
6091
+ readonly validateWhenHidden?: boolean;
6092
+ }
6093
+ /**
6094
+ * Fully resolved validation execution config — every property defined.
6095
+ *
6096
+ * @public
6097
+ */
6098
+ type ResolvedValidationExecutionConfig = Required<ValidationExecutionConfig>;
6099
+
6100
+ /**
6101
+ * Configures global validation execution defaults — when validators should run
6102
+ * relative to a field's reactive state.
6103
+ *
6104
+ * By default, validators are **skipped** for hidden fields (`validateWhenHidden: false`),
6105
+ * matching what most users expect: a hidden field is inert. Use this feature to
6106
+ * restore the legacy behavior where hidden fields still validate.
6107
+ *
6108
+ * @example Default behavior — no feature needed
6109
+ * ```typescript
6110
+ * provideDynamicForm(...withMaterialFields())
6111
+ * // hidden fields skip validation
6112
+ * ```
6113
+ *
6114
+ * @example Restore legacy validation-on-hidden behavior
6115
+ * ```typescript
6116
+ * provideDynamicForm(
6117
+ * ...withMaterialFields(),
6118
+ * withValidationExecutionDefaults({ validateWhenHidden: true })
5371
6119
  * )
5372
6120
  * ```
5373
6121
  *
5374
- * @param config - Boolean to enable/disable logging, a function returning boolean, or a config object
5375
- * @returns A DynamicFormFeature that configures logging
6122
+ * @remarks
6123
+ * **Precedence rules:**
6124
+ * 1. Per-field `FieldDef.validateWhenHidden` — wins for that field; inherits down to descendants
6125
+ * that don't override.
6126
+ * 2. Per-form `FormOptions.validateWhenHidden` — root inherited value for the form.
6127
+ * 3. Global `withValidationExecutionDefaults()` — fallback when no form/field setting.
6128
+ * 4. No feature configured — uses token default (`validateWhenHidden: false`).
6129
+ *
6130
+ * @param config - Partial override. Unspecified properties keep their defaults.
6131
+ * @returns A `DynamicFormFeature` that configures validation execution defaults.
5376
6132
  *
5377
6133
  * @public
5378
6134
  */
5379
- declare function withLoggerConfig(config?: boolean | (() => boolean) | LoggerConfigOptions): DynamicFormFeature<'logger'>;
6135
+ declare function withValidationExecutionDefaults(config?: Partial<ValidationExecutionConfig>): DynamicFormFeature<'validation-execution'>;
5380
6136
 
5381
6137
  /**
5382
- * Injection token for the dynamic forms logger.
6138
+ * Opt into Angular Signal Forms' legacy `.ng-touched` / `.ng-invalid` /
6139
+ * `.ng-dirty` / `.ng-pristine` / `.ng-untouched` / `.ng-pending` /
6140
+ * `.ng-valid` CSS classes on form-bound elements.
5383
6141
  *
5384
- * Provided by provideDynamicForm() with ConsoleLogger as default.
5385
- * Override with withLoggerConfig(false) to disable logging.
6142
+ * Wires Angular's `@angular/forms/signals/compat` `NG_STATUS_CLASSES` strategy
6143
+ * via `provideSignalFormsConfig`. Without this feature the modern Signal Forms
6144
+ * class strategy is active, which omits the legacy `ng-*` classes.
6145
+ *
6146
+ * @example Opt in once at the application root
6147
+ * ```typescript
6148
+ * bootstrapApplication(AppComponent, {
6149
+ * providers: [
6150
+ * provideDynamicForm(
6151
+ * ...withMaterialFields(),
6152
+ * withLegacyStatusClasses()
6153
+ * )
6154
+ * ]
6155
+ * });
6156
+ * ```
6157
+ *
6158
+ * @returns A DynamicFormFeature that enables the legacy `ng-*` class strategy
6159
+ *
6160
+ * @public
5386
6161
  */
5387
- declare const DynamicFormLogger: InjectionToken<Logger>;
6162
+ declare function withLegacyStatusClasses(): DynamicFormFeature<'legacy-status-classes'>;
5388
6163
 
5389
6164
  /**
5390
- * Console-based logger implementation.
6165
+ * Zero-cost type carrier used in wrapper registrations.
6166
+ *
6167
+ * Returns `undefined` at runtime but is typed as `T`, so TypeScript can
6168
+ * thread a wrapper's config type through `createWrappers(...)` into the
6169
+ * `InferWrapperRegistry<typeof ...>` utility without requiring users to
6170
+ * hand-write the augmentation shape.
6171
+ *
6172
+ * @example
6173
+ * ```typescript
6174
+ * const wrappers = createWrappers(
6175
+ * {
6176
+ * wrapperName: 'section',
6177
+ * loadComponent: () => import('./section-wrapper'),
6178
+ * props: wrapperProps<SectionWrapper>(),
6179
+ * },
6180
+ * );
6181
+ *
6182
+ * declare module '@ng-forge/dynamic-forms' {
6183
+ * interface FieldRegistryWrappers extends InferWrapperRegistry<typeof wrappers> {}
6184
+ * }
6185
+ * ```
5391
6186
  */
5392
- declare class ConsoleLogger implements Logger {
5393
- private readonly prefix;
5394
- debug(message: string, ...args: unknown[]): void;
5395
- info(message: string, ...args: unknown[]): void;
5396
- warn(message: string, ...args: unknown[]): void;
5397
- error(message: string, ...args: unknown[]): void;
5398
- }
6187
+ declare function wrapperProps<T>(): T;
5399
6188
 
5400
6189
  /**
5401
- * No-operation logger implementation.
5402
- * All methods are no-ops - used in production by default.
6190
+ * Input shape passed to a wrapper component via the `fieldInputs` input.
6191
+ *
6192
+ * Carries the wrapped field's mapper outputs plus an optional `field`
6193
+ * read-only view. Wrappers reached through a container path (which has
6194
+ * no FieldTree of its own) receive `fieldInputs === undefined`, so any
6195
+ * wrapper that reads `fieldInputs.field` must guard for it.
6196
+ *
6197
+ * Mappers must emit new rawInputs objects per tick rather than mutating
6198
+ * the previous one — this bag is a shallow spread, so mutations to shared
6199
+ * nested values leak to downstream wrappers.
5403
6200
  */
5404
- declare class NoopLogger implements Logger {
5405
- debug(): void;
5406
- info(): void;
5407
- warn(): void;
5408
- error(): void;
6201
+ interface WrapperFieldInputs {
6202
+ readonly key: string;
6203
+ /** Field-type discriminant (mirrors `FieldDef.type`) — useful for kind-aware addons. */
6204
+ readonly type?: string;
6205
+ readonly label?: string;
6206
+ readonly placeholder?: string;
6207
+ readonly className?: string;
6208
+ readonly props?: Record<string, unknown>;
6209
+ readonly validationMessages?: Record<string, string>;
6210
+ readonly defaultValidationMessages?: Record<string, string>;
6211
+ readonly field?: ReadonlyFieldTree;
6212
+ /**
6213
+ * Write a value into the wrapped field's tree. Provided by `buildFieldInputs`
6214
+ * when a `FieldTree` is present. Addons (e.g., `prime-button` presets) use this
6215
+ * to mutate the host field; wrappers should treat the field as read-only.
6216
+ */
6217
+ readonly setValue?: (next: unknown) => void;
6218
+ readonly [key: string]: unknown;
5409
6219
  }
5410
6220
 
5411
6221
  /**
5412
- * Enables automatic form value attachment to all events dispatched through the EventBus.
6222
+ * Dispatcher component that renders an addon by looking up its `kind` in
6223
+ * `ADDON_KIND_REGISTRY` and instantiating the registered component.
5413
6224
  *
5414
- * When this feature is enabled, events dispatched via `eventBus.dispatch()` will include
5415
- * the current form value in the `formValue` property. This is useful for event handlers
5416
- * that need access to the complete form state at the time of the event.
6225
+ * Used by every adapter's field component to render slot content:
5417
6226
  *
5418
- * @example Global opt-in
5419
- * ```typescript
5420
- * provideDynamicForm(
5421
- * ...withMaterialFields(),
5422
- * withEventFormValue()
5423
- * )
6227
+ * ```html
6228
+ * \@for (a of addonsAt('prefix'); track $index) {
6229
+ * <df-addon-slot [addon]="a" />
6230
+ * }
5424
6231
  * ```
5425
6232
  *
5426
- * @example Consumer usage
5427
- * ```typescript
5428
- * eventBus.on<SubmitEvent>('submit').subscribe(event => {
5429
- * if (hasFormValue(event)) {
5430
- * console.log('Form value:', event.formValue);
5431
- * }
5432
- * });
6233
+ * Behaviour:
6234
+ * - Resolves and caches the kind's component asynchronously on first render.
6235
+ * - Forwards the `slot` HTML attribute on the host so Ionic shadow-DOM
6236
+ * projection works (`<ion-input>` projects children with `slot="start"`
6237
+ * into its native start slot).
6238
+ * - Honours the addon's reactive `hidden` flag — when `true`, the host is
6239
+ * `display: none` so it occupies no layout but stays in DOM (cheaper than
6240
+ * tearing down the component).
6241
+ * - Forwards `className` to the host element.
6242
+ * - ARIA semantics are owned by individual kind components — decorative
6243
+ * kinds (icons, text) set `aria-hidden="true"` on themselves; interactive
6244
+ * kinds (buttons) handle their own `aria-label`.
6245
+ *
6246
+ * If the kind cannot be resolved (registry miss, load failure), the
6247
+ * dispatcher logs a warning and renders nothing — never throws. The
6248
+ * runtime addon validator should have caught unknown kinds at config init,
6249
+ * but this provides a defence-in-depth fallback for misconfigured registries.
6250
+ */
6251
+ declare class DfAddonSlot {
6252
+ private readonly registry;
6253
+ private readonly logger;
6254
+ private readonly hostInjector;
6255
+ readonly addon: _angular_core.InputSignal<AnyAddon>;
6256
+ /**
6257
+ * Optional read-only view of the host field's mapped inputs — same shape
6258
+ * wrappers receive (`field?: ReadonlyFieldTree`, `key`, `props`, etc.).
6259
+ * Forwarded to kind components so they can read field state without
6260
+ * re-injecting `FIELD_SIGNAL_CONTEXT` and rebuilding context themselves.
6261
+ *
6262
+ * `undefined` when the addon is rendered outside a field (rare).
6263
+ */
6264
+ readonly fieldInputs: _angular_core.InputSignal<WrapperFieldInputs | undefined>;
6265
+ /**
6266
+ * Pre-resolved `hidden` signal supplied by the host field. When provided,
6267
+ * the dispatcher reads this instead of re-resolving `addon.hidden` —
6268
+ * lets the host (e.g., `prime-input`) share a single subscription per
6269
+ * Observable-typed `hidden` between its own visibility filter and the
6270
+ * slot's `[style.display]` binding.
6271
+ */
6272
+ readonly hidden: _angular_core.InputSignal<Signal<boolean> | undefined>;
6273
+ /** `slot` HTML attribute forwarded to host — needed for Ionic shadow projection. */
6274
+ protected readonly slotAttr: Signal<_ng_forge_dynamic_forms.AddonSlot>;
6275
+ protected readonly className: Signal<string | null>;
6276
+ /**
6277
+ * Locally-resolved `hidden` signal — always reflects `addon.hidden`
6278
+ * regardless of whether the host supplied a pre-resolved signal.
6279
+ * Populated via `explicitEffect` so `resolveDynamicValue` (which may call
6280
+ * `toSignal` for Observable-typed values) does NOT run inside a reactive
6281
+ * context (NG0602). The WeakMap caches per `hidden`-value identity, so
6282
+ * swapping `addon` inputs that share the same Observable reuses the
6283
+ * subscription instead of leaking a fresh one per swap.
6284
+ *
6285
+ * Resolved unconditionally (not gated on host `hidden()` presence) so a
6286
+ * transition from `host hidden = Signal` → `undefined` doesn't expose a
6287
+ * brief window where neither value is current — the upstream effect
6288
+ * re-runs eventually, but the computed read can happen in between.
6289
+ */
6290
+ private readonly _hiddenByValue;
6291
+ private readonly _resolvedHidden;
6292
+ /**
6293
+ * `hidden` resolved from `DynamicValue<boolean>` to a flat `Signal<boolean>`.
6294
+ * Prefers the host-supplied signal when present (avoids duplicate `toSignal`
6295
+ * subscriptions for Observable-typed `hidden`); falls back to the locally-
6296
+ * resolved signal, which is always populated by the addon-tracking effect.
6297
+ */
6298
+ protected readonly isHidden: Signal<boolean>;
6299
+ /**
6300
+ * Latest async-loaded `{ kind, component }` pair. Tagging the result with
6301
+ * its kind lets `resolvedComponent` filter stale loads structurally — no
6302
+ * monotonic token needed. When `addon.kind` changes mid-load, the prior
6303
+ * load still completes and writes here, but the computed ignores it
6304
+ * because the recorded kind no longer matches the current one.
6305
+ */
6306
+ private readonly asyncLoaded;
6307
+ /**
6308
+ * Resolved kind component — composed declaratively from two sources of
6309
+ * truth: the registry's synchronous cache (warm hits, e.g., a second
6310
+ * `<df-addon-slot>` for an already-loaded kind) and the kind-tagged async
6311
+ * loader result. Reading `addon().kind` first makes the computed react to
6312
+ * addon swaps and naturally invalidate stale async-load results.
6313
+ */
6314
+ protected readonly resolvedComponent: Signal<Type<unknown> | undefined>;
6315
+ /** Inputs passed to the kind component — addon plus the wrapper-style host bag. */
6316
+ protected readonly kindInputs: Signal<{
6317
+ addon: AnyAddon;
6318
+ fieldInputs: WrapperFieldInputs | undefined;
6319
+ }>;
6320
+ constructor();
6321
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<DfAddonSlot, never>;
6322
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<DfAddonSlot, "df-addon-slot", never, { "addon": { "alias": "addon"; "required": true; "isSignal": true; }; "fieldInputs": { "alias": "fieldInputs"; "required": false; "isSignal": true; }; "hidden": { "alias": "hidden"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
6323
+ }
6324
+
6325
+ /**
6326
+ * Marks an `<ng-template>` projected into `<df-dynamic-form>` content as
6327
+ * available to template addons by name.
6328
+ *
6329
+ * The host form collects all `DfTemplate` instances via `contentChildren`
6330
+ * and exposes them through {@link DF_FIELD_TEMPLATES}, where the template
6331
+ * addon kind looks them up by `templateKey` at render time.
6332
+ *
6333
+ * @example
6334
+ * ```html
6335
+ * <df-dynamic-form [config]="config">
6336
+ * <ng-template dfTemplate="searchIcon" let-field>
6337
+ * <my-icon [field]="field" />
6338
+ * </ng-template>
6339
+ * </df-dynamic-form>
5433
6340
  * ```
5434
6341
  *
5435
- * @example Per-form disable (when globally enabled)
5436
- * ```typescript
5437
- * const config: FormConfig = {
5438
- * fields: [...],
5439
- * options: { emitFormValueOnEvents: false }
5440
- * };
6342
+ * In the field config:
6343
+ * ```ts
6344
+ * { slot: 'prefix', kind: 'template', templateKey: 'searchIcon' }
5441
6345
  * ```
6346
+ */
6347
+ declare class DfTemplate {
6348
+ /** Name this template registers under — referenced from `TemplateAddon.templateKey`. */
6349
+ readonly dfTemplate: _angular_core.InputSignal<string>;
6350
+ /** The wrapped template, captured for registration and `NgTemplateOutlet` rendering. */
6351
+ readonly templateRef: TemplateRef<unknown>;
6352
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<DfTemplate, never>;
6353
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<DfTemplate, "ng-template[dfTemplate]", ["dfTemplate"], { "dfTemplate": { "alias": "dfTemplate"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
6354
+ }
6355
+
6356
+ /**
6357
+ * Renderer for the universal `text` addon kind.
5442
6358
  *
5443
- * @remarks
5444
- * **Precedence rules:**
5445
- * 1. Per-form `false` - Disables for this form, regardless of global setting
5446
- * 2. Per-form `true` - Enables for this form, regardless of global setting
5447
- * 3. Per-form `undefined` - Uses global setting
5448
- * 4. Global `withEventFormValue()` - Enables globally
5449
- * 5. No global feature - Disabled globally (default)
6359
+ * Renders the addon's `text` (a `DynamicText`) inside a span. Decorative —
6360
+ * sets `aria-hidden="true"` on the host so screen readers skip it; users
6361
+ * who need announcement should use a `*-button` kind with `ariaLabel` instead.
6362
+ */
6363
+ declare class TextAddonComponent {
6364
+ readonly addon: _angular_core.InputSignal<TextAddon>;
6365
+ /** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */
6366
+ readonly fieldInputs: _angular_core.InputSignal<WrapperFieldInputs | undefined>;
6367
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TextAddonComponent, never>;
6368
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TextAddonComponent, "df-text-addon", never, { "addon": { "alias": "addon"; "required": true; "isSignal": true; }; "fieldInputs": { "alias": "fieldInputs"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
6369
+ }
6370
+
6371
+ /**
6372
+ * Renderer for the universal `template` addon kind.
5450
6373
  *
5451
- * @returns A DynamicFormFeature that enables form value attachment to events
6374
+ * Resolves the addon's `templateKey` against {@link DF_FIELD_TEMPLATES}
6375
+ * (populated by `<df-dynamic-form>` from `<ng-template dfTemplate="...">`
6376
+ * children) and renders via `NgTemplateOutlet`. The wrapper-style host bag
6377
+ * (`fieldInputs`) is forwarded as the template's implicit context — author
6378
+ * the template as `<ng-template dfTemplate="..." let-fi>{{ fi.key }}</ng-template>`.
5452
6379
  *
5453
- * @public
6380
+ * If the key is unresolved, logs a warning and renders nothing — keeps the
6381
+ * form alive when a backend references a template the FE has not registered.
5454
6382
  */
5455
- declare function withEventFormValue(): DynamicFormFeature<'event-form-value'>;
6383
+ declare class TemplateAddonComponent {
6384
+ private readonly templates;
6385
+ private readonly logger;
6386
+ readonly addon: _angular_core.InputSignal<TemplateAddon>;
6387
+ /** Forwarded by `df-addon-slot`; surfaced to the template as its implicit `$implicit` context. */
6388
+ readonly fieldInputs: _angular_core.InputSignal<WrapperFieldInputs | undefined>;
6389
+ protected readonly template: _angular_core.Signal<_angular_core.TemplateRef<unknown> | null>;
6390
+ /** Implicit context payload — template authors bind via `let-fi`. */
6391
+ protected readonly outletContext: _angular_core.Signal<{
6392
+ $implicit: WrapperFieldInputs | undefined;
6393
+ }>;
6394
+ constructor();
6395
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TemplateAddonComponent, never>;
6396
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TemplateAddonComponent, "df-template-addon", never, { "addon": { "alias": "addon"; "required": true; "isSignal": true; }; "fieldInputs": { "alias": "fieldInputs"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
6397
+ }
5456
6398
 
5457
6399
  /**
5458
- * Configuration for excluding field values from form submission output
5459
- * based on their reactive state (hidden, disabled, readonly).
6400
+ * Renderer for the universal `component` addon kind.
5460
6401
  *
5461
- * Supports a 3-tier configuration hierarchy: Field > Form > Global.
5462
- * The most specific level wins for each property.
6402
+ * Loads the user-supplied component lazily (`addon.component()`) and renders
6403
+ * it via `NgComponentOutlet` with `addon.inputs` forwarded as input bindings.
6404
+ * The wrapper-style `fieldInputs` bag is not propagated — user components
6405
+ * receive only what `addon.inputs` declares. Authors that need field state
6406
+ * should switch to the `template` kind or build a wrapper.
5463
6407
  *
5464
- * @public
6408
+ * @codeOnly The `component` loader is a function; this kind is dropped in
6409
+ * lenient validation when the config originated from JSON.
5465
6410
  */
5466
- interface ValueExclusionConfig {
6411
+ declare class ComponentAddonComponent {
6412
+ private readonly logger;
6413
+ readonly addon: _angular_core.InputSignal<ComponentAddon>;
6414
+ /** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */
6415
+ readonly fieldInputs: _angular_core.InputSignal<WrapperFieldInputs | undefined>;
6416
+ protected readonly inputs: _angular_core.Signal<Record<string, unknown>>;
6417
+ /**
6418
+ * Latest async-loaded `{ addon, component }` pair. Tagging the result with
6419
+ * its source addon lets `resolvedComponent` filter stale loads structurally
6420
+ * — same kind-tagging pattern `DfAddonSlot` uses. When `addon` swaps
6421
+ * mid-load, the prior loader still completes and writes here, but the
6422
+ * computed ignores it because the recorded addon no longer matches the
6423
+ * current one.
6424
+ */
6425
+ private readonly asyncLoaded;
6426
+ protected readonly resolvedComponent: _angular_core.Signal<Type<unknown> | undefined>;
6427
+ constructor();
6428
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ComponentAddonComponent, never>;
6429
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ComponentAddonComponent, "df-component-addon", never, { "addon": { "alias": "addon"; "required": true; "isSignal": true; }; "fieldInputs": { "alias": "fieldInputs"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
6430
+ }
6431
+
6432
+ /**
6433
+ * Optional collaborators a preset may need beyond the base context. Passed
6434
+ * by the button kind component which has access to its own DI scope.
6435
+ *
6436
+ * Adapter input components fill in the collaborators their preset semantics
6437
+ * require:
6438
+ * - `mat-input` / `bs-input` / `prime-input` / `ion-input` populate
6439
+ * `typeOverride` + `fieldValueSetter` + `fieldDefaultValueGetter` +
6440
+ * `baselineType` from their own DI scope.
6441
+ * - Addons rendered outside any input field receive `{ logger }` only and
6442
+ * the input-specific presets warn-and-no-op.
6443
+ */
6444
+ interface PresetCollaborators {
6445
+ /** Per-field input-type override signal — populated only inside an input field. */
6446
+ readonly typeOverride?: WritableSignal<string | undefined>;
6447
+ /** Writer that pushes a value into the host field's tree. */
6448
+ readonly fieldValueSetter?: (next: unknown) => void;
5467
6449
  /**
5468
- * Whether to exclude the value of hidden fields from submission output.
5469
- *
5470
- * When `true`, fields whose `hidden()` state is `true` will have their
5471
- * values omitted from the submitted form value.
5472
- *
5473
- * @remarks
5474
- * This does NOT affect HiddenField (`type: 'hidden'`) — those fields
5475
- * store values without UI and their `hidden()` state is always `false`.
6450
+ * Read the host field's configured default value (from the form's
6451
+ * `defaultValues` map keyed by `field.key`). Used by the `'reset'`
6452
+ * preset; absent when the addon is rendered outside an active field
6453
+ * context, in which case `'reset'` falls back to clearing.
5476
6454
  */
5477
- readonly excludeValueIfHidden?: boolean;
6455
+ readonly fieldDefaultValueGetter?: () => unknown;
5478
6456
  /**
5479
- * Whether to exclude the value of disabled fields from submission output.
5480
- *
5481
- * When `true`, fields whose `disabled()` state is `true` will have their
5482
- * values omitted from the submitted form value.
6457
+ * Baseline configured input type (from `props().type`), read at preset-run
6458
+ * time. Used by `toggle-password-visibility` to refuse to corrupt
6459
+ * `'email'`/`'number'`/etc. inputs into `'password'` when no override is
6460
+ * yet set.
5483
6461
  */
5484
- readonly excludeValueIfDisabled?: boolean;
6462
+ readonly baselineType?: () => string | undefined;
5485
6463
  /**
5486
- * Whether to exclude the value of readonly fields from submission output.
6464
+ * Logger for warnings about presets that can't be fulfilled in this context.
5487
6465
  *
5488
- * When `true`, fields whose `readonly()` state is `true` will have their
5489
- * values omitted from the submitted form value.
6466
+ * Passed via collaborators (rather than injected) because `runPresetAction`
6467
+ * is a DI-free pure function shared across adapters — each adapter's
6468
+ * `ADDON_PRESET_HANDLER` factory injects `DynamicFormLogger` itself and
6469
+ * forwards it here. The `[Dynamic Forms]` prefix comes from
6470
+ * `ConsoleLogger`'s implementation, so the final console line is identical
6471
+ * to one logged directly via `inject(DynamicFormLogger)`.
5490
6472
  */
5491
- readonly excludeValueIfReadonly?: boolean;
6473
+ readonly logger: Logger;
6474
+ }
6475
+ /**
6476
+ * Apply a built-in preset action against the addon's host field. Shared
6477
+ * across all adapters — each adapter's `ADDON_PRESET_HANDLER` provider
6478
+ * delegates here.
6479
+ *
6480
+ * Presets that require adapter-specific UI mechanics (clipboard, type
6481
+ * override) are best-effort — they no-op with a warning when the necessary
6482
+ * collaborator isn't available (e.g., toggle-password-visibility outside an
6483
+ * input field).
6484
+ *
6485
+ * @param adapterLabel Used in warnings — `'Material'` / `'Bootstrap'` / etc.
6486
+ * @param fieldLabel Used in warnings — `'mat-input'` / `'bs-input'` / etc.
6487
+ */
6488
+ declare function runPresetAction(preset: AddonActionPreset, ctx: AddonActionContext, collaborators: PresetCollaborators, adapterLabel: string, fieldLabel: string): Promise<void>;
6489
+
6490
+ /**
6491
+ * Cache for resolved addon-kind components.
6492
+ *
6493
+ * Provided per-form by `provideDynamicFormDI()` so two `<df-dynamic-form>`
6494
+ * instances with conflicting `loadComponent` factories for the same kind
6495
+ * never share a stale entry. Has no `providedIn: 'root'` factory — the
6496
+ * registry consumer is always inside a form scope, and a root fallback
6497
+ * would only hide misconfiguration (an SSR cross-request hit if a future
6498
+ * code path forgot to provide it locally).
6499
+ *
6500
+ * Mirrors `COMPONENT_CACHE` (field types) and `WRAPPER_COMPONENT_CACHE`
6501
+ * (wrappers).
6502
+ *
6503
+ * @internal
6504
+ */
6505
+ declare const ADDON_KIND_COMPONENT_CACHE: InjectionToken<Map<string, Type<unknown>>>;
6506
+ /**
6507
+ * Public shape returned by {@link injectAddonKindRegistry}. Pinned explicitly
6508
+ * so the API extractor doesn't churn between versions when the inner factory
6509
+ * gets refactored.
6510
+ */
6511
+ interface AddonKindRegistryRef {
6512
+ getKind(kind: string): AddonKindDefinition | undefined;
6513
+ hasKind(kind: string): boolean;
6514
+ getKindNames(): string[];
6515
+ loadKindComponent(kind: string): Promise<Type<unknown>>;
6516
+ getLoadedKindComponent(kind: string): Type<unknown> | undefined;
6517
+ readonly raw: ReadonlyMap<string, AddonKindDefinition>;
5492
6518
  }
5493
6519
  /**
5494
- * Fully resolved exclusion config with all properties required.
5495
- * Produced by the 3-tier resolution: `field ?? form ?? global`.
6520
+ * Injection function for accessing the addon kind registry.
6521
+ *
6522
+ * Usage parallels {@link injectFieldRegistry}: lookup, lazy load, cache.
6523
+ * Must be called within an injection context.
6524
+ *
6525
+ * @example
6526
+ * ```typescript
6527
+ * const addonKinds = injectAddonKindRegistry();
6528
+ * const Component = await addonKinds.loadKindComponent('prime-icon');
6529
+ * ```
6530
+ */
6531
+ declare function injectAddonKindRegistry(): AddonKindRegistryRef;
6532
+
6533
+ /**
6534
+ * One entry per field type that declares addon support.
5496
6535
  *
5497
- * @public
6536
+ * `name` is the field-type discriminant; `slots` and `allowedKinds` mirror
6537
+ * the `FieldTypeDefinition.addons` registration.
5498
6538
  */
5499
- type ResolvedValueExclusionConfig = Required<ValueExclusionConfig>;
5500
-
6539
+ interface FieldAddonSupportEntry {
6540
+ readonly name: string;
6541
+ readonly slots: FieldAddonSupport['slots'];
6542
+ readonly allowedKinds: FieldAddonSupport['allowedKinds'];
6543
+ }
5501
6544
  /**
5502
- * Configures global value exclusion defaults for form submission output.
6545
+ * Walk `FIELD_REGISTRY` and return every field type that opted into addons
6546
+ * (i.e., declared `addons` on its `FieldTypeDefinition`).
5503
6547
  *
5504
- * Value exclusion is **enabled by default** — field values are excluded from the
5505
- * `(submitted)` output based on their reactive state. Use this feature to
5506
- * override those defaults. This does NOT affect two-way binding (`value` model /
5507
- * `entity`) — fields retain their values internally.
6548
+ * Useful for tooling — admin UIs surfacing "which fields can carry addons?",
6549
+ * docs generators, MCP server cross-checks. Field types without an `addons`
6550
+ * registration ("Tier 3" — toggle, checkbox, radio, slider) are excluded.
5508
6551
  *
5509
- * @example Default behavior (all exclusions enabled)
6552
+ * Must be called within an injection context.
6553
+ *
6554
+ * @example
5510
6555
  * ```typescript
5511
- * provideDynamicForm(
5512
- * ...withMaterialFields(),
5513
- * withValueExclusionDefaults()
5514
- * )
6556
+ * @Component({ ... })
6557
+ * class FormBuilder {
6558
+ * protected readonly addonable = injectFieldsSupportingAddons();
6559
+ *
6560
+ * constructor() {
6561
+ * // -> [{ name: 'prime-input', slots: ['prefix', 'suffix'], allowedKinds: [...] }, ...]
6562
+ * }
6563
+ * }
5515
6564
  * ```
6565
+ */
6566
+ declare function injectFieldsSupportingAddons(): FieldAddonSupportEntry[];
6567
+
6568
+ /**
6569
+ * Normalises a {@link DynamicValue} into a `Signal<T>`.
5516
6570
  *
5517
- * @example Disable specific exclusions
6571
+ * Accepts a static value, a `Signal<T>`, or an `Observable<T>`. The result
6572
+ * is a `Signal<T>` that reactively reflects the current value, suitable
6573
+ * for direct use in Angular templates.
6574
+ *
6575
+ * Static values yield a `computed` that always returns the same constant —
6576
+ * preferable to wrapping in `signal()` because the computed never causes
6577
+ * change-detection invalidation.
6578
+ *
6579
+ * Pass an `injector` when calling outside an Angular injection context
6580
+ * (e.g., from inside a `computed`). `toSignal` needs a `DestroyRef` to
6581
+ * unsubscribe — without an injector it falls back to `inject(DestroyRef)`,
6582
+ * which throws when called outside injection context.
6583
+ *
6584
+ * @example
5518
6585
  * ```typescript
5519
- * provideDynamicForm(
5520
- * ...withMaterialFields(),
5521
- * withValueExclusionDefaults({ excludeValueIfReadonly: false })
5522
- * )
6586
+ * const isHidden = resolveDynamicValue(addon().hidden, false);
6587
+ * // template: \@if (isHidden()) { ... }
5523
6588
  * ```
6589
+ */
6590
+ declare function resolveDynamicValue<T>(value: DynamicValue<T> | undefined, fallback: T, injector?: Injector): Signal<T>;
6591
+
6592
+ /**
6593
+ * Register a custom addon kind with the form.
5524
6594
  *
5525
- * @example Disable all exclusions (restore pre-v1 behavior)
6595
+ * The provided definition is added to `ADDON_KIND_REGISTRY` at form
6596
+ * initialisation, alongside the built-in `text` / `template` / `component`
6597
+ * kinds and any kinds contributed by adapter feature helpers
6598
+ * (`withPrimeNGAddons()` etc.).
6599
+ *
6600
+ * @example
5526
6601
  * ```typescript
5527
6602
  * provideDynamicForm(
5528
6603
  * ...withMaterialFields(),
5529
- * withValueExclusionDefaults({
5530
- * excludeValueIfHidden: false,
5531
- * excludeValueIfDisabled: false,
5532
- * excludeValueIfReadonly: false,
5533
- * })
5534
- * )
6604
+ * withCustomAddon({
6605
+ * kind: 'lucide-icon',
6606
+ * // Match how field types register: a default-exported component lets
6607
+ * // `loadComponent` return the import promise directly.
6608
+ * loadComponent: () => import('./lucide-icon-addon.component'),
6609
+ * validate: (addon) => {
6610
+ * if (typeof addon.icon !== 'string') {
6611
+ * throw new Error('lucide-icon: icon string is required');
6612
+ * }
6613
+ * },
6614
+ * }),
6615
+ * );
5535
6616
  * ```
5536
6617
  *
5537
- * @remarks
5538
- * **Precedence rules:**
5539
- * 1. Per-field `excludeValueIf*` on `FieldDef` — wins for that field
5540
- * 2. Per-form `excludeValueIf*` on `FormOptions` — wins for all fields in that form
5541
- * 3. Global `withValueExclusionDefaults()` — baseline default
5542
- * 4. No global feature — uses token default (all enabled)
5543
- *
5544
- * @param config - Partial override of exclusion rules. Unspecified properties default to `true`.
5545
- * @returns A DynamicFormFeature that configures value exclusion defaults
5546
- *
5547
- * @public
6618
+ * Multiple `withCustomAddon(...)` calls are supported; later registrations
6619
+ * for the same kind override earlier ones (with a warning logged).
5548
6620
  */
5549
- declare function withValueExclusionDefaults(config?: Partial<ValueExclusionConfig>): DynamicFormFeature<'value-exclusion'>;
6621
+ declare function withCustomAddon<T extends BaseAddon>(definition: AddonKindDefinition<T>): DynamicFormFeature<'addons'>;
5550
6622
 
5551
6623
  /**
5552
- * Configuration for when validation runs based on a field's reactive state.
5553
- *
5554
- * Currently controls whether hidden fields are validated. Resolved through a
5555
- * tree-aware hierarchy: per-field overrides inherit down to descendants, with
5556
- * per-form `FormOptions` and global `withValidationExecutionDefaults()` providing
5557
- * the root inherited value.
6624
+ * Handler signature for user-registered addon actions.
5558
6625
  *
5559
- * @public
6626
+ * Backends reference handlers by name via `actionRef: 'name'` on button-style
6627
+ * addons; the dispatcher invokes the registered function with the addon's
6628
+ * action context.
5560
6629
  */
5561
- interface ValidationExecutionConfig {
5562
- /**
5563
- * Whether validators run while a field is hidden (statically, via `hidden`
5564
- * logic, or because any ancestor is hidden).
5565
- *
5566
- * When `false`, all validators on a hidden field are skipped — the field is
5567
- * effectively inert until shown. State logic (hidden/disabled/readonly) and
5568
- * value derivations continue to apply.
5569
- */
5570
- readonly validateWhenHidden?: boolean;
5571
- }
6630
+ type AddonActionHandler<TValue = unknown> = (ctx: AddonActionContext<TValue>) => void;
5572
6631
  /**
5573
- * Fully resolved validation execution config — every property defined.
6632
+ * Multi-provider token populated by `provideAddonActions({...})` calls.
6633
+ * `ADDON_ACTION_REGISTRY` aggregates these into a single name → handler map.
5574
6634
  *
5575
- * @public
6635
+ * @internal
5576
6636
  */
5577
- type ResolvedValidationExecutionConfig = Required<ValidationExecutionConfig>;
6637
+ declare const ADDON_ACTION_HANDLERS: InjectionToken<readonly Record<string, AddonActionHandler<unknown>>[]>;
6638
+ /**
6639
+ * Form-scoped name → handler map. Read by adapter button kinds at click
6640
+ * time for `actionRef: 'name'` lookups. Provided at form scope (not root)
6641
+ * because `ADDON_ACTION_HANDLERS` is form-scoped via `provideAddonActions`.
6642
+ */
6643
+ declare const ADDON_ACTION_REGISTRY: InjectionToken<ReadonlyMap<string, AddonActionHandler<unknown>>>;
5578
6644
 
5579
6645
  /**
5580
- * Configures global validation execution defaults — when validators should run
5581
- * relative to a field's reactive state.
6646
+ * Typed feature returned by {@link provideAddonActions}. Carries the
6647
+ * registered handler keys as a phantom type so consumers can derive the
6648
+ * `DynamicFormActionRegistry` augmentation in one line instead of hand-typing
6649
+ * every key.
5582
6650
  *
5583
- * By default, validators are **skipped** for hidden fields (`validateWhenHidden: false`),
5584
- * matching what most users expect: a hidden field is inert. Use this feature to
5585
- * restore the legacy behavior where hidden fields still validate.
6651
+ * Phantom field is `__handlerKeys` (the leading underscores signal "internal,
6652
+ * type-only — not for runtime access").
6653
+ */
6654
+ type AddonActionsFeature<K extends string = string> = DynamicFormFeature<'addon-actions'> & {
6655
+ /** @internal Phantom field carrying the registered keys; never read at runtime. */
6656
+ readonly __handlerKeys?: K;
6657
+ };
6658
+ /**
6659
+ * Register named action handlers reachable via JSON-driven addon configs.
5586
6660
  *
5587
- * @example Default behavior — no feature needed
5588
- * ```typescript
5589
- * provideDynamicForm(...withMaterialFields())
5590
- * // hidden fields skip validation
5591
- * ```
6661
+ * Once registered, a backend can ship
6662
+ * `{ kind: 'prime-button', actionRef: 'openProfile' }` and the FE will invoke
6663
+ * the corresponding handler at click time.
6664
+ *
6665
+ * Define handlers once and augment the registry from the same object:
5592
6666
  *
5593
- * @example Restore legacy validation-on-hidden behavior
5594
6667
  * ```typescript
5595
- * provideDynamicForm(
5596
- * ...withMaterialFields(),
5597
- * withValidationExecutionDefaults({ validateWhenHidden: true })
5598
- * )
6668
+ * export const appActions = provideAddonActions({
6669
+ * openProfile: (ctx) => modalService.open(ProfileModal, { data: ctx.value }),
6670
+ * runWorkflow: (ctx) => workflowService.run(ctx.field.key, ctx.value),
6671
+ * });
6672
+ *
6673
+ * declare module '@ng-forge/dynamic-forms' {
6674
+ * // Pulls the keys straight off `appActions` — no manual list to keep in sync.
6675
+ * interface DynamicFormActionRegistry extends Record<NonNullable<typeof appActions['__handlerKeys']>, true> {}
6676
+ * }
6677
+ *
6678
+ * // Then in your bootstrap:
6679
+ * provideDynamicForm(...withPrimeNGFields(), appActions);
5599
6680
  * ```
5600
6681
  *
5601
- * @remarks
5602
- * **Precedence rules:**
5603
- * 1. Per-field `FieldDef.validateWhenHidden` — wins for that field; inherits down to descendants
5604
- * that don't override.
5605
- * 2. Per-form `FormOptions.validateWhenHidden` — root inherited value for the form.
5606
- * 3. Global `withValidationExecutionDefaults()` — fallback when no form/field setting.
5607
- * 4. No feature configured — uses token default (`validateWhenHidden: false`).
6682
+ * Multiple `provideAddonActions(...)` calls are merged; later names override
6683
+ * earlier ones for the same key.
6684
+ */
6685
+ declare function provideAddonActions<const H extends Record<string, AddonActionHandler>>(handlers: H): AddonActionsFeature<keyof H & string>;
6686
+
6687
+ /**
6688
+ * Multi-provider token used by `withCustomAddon(...)` features to contribute
6689
+ * addon kind definitions. `provideDynamicForm` aggregates all values into
6690
+ * the form-scoped `ADDON_KIND_REGISTRY` map.
5608
6691
  *
5609
- * @param config - Partial override. Unspecified properties keep their defaults.
5610
- * @returns A `DynamicFormFeature` that configures validation execution defaults.
6692
+ * @internal
6693
+ */
6694
+ declare const ADDON_KIND_DEFINITIONS: InjectionToken<readonly AddonKindDefinition<_ng_forge_dynamic_forms.BaseAddon<_ng_forge_dynamic_forms.AddonSlot>>[]>;
6695
+
6696
+ /**
6697
+ * Reason an addon was dropped during validation.
5611
6698
  *
5612
- * @public
6699
+ * Each variant carries its own context fields so log messages can be both
6700
+ * actionable (registered kinds enumerated, fix suggestion present) and
6701
+ * machine-readable (admin UIs surfacing per-error guidance).
5613
6702
  */
5614
- declare function withValidationExecutionDefaults(config?: Partial<ValidationExecutionConfig>): DynamicFormFeature<'validation-execution'>;
6703
+ type AddonWarning = {
6704
+ type: 'unknown-field-type';
6705
+ fieldKey: string;
6706
+ fieldType: string;
6707
+ } | {
6708
+ type: 'field-type-no-addon-support';
6709
+ fieldKey: string;
6710
+ fieldType: string;
6711
+ } | {
6712
+ type: 'unknown-slot';
6713
+ fieldKey: string;
6714
+ fieldType: string;
6715
+ slot: string;
6716
+ allowedSlots: readonly string[];
6717
+ } | {
6718
+ type: 'unknown-kind';
6719
+ fieldKey: string;
6720
+ kind: string;
6721
+ registeredKinds: readonly string[];
6722
+ } | {
6723
+ type: 'kind-not-allowed';
6724
+ fieldKey: string;
6725
+ fieldType: string;
6726
+ kind: string;
6727
+ allowedKinds: readonly string[];
6728
+ } | {
6729
+ type: 'shape-violation';
6730
+ fieldKey: string;
6731
+ kind: string;
6732
+ reason: string;
6733
+ } | {
6734
+ type: 'code-only-kind-in-json';
6735
+ fieldKey: string;
6736
+ kind: string;
6737
+ } | {
6738
+ type: 'code-only-action-in-json';
6739
+ fieldKey: string;
6740
+ reason: string;
6741
+ };
6742
+ /**
6743
+ * Render an `AddonWarning` to a developer-friendly message.
6744
+ *
6745
+ * Returns the bare message — the `[Dynamic Forms]` prefix is added by the
6746
+ * library's logger (`ConsoleLogger`) and by `DynamicFormError`. Pass the
6747
+ * result of this function directly into `logger.warn(...)` to get the
6748
+ * standard prefixed line; use `logAddonWarnings()` for direct
6749
+ * `console.warn` output (which prefixes manually).
6750
+ *
6751
+ * Messages are designed to be actionable: they enumerate the relevant set
6752
+ * (registered kinds, allowed slots) and suggest a likely fix.
6753
+ */
6754
+ declare function formatAddonWarning(w: AddonWarning): string;
5615
6755
 
5616
6756
  /**
5617
- * Zero-cost type carrier used in wrapper registrations.
6757
+ * Walk a field's addons; return the survivors plus a list of warnings for
6758
+ * anything dropped.
5618
6759
  *
5619
- * Returns `undefined` at runtime but is typed as `T`, so TypeScript can
5620
- * thread a wrapper's config type through `createWrappers(...)` into the
5621
- * `InferWrapperRegistry<typeof ...>` utility without requiring users to
5622
- * hand-write the augmentation shape.
6760
+ * Pure: receives the registries as arguments (so the function is reusable
6761
+ * outside Angular's DI — tests, server-side validation, etc.).
6762
+ *
6763
+ * @param field The field to validate.
6764
+ * @param fieldRegistry Snapshot of `FIELD_REGISTRY` map.
6765
+ * @param kindRegistry Snapshot of `ADDON_KIND_REGISTRY` map.
6766
+ * @param source Whether the config originated from JSON (lenient drop
6767
+ * of code-only kinds) or inline TypeScript.
6768
+ */
6769
+ declare function validateFieldAddons(field: FieldDef<unknown>, fieldRegistry: ReadonlyMap<string, FieldTypeDefinition>, kindRegistry: ReadonlyMap<string, AddonKindDefinition>, source: 'json' | 'inline'): {
6770
+ addons: AnyAddon[];
6771
+ warnings: AddonWarning[];
6772
+ };
6773
+ /**
6774
+ * Recursively walk a field tree, validating each field's addons. Returns a
6775
+ * sanitized copy of the input field array plus accumulated warnings.
6776
+ */
6777
+ declare function walkAndValidateAddons(fields: readonly FieldDef<unknown>[], fieldRegistry: ReadonlyMap<string, FieldTypeDefinition>, kindRegistry: ReadonlyMap<string, AddonKindDefinition>, source: 'json' | 'inline'): {
6778
+ fields: FieldDef<unknown>[];
6779
+ warnings: AddonWarning[];
6780
+ };
6781
+
6782
+ /**
6783
+ * Options accepted by {@link sanitizeFormConfig}.
6784
+ */
6785
+ interface SanitizeFormConfigOptions {
6786
+ /**
6787
+ * Whether the config originated from JSON (server-driven) or inline
6788
+ * TypeScript code.
6789
+ *
6790
+ * - `'inline'` (default): keeps everything; assumes the config is
6791
+ * authored in code and code-only constructs (inline `action`
6792
+ * functions, `component` kind) are intentional. The default avoids
6793
+ * silently stripping inline behaviour during in-process authoring.
6794
+ * - `'json'`: drops code-only addon kinds (`component`, inline
6795
+ * `action: function`) with a warning, since they cannot survive
6796
+ * serialization. Use this when sanitising configs received from a
6797
+ * backend / form-builder UI.
6798
+ */
6799
+ source?: 'json' | 'inline';
6800
+ }
6801
+ /**
6802
+ * Result of a `sanitizeFormConfig` call.
6803
+ */
6804
+ interface SanitizedFormConfig {
6805
+ /** Sanitized config — invalid addons stripped from the field tree. */
6806
+ sanitized: FormConfig;
6807
+ /** Structured list of every addon that was dropped, and why. */
6808
+ warnings: readonly AddonWarning[];
6809
+ }
6810
+ /**
6811
+ * Walk a `FormConfig`, validate every field's addons against the active
6812
+ * `FIELD_REGISTRY` and `ADDON_KIND_REGISTRY`, and return a sanitized copy
6813
+ * plus a list of dropped-addon warnings.
6814
+ *
6815
+ * Distinct from the INTERNAL `validateFormConfig` in
6816
+ * `utils/config-validation/config-validator.ts` (which throws on
6817
+ * structural errors like duplicate keys / unknown field types). This
6818
+ * helper is the PUBLIC, lenient addon-shape pass — renamed to
6819
+ * `sanitizeFormConfig` to avoid the name collision.
6820
+ *
6821
+ * Lenient by design: invalid addons are dropped (with warnings) rather than
6822
+ * thrown. The form keeps rendering even when the backend ships an addon the
6823
+ * FE doesn't understand.
6824
+ *
6825
+ * `<df-dynamic-form>` already runs this internally at form init — calling
6826
+ * the public helper AND passing the original config to the form will
6827
+ * double-emit warnings. Use this entry point only when you need the
6828
+ * sanitized config or warning list outside the form (e.g., admin-UI
6829
+ * diagnostics, build-time lints).
6830
+ *
6831
+ * Must be called within an Angular injection context — typically inside
6832
+ * an admin-UI component or a `runInInjectionContext()` block on the server.
5623
6833
  *
5624
6834
  * @example
5625
6835
  * ```typescript
5626
- * const wrappers = createWrappers(
5627
- * {
5628
- * wrapperName: 'section',
5629
- * loadComponent: () => import('./section-wrapper'),
5630
- * props: wrapperProps<SectionWrapper>(),
5631
- * },
5632
- * );
5633
- *
5634
- * declare module '@ng-forge/dynamic-forms' {
5635
- * interface FieldRegistryWrappers extends InferWrapperRegistry<typeof wrappers> {}
6836
+ * @Component({ ... })
6837
+ * class AdminFormBuilder {
6838
+ * private readonly check = (cfg: FormConfig) => {
6839
+ * const { sanitized, warnings } = sanitizeFormConfig(cfg, { source: 'json' });
6840
+ * if (warnings.length > 0) {
6841
+ * this.diagnostics.set(warnings);
6842
+ * }
6843
+ * return sanitized;
6844
+ * };
5636
6845
  * }
5637
6846
  * ```
5638
6847
  */
5639
- declare function wrapperProps<T>(): T;
6848
+ declare function sanitizeFormConfig(config: FormConfig, options?: SanitizeFormConfigOptions): SanitizedFormConfig;
6849
+ /**
6850
+ * Default warning sink. Pass a `DynamicFormLogger` to route through the
6851
+ * form's configured logger (respects `NoopLogger` opt-out); omit it to
6852
+ * fall back to `console.warn` for build-time / admin-UI use cases that
6853
+ * run outside an Angular injection context.
6854
+ */
6855
+ declare function logAddonWarnings(warnings: readonly AddonWarning[], logger?: Logger): void;
6856
+ /**
6857
+ * DI-free variant of {@link sanitizeFormConfig} for tooling that lives
6858
+ * outside Angular's injector — build-time linters, MCP-side checks, CLI
6859
+ * scripts.
6860
+ *
6861
+ * Caller passes the registries explicitly; the result has the same shape
6862
+ * as the DI-bound version.
6863
+ */
6864
+ declare function sanitizeFormConfigPure(config: FormConfig, fieldRegistry: ReadonlyMap<string, FieldTypeDefinition>, kindRegistry: ReadonlyMap<string, AddonKindDefinition>, options?: SanitizeFormConfigOptions): SanitizedFormConfig;
5640
6865
 
5641
6866
  interface FieldWithValidation {
5642
6867
  readonly required?: boolean;
@@ -5722,21 +6947,35 @@ interface BaseValueField<TProps, TValue, TMeta extends FieldMeta = FieldMeta, TN
5722
6947
  nullable?: TNullable;
5723
6948
  }
5724
6949
  declare function isValueField<TProps, TMeta extends FieldMeta = FieldMeta>(field: FieldDef<TProps, TMeta>): field is BaseValueField<TProps, ValueType, TMeta, boolean>;
5725
- type ExcludedKeys$1 = 'type' | 'conditionals' | 'value' | 'valueType' | 'nullable' | 'disabled' | 'readonly' | 'hidden' | 'col' | 'minValue' | 'maxValue' | 'step' | 'required' | 'email' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'validators' | 'logic' | 'derivation' | 'schemas' | 'excludeValueIfHidden' | 'excludeValueIfDisabled' | 'excludeValueIfReadonly' | 'validateWhenHidden' | 'wrappers' | 'skipAutoWrappers' | 'skipDefaultWrappers';
6950
+ type ExcludedKeys$1 = 'type' | 'conditionals' | 'value' | 'valueType' | 'nullable' | 'disabled' | 'readonly' | 'hidden' | 'col' | 'minValue' | 'maxValue' | 'step' | 'required' | 'email' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'validators' | 'logic' | 'derivation' | 'schemas' | 'excludeValueIfHidden' | 'excludeValueIfDisabled' | 'excludeValueIfReadonly' | 'validateWhenHidden' | 'wrappers' | 'skipAutoWrappers' | 'skipDefaultWrappers' | 'addons';
5726
6951
  type ValueFieldComponent<T extends BaseValueField<Record<string, unknown> | unknown, unknown, FieldMeta, boolean>> = Prettify<WithInputSignals<Omit<T, ExcludedKeys$1>>>;
5727
6952
 
5728
- interface BaseCheckedField<TProps, TMeta extends FieldMeta = FieldMeta> extends FieldDef<TProps, TMeta>, FieldWithValidation {
5729
- value?: boolean;
6953
+ interface BaseCheckedField<TProps, TMeta extends FieldMeta = FieldMeta, TNullable extends boolean = false> extends FieldDef<TProps, TMeta>, FieldWithValidation {
6954
+ value?: TNullable extends true ? boolean | null : boolean;
5730
6955
  /**
5731
6956
  * Placeholder text displayed when the field is empty.
5732
6957
  * Supports static strings, Observables, and Signals for dynamic content.
5733
6958
  */
5734
6959
  placeholder?: DynamicText;
5735
6960
  required?: boolean;
6961
+ /**
6962
+ * Whether the field accepts `null` as a valid value.
6963
+ *
6964
+ * When `true`, `value` may be `null` and an omitted `value` resolves to `null`
6965
+ * (rather than the type-specific empty default of `false`). Orthogonal to `required`.
6966
+ *
6967
+ * The data model is decoupled from the UI: a null value is a legitimate
6968
+ * "undecided" state at the storage / OpenAPI layer. Whether the rendered
6969
+ * checkbox shows an indeterminate visual is controlled separately via
6970
+ * `props.indeterminate` and is not driven by this flag.
6971
+ *
6972
+ * @default false
6973
+ */
6974
+ nullable?: TNullable;
5736
6975
  }
5737
- declare function isCheckedField<TProps, TMeta extends FieldMeta = FieldMeta>(field: FieldDef<TProps, TMeta>): field is BaseCheckedField<TProps, TMeta>;
5738
- type ExcludedKeys = 'type' | 'conditionals' | 'value' | 'disabled' | 'readonly' | 'hidden' | 'col' | keyof FieldWithValidation | 'excludeValueIfHidden' | 'excludeValueIfDisabled' | 'excludeValueIfReadonly' | 'validateWhenHidden' | 'wrappers' | 'skipAutoWrappers' | 'skipDefaultWrappers';
5739
- type CheckedFieldComponent<T extends BaseCheckedField<Record<string, unknown> | unknown>> = Prettify<WithInputSignals<Omit<T, ExcludedKeys>>>;
6976
+ declare function isCheckedField<TProps, TMeta extends FieldMeta = FieldMeta>(field: FieldDef<TProps, TMeta>): field is BaseCheckedField<TProps, TMeta, boolean>;
6977
+ type ExcludedKeys = 'type' | 'conditionals' | 'value' | 'nullable' | 'disabled' | 'readonly' | 'hidden' | 'col' | keyof FieldWithValidation | 'excludeValueIfHidden' | 'excludeValueIfDisabled' | 'excludeValueIfReadonly' | 'validateWhenHidden' | 'wrappers' | 'skipAutoWrappers' | 'skipDefaultWrappers' | 'addons';
6978
+ type CheckedFieldComponent<T extends BaseCheckedField<Record<string, unknown> | unknown, FieldMeta, boolean>> = Prettify<WithInputSignals<Omit<T, ExcludedKeys>>>;
5740
6979
 
5741
6980
  declare function applyValidator(config: ValidatorConfig, fieldPath: SchemaPath<any> | SchemaPathTree<any>): void;
5742
6981
  declare function applyValidators(configs: ValidatorConfig[], fieldPath: SchemaPath<any> | SchemaPathTree<any>): void;
@@ -6852,71 +8091,6 @@ declare class ContainerFieldComponent {
6852
8091
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContainerFieldComponent, "div[container-field]", never, { "field": { "alias": "field"; "required": true; "isSignal": true; }; "key": { "alias": "key"; "required": true; "isSignal": true; }; "className": { "alias": "className"; "required": false; "isSignal": true; }; "hidden": { "alias": "hidden"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
6853
8092
  }
6854
8093
 
6855
- /**
6856
- * Type representing a FieldTree for an array.
6857
- * Extends FieldTree to add numeric indexing for accessing item FieldTrees.
6858
- */
6859
- type ArrayFieldTree<T> = FieldTree<T[]> & {
6860
- readonly [index: number]: FieldTree<T> | undefined;
6861
- };
6862
- /**
6863
- * Get the length of an array FieldTree.
6864
- */
6865
- declare function getArrayLength<T>(arrayFieldTree: ArrayFieldTree<T>): number;
6866
- /**
6867
- * Read-only view of a single field's observable state.
6868
- *
6869
- * Whitelisted read signals copied from Angular Signal Forms' `FieldState` so wrappers
6870
- * can observe a field without being able to mutate it. New write-capable members
6871
- * added in future Angular versions are excluded by default (the `Pick` list stays
6872
- * the source of truth).
6873
- *
6874
- * `value` is narrowed from `WritableSignal<TValue>` to `Signal<TValue>` so consumers
6875
- * cannot write through it.
6876
- */
6877
- interface ReadonlyFieldTree<TValue = unknown> {
6878
- readonly value: Signal<TValue>;
6879
- readonly valid: Signal<boolean>;
6880
- readonly invalid: Signal<boolean>;
6881
- readonly touched: Signal<boolean>;
6882
- readonly dirty: Signal<boolean>;
6883
- readonly required: Signal<boolean>;
6884
- readonly disabled: Signal<boolean>;
6885
- readonly hidden: Signal<boolean>;
6886
- readonly errors: Signal<readonly unknown[]>;
6887
- }
6888
- /**
6889
- * Build a `ReadonlyFieldTree` by extracting the whitelisted read signals from a
6890
- * Signal Forms `FieldTree`. Returns a fresh plain object — no casting, no proxying,
6891
- * so consumers only see the narrow surface and Angular's `WritableSignal` capability
6892
- * on `value` is hidden.
6893
- */
6894
- declare function toReadonlyFieldTree<TValue>(field: FieldTree<TValue>): ReadonlyFieldTree<TValue>;
6895
-
6896
- /**
6897
- * Input shape passed to a wrapper component via the `fieldInputs` input.
6898
- *
6899
- * Carries the wrapped field's mapper outputs plus an optional `field`
6900
- * read-only view. Wrappers reached through a container path (which has
6901
- * no FieldTree of its own) receive `fieldInputs === undefined`, so any
6902
- * wrapper that reads `fieldInputs.field` must guard for it.
6903
- *
6904
- * Mappers must emit new rawInputs objects per tick rather than mutating
6905
- * the previous one — this bag is a shallow spread, so mutations to shared
6906
- * nested values leak to downstream wrappers.
6907
- */
6908
- interface WrapperFieldInputs {
6909
- readonly key: string;
6910
- readonly label?: string;
6911
- readonly placeholder?: string;
6912
- readonly className?: string;
6913
- readonly props?: Record<string, unknown>;
6914
- readonly validationMessages?: Record<string, string>;
6915
- readonly defaultValidationMessages?: Record<string, string>;
6916
- readonly field?: ReadonlyFieldTree;
6917
- readonly [key: string]: unknown;
6918
- }
6919
-
6920
8094
  /**
6921
8095
  * Native JavaScript utility functions to replace lodash-es
6922
8096
  * These are simple implementations for common object manipulation patterns
@@ -6995,5 +8169,5 @@ declare class RootFormRegistryService {
6995
8169
  */
6996
8170
  declare const INITIALIZATION_TIMEOUT_MS: InjectionToken<number>;
6997
8171
 
6998
- export { ARRAY_CONTEXT, AppendArrayItemEvent, ArrayFieldComponent, BUILT_IN_FIELDS, ConsoleLogger, ContainerFieldComponent, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, DEFAULT_WRAPPERS, DynamicForm, DynamicFormError, DynamicFormLogger, DynamicTextPipe, EventBus, EventDispatcher, FIELD_REGISTRY, FIELD_SIGNAL_CONTEXT, FORM_OPTIONS, FormClearEvent, FormResetEvent, GROUP_CONTEXT, GroupFieldComponent, INITIALIZATION_TIMEOUT_MS, InsertArrayItemEvent, MoveArrayItemEvent, NextPageEvent, NoopLogger, PageChangeEvent, PopArrayItemEvent, PrependArrayItemEvent, PreviousPageEvent, RemoveAtIndexEvent, RootFormRegistryService, ShiftArrayItemEvent, SubmitEvent, WRAPPER_REGISTRY, applyMetaToElement, applyValidator, applyValidators, arrayEvent, arrayFieldMapper, baseFieldMapper, buildBaseInputs, containerFieldMapper, createField, createWrappers, dynamicTextToObservable, evaluateNonFieldDisabled, evaluateNonFieldHidden, field, formConfig, getArrayLength, groupFieldMapper, hasFormValue, interpolateParams, isArrayField, isCheckedField, isContainerField, isContainerTypedField, isDisplayOnlyField, isEqual, isFormStateCondition, isGroupField, isLeafField, isPageField, isRowField, isSimplifiedArrayField, isValueBearingField, isValueField, isWrapperTypeDefinition, isWrappersBundle, omit, pageFieldMapper, provideDynamicForm, resolveNextButtonDisabled, resolveNonFieldDisabled, resolveNonFieldHidden, resolveSubmitButtonDisabled, resolveTokens, rowFieldMapper, textFieldMapper, toReadonlyFieldTree, withEventFormValue, withLoggerConfig, withPreviousValue, withValidationExecutionDefaults, withValueExclusionDefaults, wrapperProps };
6999
- export type { ArrayAllowedChildren, ArrayButtonConfig, ArrayContext, ArrayField, ArrayFieldTree, ArrayItemContext, ArrayItemDefinitionTemplate, ArrayItemTemplate, AsyncCondition, AsyncConditionFunction, AsyncCustomValidator, AsyncDerivationFunction, AsyncValidatorConfig, AvailableFieldTypes, BaseCheckedField, BaseValidatorConfig, BaseValueField, BuiltInValidatorConfig, ButtonLogicContext, CheckedFieldComponent, ConditionalExpression, ContainerAllowedChildren, ContainerField, ContainerFieldTypes, CssWrapper, CustomFnConfig, CustomFunction, CustomValidator, CustomValidatorConfig, DeclarativeHttpValidatorConfig, DynamicFormFieldRegistry, DynamicText, EvaluationContext, ExtractField, ExtractFieldDefs, ExtractFormValue, FieldComponent, FieldDef, FieldMeta, FieldOption, FieldPathAccess, FieldRegistryContainers, FieldRegistryLeaves, FieldRegistryWrappers, FieldScope, FieldSignalContext, FieldStateContext, FieldStateInfo, FieldTypeDefinition, FieldWithValidation, FieldWrapper, FormConfig, FormEvent, FormEventConstructor, FormFieldStateMap, FormMode, FormModeDetectionResult, FormOptions, FormStateCondition, GroupAllowedChildren, GroupField, HttpCondition, HttpCustomValidator, HttpRequestConfig, HttpResourceRequest, HttpValidationResponseMapping, InferFormValue$1 as InferFormValue, InferWrapperRegistry, LeafFieldTypes, Logger, LogicConfig, MapperFn, NarrowField, NarrowFields, NextButtonOptions, NonFieldLogicConfig, NonFieldLogicContext, NonFieldLogicType, PageAllowedChildren, PageField, Prettify, ReadonlyFieldTree, RegisteredFieldTypes, ResolvedValidationExecutionConfig, ResolvedValueExclusionConfig, RowAllowedChildren, RowField, SchemaApplicationConfig, SchemaDefinition, SimplifiedArrayField, StateLogicConfig, SubmissionActionResult, SubmissionConfig, SubmitButtonOptions, TextElementType, TextField, TextProps, TokenContext, ValidationError, ValidationExecutionConfig, ValidationMessages, ValidatorConfig, ValueExclusionConfig, ValueFieldComponent, ValueHandlingMode, ValueType, WithInputSignals, WrapperConfig, WrapperFieldInputs, WrapperRegistration, WrapperTypeDefinition, WrappersBundle };
8172
+ export { ADDON_ACTION_HANDLERS, ADDON_ACTION_REGISTRY, ADDON_KIND_COMPONENT_CACHE, ADDON_KIND_DEFINITIONS, ADDON_KIND_REGISTRY, ARRAY_CONTEXT, AppendArrayItemEvent, ArrayFieldComponent, BUILT_IN_FIELDS, ComponentAddonComponent, ConsoleLogger, ContainerFieldComponent, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, DEFAULT_WRAPPERS, DF_FIELD_TEMPLATES, DfAddonSlot, DfTemplate, DynamicForm, DynamicFormError, DynamicFormLogger, DynamicTextPipe, EventBus, EventDispatcher, FIELD_REGISTRY, FIELD_SIGNAL_CONTEXT, FORM_OPTIONS, FormClearEvent, FormResetEvent, GROUP_CONTEXT, GroupFieldComponent, INITIALIZATION_TIMEOUT_MS, InsertArrayItemEvent, MoveArrayItemEvent, NextPageEvent, NoopLogger, PageChangeEvent, PopArrayItemEvent, PrependArrayItemEvent, PreviousPageEvent, RemoveAtIndexEvent, RootFormRegistryService, ShiftArrayItemEvent, SubmitEvent, TemplateAddonComponent, TextAddonComponent, WRAPPER_REGISTRY, applyMetaToElement, applyValidator, applyValidators, arrayEvent, arrayFieldMapper, baseFieldMapper, buildBaseInputs, containerFieldMapper, createField, createWrappers, dynamicTextToObservable, evaluateNonFieldDisabled, evaluateNonFieldHidden, field, formConfig, formatAddonWarning, getArrayLength, groupFieldMapper, hasFormValue, injectAddonKindRegistry, injectFieldSignalContext, injectFieldsSupportingAddons, interpolateParams, isArrayField, isCheckedField, isContainerField, isContainerTypedField, isDisplayOnlyField, isEqual, isFieldBoundContext, isFormStateCondition, isGroupField, isLeafField, isPageField, isRowField, isSimplifiedArrayField, isValueBearingField, isValueField, isWrapperTypeDefinition, isWrappersBundle, logAddonWarnings, omit, pageFieldMapper, provideAddonActions, provideDynamicForm, resolveDynamicValue, resolveNextButtonDisabled, resolveNonFieldDisabled, resolveNonFieldHidden, resolveSubmitButtonDisabled, resolveTokens, rowFieldMapper, runPresetAction, sanitizeFormConfig, sanitizeFormConfigPure, textFieldMapper, toReadonlyFieldTree, validateFieldAddons, walkAndValidateAddons, withCustomAddon, withEventFormValue, withLegacyStatusClasses, withLoggerConfig, withPreviousValue, withValidationExecutionDefaults, withValueExclusionDefaults, wrapperProps, writeToFieldValue };
8173
+ export type { AddonActionContext, AddonActionHandler, AddonActionPreset, AddonActionsFeature, AddonKindDefinition, AddonKindSchema, AddonShapeValidator, AddonSlot, AddonWarning, AnyAddon, ArrayAllowedChildren, ArrayButtonConfig, ArrayContext, ArrayField, ArrayFieldTree, ArrayItemContext, ArrayItemDefinitionTemplate, ArrayItemTemplate, AsyncCondition, AsyncConditionFunction, AsyncCustomValidator, AsyncDerivationFunction, AsyncValidatorConfig, AvailableFieldTypes, BaseAddon, BaseCheckedField, BaseValidatorConfig, BaseValueField, BuiltInValidatorConfig, ButtonLogicContext, CheckedFieldComponent, CommonAddonActionPreset, CommonAddonSlot, ComponentAddon, ConditionalExpression, ContainerAllowedChildren, ContainerField, ContainerFieldTypes, CssWrapper, CustomFnConfig, CustomFunction, CustomValidator, CustomValidatorConfig, DeclarativeHttpValidatorConfig, DynamicFormActionRegistry, DynamicFormAddonActionPresetRegistry, DynamicFormAddonRegistry, DynamicFormAddonSlotRegistry, DynamicFormFieldRegistry, DynamicText, DynamicValue, EvaluationContext, ExtractField, ExtractFieldDefs, ExtractFormValue, FieldAddonSupport, FieldAddonSupportEntry, FieldBoundAddonActionContext, FieldComponent, FieldDef, FieldMeta, FieldOption, FieldPathAccess, FieldRegistryContainers, FieldRegistryLeaves, FieldRegistryWrappers, FieldScope, FieldSignalContext, FieldStateContext, FieldStateInfo, FieldTypeDefinition, FieldWithValidation, FieldWrapper, FormConfig, FormEvent, FormEventConstructor, FormFieldStateMap, FormMode, FormModeDetectionResult, FormOptions, FormStateCondition, GroupAllowedChildren, GroupField, HttpCondition, HttpCustomValidator, HttpRequestConfig, HttpResourceRequest, HttpValidationResponseMapping, InferFormValue$1 as InferFormValue, InferWrapperRegistry, LeafFieldTypes, Logger, LogicConfig, MapperFn, NarrowField, NarrowFields, NextButtonOptions, NonFieldLogicConfig, NonFieldLogicContext, NonFieldLogicType, OrphanAddonActionContext, PageAllowedChildren, PageField, PresetCollaborators, Prettify, ReadonlyFieldTree, RegisteredActionRef, RegisteredFieldTypes, ResolvedValidationExecutionConfig, ResolvedValueExclusionConfig, RowAllowedChildren, RowField, SanitizeFormConfigOptions, SanitizedFormConfig, SchemaApplicationConfig, SchemaDefinition, SimplifiedArrayField, StateLogicConfig, SubmissionActionResult, SubmissionConfig, SubmitButtonOptions, TemplateAddon, TextAddon, TextElementType, TextField, TextProps, TokenContext, ValidationError, ValidationExecutionConfig, ValidationMessages, ValidatorConfig, ValueExclusionConfig, ValueFieldComponent, ValueHandlingMode, ValueType, WithInputSignals, WrapperConfig, WrapperFieldInputs, WrapperRegistration, WrapperTypeDefinition, WrappersBundle };