@ng-forge/dynamic-forms 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/README.md +6 -4
  2. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +121 -4
  3. package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
  4. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-B_676NHl.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-CMSmISSp.mjs} +561 -331
  5. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CMSmISSp.mjs.map +1 -0
  6. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs → ng-forge-dynamic-forms-page-field.component-rdGD8A9d.mjs} +6 -6
  7. package/fesm2022/ng-forge-dynamic-forms-page-field.component-rdGD8A9d.mjs.map +1 -0
  8. package/fesm2022/{ng-forge-dynamic-forms-testing-test-button.harness-BenmCFcM.mjs → ng-forge-dynamic-forms-testing-test-button.harness-0iXHLraC.mjs} +7 -7
  9. package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-0iXHLraC.mjs.map +1 -0
  10. package/fesm2022/ng-forge-dynamic-forms-testing.mjs +41 -38
  11. package/fesm2022/ng-forge-dynamic-forms-testing.mjs.map +1 -1
  12. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CiYq73aF.mjs → ng-forge-dynamic-forms-text-field.component-CDxka2KD.mjs} +5 -5
  13. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CiYq73aF.mjs.map → ng-forge-dynamic-forms-text-field.component-CDxka2KD.mjs.map} +1 -1
  14. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  15. package/package.json +4 -4
  16. package/types/ng-forge-dynamic-forms-integration.d.ts +237 -9
  17. package/types/ng-forge-dynamic-forms-testing.d.ts +67 -35
  18. package/types/ng-forge-dynamic-forms.d.ts +263 -99
  19. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-B_676NHl.mjs.map +0 -1
  20. package/fesm2022/ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs.map +0 -1
  21. package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-BenmCFcM.mjs.map +0 -1
@@ -1,11 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { isSignal, computed, runInInjectionContext, InjectionToken, inject, isDevMode, signal, Injectable, untracked, input, linkedSignal, ChangeDetectionStrategy, Component, DestroyRef, Injector, model, afterNextRender, makeEnvironmentProviders, Pipe } from '@angular/core';
2
+ import { isSignal, computed, runInInjectionContext, InjectionToken, inject, signal, Injectable, untracked, input, linkedSignal, ChangeDetectionStrategy, Component, DestroyRef, Injector, model, afterNextRender, makeEnvironmentProviders, Pipe } from '@angular/core';
3
3
  import { NgComponentOutlet } from '@angular/common';
4
- import { submit, email, min, max, minLength, maxLength, pattern, required, validate, validateAsync, validateHttp, disabled, readonly, hidden, applyEach, applyWhenValue, applyWhen, apply, schema, validateTree, form } from '@angular/forms/signals';
4
+ import { submit, email, min, max, minLength, maxLength, pattern, required, validate, validateAsync, validateHttp, disabled, readonly, hidden, applyEach, applyWhenValue, applyWhen, apply, schema, validateTree, form, provideSignalFormsConfig } from '@angular/forms/signals';
5
5
  import { toObservable, toSignal, takeUntilDestroyed, outputFromObservable } from '@angular/core/rxjs-interop';
6
6
  import { defer, combineLatest, from, map, catchError, of, pipe, switchMap, forkJoin, scan, isObservable, firstValueFrom, EMPTY, Subject, filter } from 'rxjs';
7
7
  import { scan as scan$1, map as map$1, filter as filter$1, take, switchMap as switchMap$1, shareReplay } from 'rxjs/operators';
8
8
  import { explicitEffect } from 'ngxtension/explicit-effect';
9
+ import { NG_STATUS_CLASSES } from '@angular/forms/signals/compat';
9
10
 
10
11
  /**
11
12
  * Type guard to check if a value is an array of Signals.
@@ -73,27 +74,12 @@ function derivedFromDeferred(source, operator, options) {
73
74
  * // Returns: ''
74
75
  * ```
75
76
  *
76
- * @example
77
- * ```typescript
78
- * // Usage in component template
79
- * @Component({
80
- * template: `
81
- * <div [class]="getGridClassString(field)" class="form-field">
82
- * <!-- Field content -->
83
- * </div>
84
- * `
85
- * })
86
- * export class FieldComponent {
87
- * field: FieldDef<unknown>;
88
- * getGridClassString = getGridClassString;
89
- * }
90
- * ```
91
- *
92
77
  * @public
93
78
  */
94
79
  function getGridClassString(fieldDef) {
95
- if (typeof fieldDef.col === 'number' && Number.isInteger(fieldDef.col) && fieldDef.col > 0 && fieldDef.col <= 12) {
96
- return `df-col-${fieldDef.col}`;
80
+ const col = fieldDef.col;
81
+ if (typeof col === 'number' && Number.isInteger(col) && col > 0 && col <= 12) {
82
+ return `df-col-${col}`;
97
83
  }
98
84
  return '';
99
85
  }
@@ -108,7 +94,7 @@ function getGridClassString(fieldDef) {
108
94
  * @returns Record of input names to values
109
95
  */
110
96
  function buildBaseInputs(fieldDef) {
111
- const { key, label, className, tabIndex, props } = fieldDef;
97
+ const { key, label, className, tabIndex, props, meta } = fieldDef;
112
98
  const inputs = {};
113
99
  // Always include key - required by components for accessibility and identification
114
100
  inputs['key'] = key;
@@ -133,6 +119,9 @@ function buildBaseInputs(fieldDef) {
133
119
  if (props !== undefined) {
134
120
  inputs['props'] = props;
135
121
  }
122
+ if (meta !== undefined) {
123
+ inputs['meta'] = meta;
124
+ }
136
125
  return inputs;
137
126
  }
138
127
  /**
@@ -150,6 +139,43 @@ function baseFieldMapper(fieldDef) {
150
139
  return computed(() => buildBaseInputs(fieldDef));
151
140
  }
152
141
 
142
+ /**
143
+ * Merges forwarded props into meta, with meta taking precedence.
144
+ *
145
+ * @param inputs The current inputs record
146
+ * @param propsToMeta Array of prop keys to forward to meta
147
+ * @returns Updated inputs record with merged meta
148
+ */
149
+ function mergeForwardedPropsToMeta(inputs, propsToMeta) {
150
+ const props = inputs['props'];
151
+ if (!props || propsToMeta.length === 0) {
152
+ return inputs;
153
+ }
154
+ // Extract values to forward from props
155
+ // Values are typed as FieldMeta-compatible since they're forwarded attributes (type, rows, cols, etc.)
156
+ const forwardedValues = {};
157
+ for (const key of propsToMeta) {
158
+ const value = props[key];
159
+ if (value !== undefined && value !== null) {
160
+ // Props being forwarded are known HTML attributes (string, number, boolean)
161
+ forwardedValues[key] = value;
162
+ }
163
+ }
164
+ // If nothing to forward, return inputs unchanged
165
+ if (Object.keys(forwardedValues).length === 0) {
166
+ return inputs;
167
+ }
168
+ // Merge: forwarded props first, then meta (meta wins)
169
+ const existingMeta = inputs['meta'];
170
+ const mergedMeta = {
171
+ ...forwardedValues,
172
+ ...existingMeta,
173
+ };
174
+ return {
175
+ ...inputs,
176
+ meta: mergedMeta,
177
+ };
178
+ }
153
179
  /**
154
180
  * Main field mapper function that uses the field registry to get the appropriate mapper
155
181
  * based on the field's type property.
@@ -157,30 +183,49 @@ function baseFieldMapper(fieldDef) {
157
183
  * This function must be called within an injection context where FIELD_SIGNAL_CONTEXT
158
184
  * is provided, as mappers inject the context to access form state.
159
185
  *
186
+ * For componentless fields (no loadComponent and no mapper), returns undefined
187
+ * since there's no component to bind inputs to. Callers should check for undefined
188
+ * and skip rendering logic for such fields.
189
+ *
190
+ * If the field type definition specifies `propsToMeta`, the specified props
191
+ * will be merged into the meta object (with meta taking precedence).
192
+ *
160
193
  * @param fieldDef The field definition to map
161
194
  * @param fieldRegistry The registry of field type definitions
162
- * @returns Signal containing Record of input names to values for ngComponentOutlet
195
+ * @returns Signal containing Record of input names to values, or undefined for componentless fields
163
196
  */
164
197
  function mapFieldToInputs(fieldDef, fieldRegistry) {
165
198
  // Get the field type definition from registry
166
199
  const fieldType = fieldRegistry.get(fieldDef.type);
167
- if (fieldType?.mapper) {
168
- // Use the registered mapper for this field type
169
- // Mapper will inject FIELD_SIGNAL_CONTEXT internally
170
- return fieldType.mapper(fieldDef);
200
+ // Componentless field (no mapper and no loadComponent) - nothing to map
201
+ if (fieldType && !fieldType.loadComponent && !fieldType.mapper) {
202
+ return undefined;
203
+ }
204
+ // Get the base mapper result
205
+ const mapperResult = fieldType?.mapper ? fieldType.mapper(fieldDef) : baseFieldMapper(fieldDef);
206
+ // If no props forwarding configured, return as-is
207
+ const propsToMeta = fieldType?.propsToMeta;
208
+ if (!propsToMeta || propsToMeta.length === 0) {
209
+ return mapperResult;
171
210
  }
172
- // Fallback to base mapper
173
- // Mapper will inject FIELD_SIGNAL_CONTEXT internally
174
- return baseFieldMapper(fieldDef);
211
+ // Wrap in computed to merge forwarded props into meta
212
+ return computed(() => {
213
+ const inputs = mapperResult();
214
+ return mergeForwardedPropsToMeta(inputs, propsToMeta);
215
+ });
175
216
  }
176
217
 
177
218
  /**
178
219
  * Resolves a single field definition to a ResolvedField using RxJS.
179
220
  * Loads the component asynchronously and maps inputs in the injection context.
180
221
  *
222
+ * For componentless fields (e.g., hidden fields), returns undefined since
223
+ * there's nothing to render. These fields still contribute to form values
224
+ * through the form schema.
225
+ *
181
226
  * @param fieldDef - The field definition to resolve
182
227
  * @param context - The context containing dependencies for resolution
183
- * @returns Observable that emits ResolvedField or undefined on error
228
+ * @returns Observable that emits ResolvedField or undefined (for componentless fields or on error)
184
229
  */
185
230
  function resolveField(fieldDef, context) {
186
231
  return from(context.loadTypeComponent(fieldDef.type)).pipe(map((component) => {
@@ -188,8 +233,16 @@ function resolveField(fieldDef, context) {
188
233
  if (context.destroyRef.destroyed) {
189
234
  return undefined;
190
235
  }
236
+ // Componentless fields (e.g., hidden) return undefined - nothing to render
237
+ if (!component) {
238
+ return undefined;
239
+ }
191
240
  // Run mapper in injection context
192
241
  const inputs = runInInjectionContext(context.injector, () => mapFieldToInputs(fieldDef, context.registry));
242
+ // Fields with components should always have inputs (componentless fields are handled above)
243
+ if (!inputs) {
244
+ return undefined;
245
+ }
193
246
  return {
194
247
  key: fieldDef.key,
195
248
  component,
@@ -248,7 +301,7 @@ function createFieldResolutionPipe(getContext) {
248
301
  * @param action - The submission action function
249
302
  * @returns A wrapped function that always returns a Promise
250
303
  */
251
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
304
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Form tree type is dynamic, action signatures vary
252
305
  function wrapSubmissionAction(action) {
253
306
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
254
307
  return (formTree) => {
@@ -399,11 +452,11 @@ function memoize(fn, resolverOrOptions) {
399
452
  const cache = new Map();
400
453
  return ((...args) => {
401
454
  const key = resolver ? resolver(...args) : JSON.stringify(args);
402
- if (cache.has(key)) {
403
- const value = cache.get(key);
455
+ const cachedValue = cache.get(key);
456
+ if (cachedValue !== undefined) {
404
457
  cache.delete(key);
405
- cache.set(key, value);
406
- return value;
458
+ cache.set(key, cachedValue);
459
+ return cachedValue;
407
460
  }
408
461
  const result = fn(...args);
409
462
  if (cache.size >= maxSize) {
@@ -534,6 +587,21 @@ const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT', {
534
587
  */
535
588
  const ARRAY_CONTEXT = new InjectionToken('ARRAY_CONTEXT');
536
589
 
590
+ /**
591
+ * Base error class for all Dynamic Forms errors.
592
+ *
593
+ * This class centralizes the `[Dynamic Forms]` prefix, ensuring consistent
594
+ * error messaging across the library without requiring each error site to
595
+ * manually include the prefix.
596
+ */
597
+ class DynamicFormError extends Error {
598
+ static PREFIX = '[Dynamic Forms]';
599
+ constructor(message) {
600
+ super(`${DynamicFormError.PREFIX} ${message}`);
601
+ this.name = 'DynamicFormError';
602
+ }
603
+ }
604
+
537
605
  /**
538
606
  * Injection token for the global field type registry.
539
607
  *
@@ -646,37 +714,41 @@ function injectFieldRegistry() {
646
714
  * Handles both synchronous component references and asynchronous
647
715
  * dynamic imports. Automatically extracts default exports from ES modules.
648
716
  *
717
+ * Returns `undefined` for componentless field types (e.g., hidden fields)
718
+ * that only contribute to form values without rendering UI.
719
+ *
649
720
  * @param name - The name of the field type to load
650
- * @returns Promise resolving to the component constructor
721
+ * @returns Promise resolving to the component constructor, or undefined for componentless fields
651
722
  * @throws {Error} When field type is not registered or loading fails
652
723
  *
653
724
  * @example
654
725
  * ```typescript
655
- * try {
656
- * const InputComponent = await fieldRegistry.loadTypeComponent('input');
657
- * const componentRef = vcr.createComponent(InputComponent);
658
- * } catch (error) {
659
- * console.error('Failed to load component:', error);
726
+ * const component = await fieldRegistry.loadTypeComponent('input');
727
+ * if (component) {
728
+ * const componentRef = vcr.createComponent(component);
660
729
  * }
730
+ * // For componentless fields like 'hidden', component will be undefined
661
731
  * ```
662
732
  */
663
733
  async loadTypeComponent(name) {
664
734
  const fieldType = registry.get(name);
665
735
  if (!fieldType) {
666
- throw new Error(`[Dynamic Forms] Field type "${name}" is not registered`);
736
+ throw new DynamicFormError(`Field type "${name}" is not registered`);
737
+ }
738
+ // Componentless field types (e.g., hidden) don't have loadComponent
739
+ if (!fieldType.loadComponent) {
740
+ return undefined;
667
741
  }
668
742
  // Handle async loading
669
- if (fieldType.loadComponent) {
670
- try {
671
- const result = await fieldType.loadComponent();
672
- // Handle ES module imports - extract default export if needed
673
- return result?.default || result;
674
- }
675
- catch (error) {
676
- throw new Error(`[Dynamic Forms] Failed to load component for field type "${name}": ${error}`);
677
- }
743
+ try {
744
+ const result = await fieldType.loadComponent();
745
+ // Handle ES module imports - extract default export if needed
746
+ const moduleResult = result;
747
+ return (typeof moduleResult === 'object' && 'default' in moduleResult && moduleResult.default) || result;
748
+ }
749
+ catch (error) {
750
+ throw new DynamicFormError(`Failed to load component for field type "${name}": ${error}`);
678
751
  }
679
- throw new Error(`[Dynamic Forms] Field type "${name}" has no component or loadComponent function`);
680
752
  },
681
753
  /**
682
754
  * Gets all registered field type definitions.
@@ -735,100 +807,13 @@ function injectFieldRegistry() {
735
807
  };
736
808
  }
737
809
 
738
- /**
739
- * Log levels for the dynamic forms logger.
740
- * Each level includes all levels above it (more severe).
741
- *
742
- * - Off: No logging
743
- * - Error: Only errors
744
- * - Warn: Errors + warnings
745
- * - Info: Errors + warnings + info
746
- * - Debug: All messages including debug
747
- */
748
- var LogLevel;
749
- (function (LogLevel) {
750
- /** Disable all logging */
751
- LogLevel[LogLevel["Off"] = 0] = "Off";
752
- /** Only log errors */
753
- LogLevel[LogLevel["Error"] = 1] = "Error";
754
- /** Log errors and warnings */
755
- LogLevel[LogLevel["Warn"] = 2] = "Warn";
756
- /** Log errors, warnings, and info messages */
757
- LogLevel[LogLevel["Info"] = 3] = "Info";
758
- /** Log all messages including debug */
759
- LogLevel[LogLevel["Debug"] = 4] = "Debug";
760
- })(LogLevel || (LogLevel = {}));
761
-
762
- /**
763
- * Console-based logger implementation.
764
- * Respects log level configuration to filter messages.
765
- */
766
- class ConsoleLogger {
767
- level;
768
- prefix = '[Dynamic Forms]';
769
- constructor(level) {
770
- this.level = level;
771
- }
772
- debug(message, ...args) {
773
- if (this.level >= LogLevel.Debug) {
774
- console.debug(this.prefix, message, ...args);
775
- }
776
- }
777
- info(message, ...args) {
778
- if (this.level >= LogLevel.Info) {
779
- console.info(this.prefix, message, ...args);
780
- }
781
- }
782
- warn(message, ...args) {
783
- if (this.level >= LogLevel.Warn) {
784
- console.warn(this.prefix, message, ...args);
785
- }
786
- }
787
- error(message, ...args) {
788
- if (this.level >= LogLevel.Error) {
789
- console.error(this.prefix, message, ...args);
790
- }
791
- }
792
- }
793
-
794
- /**
795
- * No-operation logger implementation.
796
- * All methods are no-ops - used in production by default.
797
- */
798
- class NoopLogger {
799
- debug() {
800
- // Intentionally empty
801
- }
802
- info() {
803
- // Intentionally empty
804
- }
805
- warn() {
806
- // Intentionally empty
807
- }
808
- error() {
809
- // Intentionally empty
810
- }
811
- }
812
-
813
810
  /**
814
811
  * Injection token for the dynamic forms logger.
815
812
  *
816
- * Default behavior:
817
- * - Development mode: ConsoleLogger with LogLevel.Warn
818
- * - Production mode: NoopLogger (silent)
819
- *
820
- * Override by providing a custom logger via withLogger() feature
821
- * or by providing directly to this token.
813
+ * Provided by provideDynamicForm() with ConsoleLogger as default.
814
+ * Override with withLoggerConfig(false) to disable logging.
822
815
  */
823
- const DYNAMIC_FORM_LOGGER = new InjectionToken('DYNAMIC_FORM_LOGGER', {
824
- providedIn: 'root',
825
- factory: () => {
826
- if (isDevMode()) {
827
- return new ConsoleLogger(LogLevel.Warn);
828
- }
829
- return new NoopLogger();
830
- },
831
- });
816
+ const DynamicFormLogger = new InjectionToken('DynamicFormLogger');
832
817
 
833
818
  /**
834
819
  * Registry service that maintains references to root form fields.
@@ -987,10 +972,10 @@ class RootFormRegistryService {
987
972
  this._formValueSignals.set(new Map());
988
973
  this.formContexts.clear();
989
974
  }
990
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: RootFormRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
991
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: RootFormRegistryService });
975
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: RootFormRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
976
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: RootFormRegistryService });
992
977
  }
993
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: RootFormRegistryService, decorators: [{
978
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: RootFormRegistryService, decorators: [{
994
979
  type: Injectable
995
980
  }] });
996
981
 
@@ -1006,7 +991,7 @@ function isChildFieldContext(context) {
1006
991
  */
1007
992
  class FieldContextRegistryService {
1008
993
  rootFormRegistry = inject(RootFormRegistryService);
1009
- logger = inject(DYNAMIC_FORM_LOGGER);
994
+ logger = inject(DynamicFormLogger);
1010
995
  /**
1011
996
  * Creates an evaluation context for a field by combining:
1012
997
  * - The field's current value
@@ -1114,10 +1099,10 @@ class FieldContextRegistryService {
1114
1099
  logger: this.logger,
1115
1100
  };
1116
1101
  }
1117
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1118
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FieldContextRegistryService });
1102
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1103
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: FieldContextRegistryService });
1119
1104
  }
1120
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FieldContextRegistryService, decorators: [{
1105
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: FieldContextRegistryService, decorators: [{
1121
1106
  type: Injectable
1122
1107
  }] });
1123
1108
 
@@ -1457,10 +1442,10 @@ class FunctionRegistryService {
1457
1442
  this.clearAsyncValidators();
1458
1443
  this.clearHttpValidators();
1459
1444
  }
1460
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1461
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FunctionRegistryService });
1445
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1446
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: FunctionRegistryService });
1462
1447
  }
1463
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FunctionRegistryService, decorators: [{
1448
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: FunctionRegistryService, decorators: [{
1464
1449
  type: Injectable
1465
1450
  }] });
1466
1451
 
@@ -1607,7 +1592,7 @@ var TokenType;
1607
1592
  /**
1608
1593
  * Parser error with position information
1609
1594
  */
1610
- class ExpressionParserError extends Error {
1595
+ class ExpressionParserError extends DynamicFormError {
1611
1596
  position;
1612
1597
  expression;
1613
1598
  constructor(message, position, expression) {
@@ -1705,7 +1690,7 @@ class Tokenizer {
1705
1690
  this.position++;
1706
1691
  }
1707
1692
  }
1708
- throw new ExpressionParserError('[Dynamic Forms] Unterminated string literal', start, this.expression);
1693
+ throw new ExpressionParserError('Unterminated string literal', start, this.expression);
1709
1694
  }
1710
1695
  readNumber() {
1711
1696
  const start = this.position;
@@ -1810,7 +1795,7 @@ class Tokenizer {
1810
1795
  case ']':
1811
1796
  return { type: TokenType.RBRACKET, value: ']', position: start };
1812
1797
  default:
1813
- throw new ExpressionParserError(`[Dynamic Forms] Unexpected character: ${char}`, start, this.expression);
1798
+ throw new ExpressionParserError(`Unexpected character: ${char}`, start, this.expression);
1814
1799
  }
1815
1800
  }
1816
1801
  }
@@ -1834,7 +1819,7 @@ class Parser {
1834
1819
  this.tokens = tokenizer.tokenize();
1835
1820
  this.current = 0;
1836
1821
  if (this.isAtEnd()) {
1837
- throw new ExpressionParserError('[Dynamic Forms] Empty expression', 0, this.expression);
1822
+ throw new ExpressionParserError('Empty expression', 0, this.expression);
1838
1823
  }
1839
1824
  return this.parseExpression();
1840
1825
  }
@@ -1909,7 +1894,7 @@ class Parser {
1909
1894
  if (this.match(TokenType.DOT)) {
1910
1895
  // Member access: obj.property
1911
1896
  if (!this.check(TokenType.IDENTIFIER)) {
1912
- throw new ExpressionParserError('[Dynamic Forms] Expected property name after "."', this.peek().position, this.expression);
1897
+ throw new ExpressionParserError('Expected property name after "."', this.peek().position, this.expression);
1913
1898
  }
1914
1899
  const property = this.advance().value;
1915
1900
  node = { type: 'MemberAccess', object: node, property };
@@ -1962,7 +1947,7 @@ class Parser {
1962
1947
  this.consume(TokenType.RPAREN, 'Expected ")" after expression');
1963
1948
  return expr;
1964
1949
  }
1965
- throw new ExpressionParserError(`[Dynamic Forms] Unexpected token: ${this.peek().value}`, this.peek().position, this.expression);
1950
+ throw new ExpressionParserError(`Unexpected token: ${this.peek().value}`, this.peek().position, this.expression);
1966
1951
  }
1967
1952
  parseArgumentList() {
1968
1953
  const args = [];
@@ -2013,7 +1998,7 @@ class Parser {
2013
1998
  consume(type, message) {
2014
1999
  if (this.check(type))
2015
2000
  return this.advance();
2016
- throw new ExpressionParserError(`[Dynamic Forms] ${message}`, this.peek().position, this.expression);
2001
+ throw new ExpressionParserError(message, this.peek().position, this.expression);
2017
2002
  }
2018
2003
  }
2019
2004
 
@@ -2165,7 +2150,7 @@ class Evaluator {
2165
2150
  case 'ArrayLiteral':
2166
2151
  return this.evaluateArrayLiteral(node);
2167
2152
  default:
2168
- throw new ExpressionParserError(`[Dynamic Forms] Unknown node type: ${node.type}`, 0, this.expression);
2153
+ throw new ExpressionParserError(`Unknown node type: ${node.type}`, 0, this.expression);
2169
2154
  }
2170
2155
  }
2171
2156
  evaluateIdentifier(name) {
@@ -2181,7 +2166,7 @@ class Evaluator {
2181
2166
  }
2182
2167
  // Block access to dangerous properties that could leak information or enable attacks
2183
2168
  if (BLOCKED_PROPERTIES.has(node.property)) {
2184
- throw new ExpressionParserError(`[Dynamic Forms] Property "${node.property}" is not accessible for security reasons`, 0, this.expression);
2169
+ throw new ExpressionParserError(`Property "${node.property}" is not accessible for security reasons`, 0, this.expression);
2185
2170
  }
2186
2171
  // Allow property access on plain objects and primitives
2187
2172
  if (typeof obj === 'object' || typeof obj === 'string' || typeof obj === 'number') {
@@ -2236,7 +2221,7 @@ class Evaluator {
2236
2221
  case '<=':
2237
2222
  return left <= right;
2238
2223
  default:
2239
- throw new ExpressionParserError(`[Dynamic Forms] Unknown binary operator: ${node.operator}`, 0, this.expression);
2224
+ throw new ExpressionParserError(`Unknown binary operator: ${node.operator}`, 0, this.expression);
2240
2225
  }
2241
2226
  }
2242
2227
  evaluateUnaryOp(node) {
@@ -2249,23 +2234,23 @@ class Evaluator {
2249
2234
  case '+':
2250
2235
  return +operand;
2251
2236
  default:
2252
- throw new ExpressionParserError(`[Dynamic Forms] Unknown unary operator: ${node.operator}`, 0, this.expression);
2237
+ throw new ExpressionParserError(`Unknown unary operator: ${node.operator}`, 0, this.expression);
2253
2238
  }
2254
2239
  }
2255
2240
  evaluateCallExpression(node) {
2256
2241
  // Only allow method calls (member access), not arbitrary function calls
2257
2242
  if (node.callee.type !== 'MemberAccess') {
2258
- throw new ExpressionParserError('[Dynamic Forms] Only method calls are allowed, not arbitrary function calls', 0, this.expression);
2243
+ throw new ExpressionParserError('Only method calls are allowed, not arbitrary function calls', 0, this.expression);
2259
2244
  }
2260
2245
  const obj = this.evaluate(node.callee.object);
2261
2246
  const methodName = node.callee.property;
2262
2247
  // Block access to dangerous properties (must check before method call)
2263
2248
  if (BLOCKED_PROPERTIES.has(methodName)) {
2264
- throw new ExpressionParserError(`[Dynamic Forms] Property "${methodName}" is not accessible for security reasons`, 0, this.expression);
2249
+ throw new ExpressionParserError(`Property "${methodName}" is not accessible for security reasons`, 0, this.expression);
2265
2250
  }
2266
2251
  // Check if the method is in the instance method whitelist
2267
2252
  if (!this.isMethodSafe(obj, methodName)) {
2268
- throw new ExpressionParserError(`[Dynamic Forms] Method "${methodName}" is not allowed for security reasons`, 0, this.expression);
2253
+ throw new ExpressionParserError(`Method "${methodName}" is not allowed for security reasons`, 0, this.expression);
2269
2254
  }
2270
2255
  // Evaluate arguments
2271
2256
  const args = node.arguments.map((arg) => this.evaluate(arg));
@@ -2383,7 +2368,7 @@ class ExpressionParser {
2383
2368
  throw error;
2384
2369
  }
2385
2370
  // Wrap other errors for consistency
2386
- throw new ExpressionParserError(`[Dynamic Forms] Error evaluating expression: ${error instanceof Error ? error.message : String(error)}`, 0, expression);
2371
+ throw new ExpressionParserError(`Error evaluating expression: ${error instanceof Error ? error.message : String(error)}`, 0, expression);
2387
2372
  }
2388
2373
  }
2389
2374
  /**
@@ -2478,6 +2463,56 @@ function evaluateOrCondition(expression, context) {
2478
2463
  return expression.conditions.some((condition) => evaluateCondition(condition, context));
2479
2464
  }
2480
2465
 
2466
+ /**
2467
+ * Cache for memoized logic functions, keyed by injection context.
2468
+ * Uses WeakMap with FunctionRegistryService as outer key to automatically clean up.
2469
+ * Inner structure: Map<FieldContextRegistryService, Map<expressionKey, LogicFn>>
2470
+ */
2471
+ const logicFunctionCache = new WeakMap();
2472
+ /**
2473
+ * Serializes a value to a deterministic string for cache key generation.
2474
+ * Unlike JSON.stringify, this sorts object keys to ensure consistent output
2475
+ * regardless of property insertion order.
2476
+ */
2477
+ function stableStringify(value) {
2478
+ if (value === null || value === undefined) {
2479
+ return String(value);
2480
+ }
2481
+ if (typeof value !== 'object') {
2482
+ return JSON.stringify(value);
2483
+ }
2484
+ if (Array.isArray(value)) {
2485
+ return '[' + value.map(stableStringify).join(',') + ']';
2486
+ }
2487
+ const obj = value;
2488
+ const sortedKeys = Object.keys(obj).sort();
2489
+ const pairs = sortedKeys.map((key) => JSON.stringify(key) + ':' + stableStringify(obj[key]));
2490
+ return '{' + pairs.join(',') + '}';
2491
+ }
2492
+ /**
2493
+ * Serializes a conditional expression to a deterministic string key for caching.
2494
+ * Uses stable stringification to ensure equivalent objects produce the same key
2495
+ * regardless of property order.
2496
+ */
2497
+ function serializeExpression(expression) {
2498
+ return stableStringify(expression);
2499
+ }
2500
+ /**
2501
+ * Gets or creates the cache map for a specific injection context.
2502
+ */
2503
+ function getContextCache(ctx) {
2504
+ let outerCache = logicFunctionCache.get(ctx.functionRegistry);
2505
+ if (!outerCache) {
2506
+ outerCache = new WeakMap();
2507
+ logicFunctionCache.set(ctx.functionRegistry, outerCache);
2508
+ }
2509
+ let innerCache = outerCache.get(ctx.fieldContextRegistry);
2510
+ if (!innerCache) {
2511
+ innerCache = new Map();
2512
+ outerCache.set(ctx.fieldContextRegistry, innerCache);
2513
+ }
2514
+ return innerCache;
2515
+ }
2481
2516
  /**
2482
2517
  * Create a logic function from a conditional expression.
2483
2518
  *
@@ -2489,17 +2524,32 @@ function evaluateOrCondition(expression, context) {
2489
2524
  * NOTE: For validators, use createEvaluationContext directly (with untracked) to prevent
2490
2525
  * infinite reactive loops. Validators with cross-field dependencies should be hoisted
2491
2526
  * to form-level using validateTree.
2527
+ *
2528
+ * @param expression The conditional expression to evaluate
2529
+ * @returns A LogicFn that evaluates the condition in the context of a field
2492
2530
  */
2493
2531
  function createLogicFunction(expression) {
2494
2532
  // Inject services during factory creation, not during execution
2495
2533
  const functionRegistry = inject(FunctionRegistryService);
2496
2534
  const fieldContextRegistry = inject(FieldContextRegistryService);
2497
- return (ctx) => {
2535
+ // Get cache for this injection context
2536
+ const contextCache = getContextCache({ functionRegistry, fieldContextRegistry });
2537
+ // Generate cache key from serialized expression
2538
+ const cacheKey = serializeExpression(expression);
2539
+ // Check cache first
2540
+ const cached = contextCache.get(cacheKey);
2541
+ if (cached) {
2542
+ return cached;
2543
+ }
2544
+ const fn = (ctx) => {
2498
2545
  // Create REACTIVE evaluation context for logic functions
2499
2546
  // This allows logic to re-evaluate when dependent fields change
2500
2547
  const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());
2501
2548
  return evaluateCondition(expression, evaluationContext);
2502
2549
  };
2550
+ // Cache the function
2551
+ contextCache.set(cacheKey, fn);
2552
+ return fn;
2503
2553
  }
2504
2554
 
2505
2555
  /**
@@ -2512,11 +2562,6 @@ const dynamicValueFunctionCache = new WeakMap();
2512
2562
  * Create a dynamic value function from an expression string.
2513
2563
  * Uses secure AST-based parsing instead of eval() or new Function().
2514
2564
  *
2515
- * Optimizations:
2516
- * - Injects FieldContextRegistryService at creation time, not execution time
2517
- * - Caches created functions by expression string per injection context
2518
- * - Expression parsing is already cached by ExpressionParser's LRU cache
2519
- *
2520
2565
  * @param expression The expression string to evaluate
2521
2566
  * @returns A LogicFn that evaluates the expression in the context of a field
2522
2567
  */
@@ -2524,7 +2569,7 @@ function createDynamicValueFunction(expression) {
2524
2569
  // Inject services during factory creation, not during execution
2525
2570
  // This captures the service instance in the closure
2526
2571
  const fieldContextRegistry = inject(FieldContextRegistryService);
2527
- const logger = inject(DYNAMIC_FORM_LOGGER);
2572
+ const logger = inject(DynamicFormLogger);
2528
2573
  // Get or create cache for this injection context
2529
2574
  let contextCache = dynamicValueFunctionCache.get(fieldContextRegistry);
2530
2575
  if (!contextCache) {
@@ -2891,7 +2936,7 @@ function applyCustomValidator(config, fieldPath) {
2891
2936
  validatorFn = createFunctionValidator(config);
2892
2937
  }
2893
2938
  else {
2894
- const logger = inject(DYNAMIC_FORM_LOGGER);
2939
+ const logger = inject(DynamicFormLogger);
2895
2940
  logger.warn('Custom validator must have either "expression" or "functionName"');
2896
2941
  return;
2897
2942
  }
@@ -2903,7 +2948,7 @@ function applyCustomValidator(config, fieldPath) {
2903
2948
  });
2904
2949
  }
2905
2950
  function createFunctionValidator(config) {
2906
- const logger = inject(DYNAMIC_FORM_LOGGER);
2951
+ const logger = inject(DynamicFormLogger);
2907
2952
  const functionName = config.functionName;
2908
2953
  if (!functionName) {
2909
2954
  logger.warn('Custom validator missing functionName');
@@ -2919,7 +2964,7 @@ function createFunctionValidator(config) {
2919
2964
  return (ctx) => validatorFn(ctx, config.params);
2920
2965
  }
2921
2966
  function createExpressionValidator(config) {
2922
- const logger = inject(DYNAMIC_FORM_LOGGER);
2967
+ const logger = inject(DynamicFormLogger);
2923
2968
  const expression = config.expression;
2924
2969
  if (!expression) {
2925
2970
  logger.warn('Custom validator missing expression');
@@ -2930,7 +2975,7 @@ function createExpressionValidator(config) {
2930
2975
  return (ctx) => {
2931
2976
  try {
2932
2977
  const evaluationContext = fieldContextRegistry.createEvaluationContext(ctx, functionRegistry.getCustomFunctions());
2933
- const result = ExpressionParser.evaluate(config.expression, evaluationContext);
2978
+ const result = ExpressionParser.evaluate(expression, evaluationContext);
2934
2979
  if (result) {
2935
2980
  return null;
2936
2981
  }
@@ -2966,7 +3011,7 @@ function createExpressionValidator(config) {
2966
3011
  * - onError: Optional handler for resource errors
2967
3012
  */
2968
3013
  function applyAsyncValidator(config, fieldPath) {
2969
- const logger = inject(DYNAMIC_FORM_LOGGER);
3014
+ const logger = inject(DynamicFormLogger);
2970
3015
  const registry = inject(FunctionRegistryService);
2971
3016
  const validatorConfig = registry.getAsyncValidator(config.functionName);
2972
3017
  if (!validatorConfig) {
@@ -2996,7 +3041,7 @@ function applyAsyncValidator(config, fieldPath) {
2996
3041
  * - onError: Optional handler for HTTP errors
2997
3042
  */
2998
3043
  function applyHttpValidator(config, fieldPath) {
2999
- const logger = inject(DYNAMIC_FORM_LOGGER);
3044
+ const logger = inject(DynamicFormLogger);
3000
3045
  const registry = inject(FunctionRegistryService);
3001
3046
  const httpValidatorConfig = registry.getHttpValidator(config.functionName);
3002
3047
  if (!httpValidatorConfig) {
@@ -3085,10 +3130,10 @@ class SchemaRegistryService {
3085
3130
  }
3086
3131
  return schema;
3087
3132
  }
3088
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: SchemaRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3089
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: SchemaRegistryService });
3133
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: SchemaRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3134
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: SchemaRegistryService });
3090
3135
  }
3091
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: SchemaRegistryService, decorators: [{
3136
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: SchemaRegistryService, decorators: [{
3092
3137
  type: Injectable
3093
3138
  }] });
3094
3139
 
@@ -3135,7 +3180,7 @@ function createTypePredicateFunction(predicate, logger) {
3135
3180
  * 3. The actual schema application happens at runtime via the schema function
3136
3181
  */
3137
3182
  function applySchema(config, fieldPath) {
3138
- const logger = inject(DYNAMIC_FORM_LOGGER);
3183
+ const logger = inject(DynamicFormLogger);
3139
3184
  const schemaRegistry = inject(SchemaRegistryService);
3140
3185
  const schema = schemaRegistry.resolveSchema(config.schema);
3141
3186
  if (!schema) {
@@ -3196,6 +3241,7 @@ function createSchemaFunction(schema) {
3196
3241
  /**
3197
3242
  * Type guard for GroupField with proper type narrowing
3198
3243
  */
3244
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type
3199
3245
  function isGroupField(field) {
3200
3246
  return field.type === 'group' && 'fields' in field;
3201
3247
  }
@@ -3203,16 +3249,58 @@ function isGroupField(field) {
3203
3249
  /**
3204
3250
  * Type guard for ArrayField with proper type narrowing
3205
3251
  */
3252
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type
3206
3253
  function isArrayField(field) {
3207
3254
  return field.type === 'array' && 'fields' in field;
3208
3255
  }
3209
3256
 
3257
+ /**
3258
+ * Type guard to check if a field is a hidden field.
3259
+ *
3260
+ * @param field - The field to check
3261
+ * @returns True if the field is a HiddenField
3262
+ */
3263
+ function isHiddenField(field) {
3264
+ return field.type === 'hidden';
3265
+ }
3266
+
3210
3267
  /**
3211
3268
  * Type guard for RowField with proper type narrowing
3212
3269
  */
3270
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type
3213
3271
  function isRowField(field) {
3214
3272
  return field.type === 'row' && 'fields' in field && Array.isArray(field.fields);
3215
3273
  }
3274
+ /**
3275
+ * Validates that a row field doesn't contain hidden fields.
3276
+ * Hidden fields are not allowed in rows because rows are for horizontal layouts,
3277
+ * and hidden fields don't render anything.
3278
+ *
3279
+ * @param rowField The row field to validate
3280
+ * @returns true if valid (no hidden fields), false otherwise
3281
+ */
3282
+ function validateRowNesting(rowField) {
3283
+ return !hasHiddenFields(rowField.fields);
3284
+ }
3285
+ /**
3286
+ * Recursively checks if fields contain any hidden fields
3287
+ * @param fields Array of field definitions to check
3288
+ * @returns true if hidden fields found, false otherwise
3289
+ */
3290
+ function hasHiddenFields(fields) {
3291
+ for (const field of fields) {
3292
+ if (isHiddenField(field)) {
3293
+ return true;
3294
+ }
3295
+ // Check group fields for nested hidden fields
3296
+ if (isGroupField(field)) {
3297
+ if (hasHiddenFields(field.fields)) {
3298
+ return true;
3299
+ }
3300
+ }
3301
+ }
3302
+ return false;
3303
+ }
3216
3304
 
3217
3305
  /**
3218
3306
  * Type guard for PageField with proper type narrowing
@@ -3610,74 +3698,9 @@ function isDisplayOnlyField(field) {
3610
3698
  return field.type === 'text' || isContainerField(field);
3611
3699
  }
3612
3700
 
3613
- /**
3614
- * Type guard to check if a value is a DynamicFormFeature
3615
- */
3616
- function isDynamicFormFeature(value) {
3617
- return (typeof value === 'object' &&
3618
- value !== null &&
3619
- 'ɵkind' in value &&
3620
- typeof value.ɵkind === 'string' &&
3621
- 'ɵproviders' in value &&
3622
- Array.isArray(value.ɵproviders));
3623
- }
3624
- /**
3625
- * Helper to create a feature with proper typing
3626
- */
3627
- function createFeature(kind, providers) {
3628
- return {
3629
- ɵkind: kind,
3630
- ɵproviders: providers,
3631
- };
3632
- }
3633
-
3634
- /**
3635
- * Configure logging for dynamic forms.
3636
- *
3637
- * @example
3638
- * ```typescript
3639
- * // Set log level
3640
- * provideDynamicForm(
3641
- * ...withMaterialFields(),
3642
- * withLogger({ level: LogLevel.Debug })
3643
- * )
3644
- *
3645
- * // Disable logging entirely
3646
- * provideDynamicForm(
3647
- * ...withMaterialFields(),
3648
- * withLogger({ level: LogLevel.Off })
3649
- * )
3650
- *
3651
- * // Custom logger implementation
3652
- * provideDynamicForm(
3653
- * ...withMaterialFields(),
3654
- * withLogger({ logger: mySentryLogger })
3655
- * )
3656
- * ```
3657
- *
3658
- * @param options - Logger configuration options
3659
- * @returns A DynamicFormFeature that configures logging
3660
- */
3661
- function withLogger(options = {}) {
3662
- let logger;
3663
- if (options.logger) {
3664
- // Use custom logger implementation
3665
- logger = options.logger;
3666
- }
3667
- else if (options.level === LogLevel.Off) {
3668
- // Explicit disable - use noop
3669
- logger = new NoopLogger();
3670
- }
3671
- else {
3672
- // Use console logger with specified level (default to Warn)
3673
- logger = new ConsoleLogger(options.level ?? LogLevel.Warn);
3674
- }
3675
- return createFeature('logger', [{ provide: DYNAMIC_FORM_LOGGER, useValue: logger }]);
3676
- }
3677
-
3678
3701
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3679
3702
  function getFieldTreeByKey(ctx, key) {
3680
- return ctx.field[key];
3703
+ return ctx.fieldTree[key];
3681
3704
  }
3682
3705
  /**
3683
3706
  * Creates an Angular signal forms schema from field definitions
@@ -3697,7 +3720,7 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3697
3720
  // Inject services for cross-field validation
3698
3721
  // These will be available because createSchemaFromFields is called within runInInjectionContext
3699
3722
  const functionRegistry = inject(FunctionRegistryService);
3700
- const logger = inject(DYNAMIC_FORM_LOGGER);
3723
+ const logger = inject(DynamicFormLogger);
3701
3724
  return schema((path) => {
3702
3725
  for (const fieldDef of fields) {
3703
3726
  const valueHandling = getFieldValueHandling(fieldDef.type, registry);
@@ -3739,7 +3762,7 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3739
3762
  * This is the key integration point that routes cross-field validation errors
3740
3763
  * to the appropriate target fields via Angular's form state system.
3741
3764
  *
3742
- * The validateTree function allows returning errors with a `field` property
3765
+ * The validateTree function allows returning errors with a `fieldTree` property
3743
3766
  * that targets specific fields, which Angular automatically routes to those
3744
3767
  * fields' errors() signal.
3745
3768
  *
@@ -3765,7 +3788,7 @@ function applyCrossFieldTreeValidator(rootPath, validators, functionRegistry, lo
3765
3788
  for (const entry of validators) {
3766
3789
  const { sourceFieldKey, config } = entry;
3767
3790
  try {
3768
- const error = evaluateCrossFieldValidator(entry, formValue, sourceFieldKey, ctx, customFunctions);
3791
+ const error = evaluateCrossFieldValidator(entry, formValue, sourceFieldKey, ctx, customFunctions, logger);
3769
3792
  if (error) {
3770
3793
  errors.push(error);
3771
3794
  }
@@ -3778,7 +3801,7 @@ function applyCrossFieldTreeValidator(rootPath, validators, functionRegistry, lo
3778
3801
  const customConfig = config;
3779
3802
  errors.push({
3780
3803
  kind: customConfig.kind || config.type || 'custom',
3781
- field: targetField,
3804
+ fieldTree: targetField,
3782
3805
  });
3783
3806
  }
3784
3807
  }
@@ -3789,7 +3812,7 @@ function applyCrossFieldTreeValidator(rootPath, validators, functionRegistry, lo
3789
3812
  /**
3790
3813
  * Evaluates a single cross-field validator entry and returns an error if validation fails.
3791
3814
  */
3792
- function evaluateCrossFieldValidator(entry, formValue, sourceFieldKey, ctx, customFunctions) {
3815
+ function evaluateCrossFieldValidator(entry, formValue, sourceFieldKey, ctx, customFunctions, logger) {
3793
3816
  const { config } = entry;
3794
3817
  const fieldValue = formValue[sourceFieldKey];
3795
3818
  // Create evaluation context for condition/expression evaluation
@@ -3798,6 +3821,7 @@ function evaluateCrossFieldValidator(entry, formValue, sourceFieldKey, ctx, cust
3798
3821
  formValue,
3799
3822
  fieldPath: sourceFieldKey,
3800
3823
  customFunctions,
3824
+ logger,
3801
3825
  };
3802
3826
  // Check if this is a custom validator (with expression) or a built-in validator (with when condition)
3803
3827
  if (config.type === 'custom') {
@@ -3815,7 +3839,7 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3815
3839
  if (!config.expression) {
3816
3840
  return null;
3817
3841
  }
3818
- const { fieldValue, formValue } = evaluationContext;
3842
+ const { fieldValue, formValue, logger, customFunctions } = evaluationContext;
3819
3843
  // First, evaluate the when condition if present
3820
3844
  // If the condition is false, the validator doesn't apply (validation passes)
3821
3845
  if (config.when) {
@@ -3823,8 +3847,8 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3823
3847
  fieldValue,
3824
3848
  formValue,
3825
3849
  fieldPath: sourceFieldKey,
3826
- customFunctions: evaluationContext.customFunctions || {},
3827
- logger: evaluationContext.logger,
3850
+ customFunctions: customFunctions || {},
3851
+ logger,
3828
3852
  });
3829
3853
  if (!conditionMet) {
3830
3854
  return null; // Condition not met, skip validation
@@ -3843,7 +3867,7 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3843
3867
  }
3844
3868
  const errorObj = {
3845
3869
  kind: config.kind || 'custom',
3846
- field: targetField,
3870
+ fieldTree: targetField,
3847
3871
  };
3848
3872
  // Evaluate and include errorParams for message interpolation
3849
3873
  if (config.errorParams) {
@@ -3863,7 +3887,7 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3863
3887
  * First checks the when condition, then applies the built-in validation logic.
3864
3888
  */
3865
3889
  function evaluateBuiltInCrossFieldValidator(config, evaluationContext, sourceFieldKey, ctx) {
3866
- const { fieldValue, formValue } = evaluationContext;
3890
+ const { fieldValue, formValue, logger, customFunctions } = evaluationContext;
3867
3891
  // First, evaluate the when condition
3868
3892
  // If the condition is false, the validator doesn't apply (validation passes)
3869
3893
  if (config.when) {
@@ -3871,8 +3895,8 @@ function evaluateBuiltInCrossFieldValidator(config, evaluationContext, sourceFie
3871
3895
  fieldValue,
3872
3896
  formValue,
3873
3897
  fieldPath: sourceFieldKey,
3874
- customFunctions: evaluationContext.customFunctions || {},
3875
- logger: evaluationContext.logger,
3898
+ customFunctions: customFunctions || {},
3899
+ logger,
3876
3900
  });
3877
3901
  if (!conditionMet) {
3878
3902
  return null; // Condition not met, skip validation
@@ -3890,7 +3914,7 @@ function evaluateBuiltInCrossFieldValidator(config, evaluationContext, sourceFie
3890
3914
  }
3891
3915
  return {
3892
3916
  kind: config.type,
3893
- field: targetField,
3917
+ fieldTree: targetField,
3894
3918
  };
3895
3919
  }
3896
3920
  /**
@@ -4038,6 +4062,7 @@ class EventBus {
4038
4062
  * eventBus.dispatch(CustomFormEvent);
4039
4063
  * ```
4040
4064
  */
4065
+ // TypeScript limitation: Must use ConstructorParameters which relies on `any` in FormEventConstructor
4041
4066
  dispatch(eventConstructor, ...args) {
4042
4067
  this.pipeline$.next(new eventConstructor(...args));
4043
4068
  }
@@ -4079,10 +4104,10 @@ class EventBus {
4079
4104
  }
4080
4105
  return this.pipeline$.pipe(filter((event) => event.type === eventType));
4081
4106
  }
4082
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4083
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: EventBus });
4107
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4108
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: EventBus });
4084
4109
  }
4085
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: EventBus, decorators: [{
4110
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: EventBus, decorators: [{
4086
4111
  type: Injectable
4087
4112
  }] });
4088
4113
 
@@ -4126,7 +4151,7 @@ class ComponentInitializedEvent {
4126
4151
  * ```
4127
4152
  */
4128
4153
  function createInitializationTracker(eventBus, expectedCount) {
4129
- return eventBus.on('component-initialized').pipe(scan$1((count, _event) => count + 1, 0), map$1((currentCount) => currentCount >= expectedCount), filter$1((isComplete) => isComplete));
4154
+ return eventBus.on('component-initialized').pipe(scan$1((count) => count + 1, 0), map$1((currentCount) => currentCount >= expectedCount), filter$1((isComplete) => isComplete));
4130
4155
  }
4131
4156
  /**
4132
4157
  * Creates an observable that tracks component initialization progress with detailed status.
@@ -4151,7 +4176,7 @@ function createInitializationTracker(eventBus, expectedCount) {
4151
4176
  * ```
4152
4177
  */
4153
4178
  function createDetailedInitializationTracker(eventBus, expectedCount) {
4154
- return eventBus.on('component-initialized').pipe(scan$1((count, _event) => count + 1, 0), map$1((currentCount) => ({
4179
+ return eventBus.on('component-initialized').pipe(scan$1((count) => count + 1, 0), map$1((currentCount) => ({
4155
4180
  currentCount,
4156
4181
  expectedCount,
4157
4182
  isComplete: currentCount >= expectedCount,
@@ -4584,6 +4609,9 @@ function tryCreateSchemaEntry(fieldKey, config) {
4584
4609
  /** Converts a built-in validator with cross-field expression to a custom validator. */
4585
4610
  function convertBuiltInToCustomValidator(config) {
4586
4611
  const expression = config.expression;
4612
+ if (!expression) {
4613
+ throw new Error(`Built-in validator ${config.type} missing required expression for cross-field conversion`);
4614
+ }
4587
4615
  let validationExpression;
4588
4616
  switch (config.type) {
4589
4617
  case 'min':
@@ -4619,7 +4647,8 @@ function convertBuiltInToCustomValidator(config) {
4619
4647
  * Comprehensive form configuration validator that checks:
4620
4648
  * 1. Form mode consistency (paged vs non-paged)
4621
4649
  * 2. Page nesting rules
4622
- * 3. Field placement constraints
4650
+ * 3. Row nesting rules (no hidden fields in rows)
4651
+ * 4. Field placement constraints
4623
4652
  */
4624
4653
  class FormModeValidator {
4625
4654
  /**
@@ -4659,11 +4688,8 @@ class FormModeValidator {
4659
4688
  static validateFormConfigurationOrThrow(fields) {
4660
4689
  const result = this.validateFormConfiguration(fields);
4661
4690
  if (!result.isValid) {
4662
- const errorMessage = [
4663
- `[Dynamic Forms] Invalid form configuration (${result.mode} mode):`,
4664
- ...result.errors.map((error) => ` - ${error}`),
4665
- ].join('\n');
4666
- throw new Error(errorMessage);
4691
+ const errorMessage = [`Invalid form configuration (${result.mode} mode):`, ...result.errors.map((error) => ` - ${error}`)].join('\n');
4692
+ throw new DynamicFormError(errorMessage);
4667
4693
  }
4668
4694
  }
4669
4695
  /**
@@ -4689,6 +4715,47 @@ class FormModeValidator {
4689
4715
  }
4690
4716
  }
4691
4717
  }
4718
+ // Warn about hidden fields in rows (they work but don't render, which may be confusing)
4719
+ const rowWarnings = this.collectRowHiddenFieldWarnings(fields);
4720
+ warnings.push(...rowWarnings);
4721
+ return warnings;
4722
+ }
4723
+ /**
4724
+ * Recursively collects warnings for hidden fields inside rows
4725
+ * @param fields The form field definitions to check
4726
+ * @param path Current path for warning messages
4727
+ * @returns Array of warning messages
4728
+ */
4729
+ static collectRowHiddenFieldWarnings(fields, path = '') {
4730
+ if (!fields) {
4731
+ return [];
4732
+ }
4733
+ const warnings = [];
4734
+ for (let i = 0; i < fields.length; i++) {
4735
+ const field = fields[i];
4736
+ const fieldPath = path ? `${path}.fields[${i}]` : `fields[${i}]`;
4737
+ const fieldKey = field.key || 'unknown';
4738
+ if (isRowField(field)) {
4739
+ if (!validateRowNesting(field)) {
4740
+ warnings.push(`Row field at ${fieldPath} (key: "${fieldKey}") contains hidden fields. ` +
4741
+ `Hidden fields in rows don't render anything - consider placing them outside the row.`);
4742
+ }
4743
+ // Continue checking nested rows within the row's children
4744
+ warnings.push(...this.collectRowHiddenFieldWarnings(field.fields, fieldPath));
4745
+ }
4746
+ else if (isPageField(field)) {
4747
+ // Check rows within pages
4748
+ warnings.push(...this.collectRowHiddenFieldWarnings(field.fields, fieldPath));
4749
+ }
4750
+ else if (isGroupField(field)) {
4751
+ // Check rows within groups
4752
+ warnings.push(...this.collectRowHiddenFieldWarnings(field.fields, fieldPath));
4753
+ }
4754
+ else if (isArrayField(field)) {
4755
+ // Check rows within array templates
4756
+ warnings.push(...this.collectRowHiddenFieldWarnings(field.fields, fieldPath));
4757
+ }
4758
+ }
4692
4759
  return warnings;
4693
4760
  }
4694
4761
  }
@@ -4885,7 +4952,7 @@ class PageOrchestratorComponent {
4885
4952
  eventBus = inject(EventBus);
4886
4953
  fieldContextRegistry = inject(FieldContextRegistryService);
4887
4954
  functionRegistry = inject(FunctionRegistryService);
4888
- logger = inject(DYNAMIC_FORM_LOGGER);
4955
+ logger = inject(DynamicFormLogger);
4889
4956
  /**
4890
4957
  * Array of page field definitions to render
4891
4958
  */
@@ -4898,6 +4965,7 @@ class PageOrchestratorComponent {
4898
4965
  /**
4899
4966
  * Field signal context for child fields
4900
4967
  */
4968
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- FieldSignalContext is contravariant in TModel, using any allows accepting any form type
4901
4969
  fieldSignalContext = input.required(...(ngDevMode ? [{ debugName: "fieldSignalContext" }] : []));
4902
4970
  /**
4903
4971
  * Computed signal that tracks which pages are hidden.
@@ -4978,7 +5046,7 @@ class PageOrchestratorComponent {
4978
5046
  const fieldKey = fieldDef.key;
4979
5047
  if (!fieldKey)
4980
5048
  continue;
4981
- // Access the field from the form using dynamic key access
5049
+ // Access the field from the form using bracket notation
4982
5050
  const field = form[fieldKey];
4983
5051
  if (field && typeof field === 'function') {
4984
5052
  const fieldState = field();
@@ -5143,7 +5211,7 @@ class PageOrchestratorComponent {
5143
5211
  }
5144
5212
  // Check each hidden logic - if ANY condition is true, the page is hidden
5145
5213
  for (const logic of hiddenLogic) {
5146
- // Handle static boolean conditions
5214
+ // Handle static boolean conditions (fast path)
5147
5215
  if (typeof logic.condition === 'boolean') {
5148
5216
  if (logic.condition) {
5149
5217
  return true;
@@ -5159,8 +5227,8 @@ class PageOrchestratorComponent {
5159
5227
  }
5160
5228
  return false;
5161
5229
  }
5162
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: PageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5163
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.5", type: PageOrchestratorComponent, isStandalone: true, selector: "div[page-orchestrator]", inputs: { pageFields: { classPropertyName: "pageFields", publicName: "pageFields", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, fieldSignalContext: { classPropertyName: "fieldSignalContext", publicName: "fieldSignalContext", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.data-current-page": "state().currentPageIndex", "attr.data-total-pages": "state().totalPages" }, classAttribute: "df-page-orchestrator" }, providers: [
5230
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: PageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5231
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: PageOrchestratorComponent, isStandalone: true, selector: "div[page-orchestrator]", inputs: { pageFields: { classPropertyName: "pageFields", publicName: "pageFields", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, fieldSignalContext: { classPropertyName: "fieldSignalContext", publicName: "fieldSignalContext", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.data-current-page": "state().currentPageIndex", "attr.data-total-pages": "state().totalPages" }, classAttribute: "df-page-orchestrator" }, providers: [
5164
5232
  {
5165
5233
  provide: FIELD_SIGNAL_CONTEXT,
5166
5234
  useFactory: (orchestrator) => orchestrator.extendedFieldSignalContext(),
@@ -5190,9 +5258,9 @@ class PageOrchestratorComponent {
5190
5258
  }
5191
5259
  }
5192
5260
  }
5193
- `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs').then(m => m.default)]] });
5261
+ `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-rdGD8A9d.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-rdGD8A9d.mjs').then(m => m.default)]] });
5194
5262
  }
5195
- i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.0.5", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
5263
+ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.0.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-rdGD8A9d.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
5196
5264
  type: Component,
5197
5265
  args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
5198
5266
  @for (pageField of pageFields(); track pageField.key; let i = $index) {
@@ -5261,7 +5329,7 @@ class DynamicForm {
5261
5329
  rootFormRegistry = inject(RootFormRegistryService);
5262
5330
  functionRegistry = inject(FunctionRegistryService);
5263
5331
  schemaRegistry = inject(SchemaRegistryService);
5264
- logger = inject(DYNAMIC_FORM_LOGGER);
5332
+ logger = inject(DynamicFormLogger);
5265
5333
  // ─────────────────────────────────────────────────────────────────────────────
5266
5334
  // Inputs
5267
5335
  // ─────────────────────────────────────────────────────────────────────────────
@@ -5629,13 +5697,13 @@ class DynamicForm {
5629
5697
  }
5630
5698
  onFormReset() {
5631
5699
  const defaults = this.defaultValues();
5632
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5700
+ // Type assertion is necessary: Angular Signal Forms expects exact form value type
5633
5701
  this.form()().value.set(defaults);
5634
5702
  this.value.set(defaults);
5635
5703
  }
5636
5704
  onFormClear() {
5637
5705
  const emptyValue = {};
5638
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5706
+ // Type assertion is necessary: Angular Signal Forms expects exact form value type
5639
5707
  this.form()().value.set(emptyValue);
5640
5708
  this.value.set(emptyValue);
5641
5709
  }
@@ -5645,8 +5713,8 @@ class DynamicForm {
5645
5713
  ngOnDestroy() {
5646
5714
  this.rootFormRegistry.unregisterForm();
5647
5715
  }
5648
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
5649
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.5", type: DynamicForm, isStandalone: true, selector: "form[dynamic-form]", inputs: { config: { classPropertyName: "config", publicName: "dynamic-form", isSignal: true, isRequired: true, transformFunction: null }, formOptions: { classPropertyName: "formOptions", publicName: "formOptions", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted", reset: "reset", cleared: "cleared", events: "events", initialized: "initialized", onPageChange: "onPageChange", onPageNavigationStateChange: "onPageNavigationStateChange" }, host: { listeners: { "submit": "onNativeSubmit($event)" }, properties: { "class.disabled": "disabled()", "class.df-form-paged": "formModeDetection().mode === \"paged\"", "class.df-form-non-paged": "formModeDetection().mode === \"non-paged\"", "attr.data-form-mode": "formModeDetection().mode" }, classAttribute: "df-dynamic-form df-form" }, providers: [EventBus, SchemaRegistryService, FunctionRegistryService, RootFormRegistryService, FieldContextRegistryService], ngImport: i0, template: `
5716
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
5717
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: DynamicForm, isStandalone: true, selector: "form[dynamic-form]", inputs: { config: { classPropertyName: "config", publicName: "dynamic-form", isSignal: true, isRequired: true, transformFunction: null }, formOptions: { classPropertyName: "formOptions", publicName: "formOptions", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted", reset: "reset", cleared: "cleared", events: "events", initialized: "initialized", onPageChange: "onPageChange", onPageNavigationStateChange: "onPageNavigationStateChange" }, host: { attributes: { "novalidate": "" }, listeners: { "submit": "onNativeSubmit($event)" }, properties: { "class.disabled": "disabled()", "class.df-form-paged": "formModeDetection().mode === \"paged\"", "class.df-form-non-paged": "formModeDetection().mode === \"non-paged\"", "attr.data-form-mode": "formModeDetection().mode" }, classAttribute: "df-dynamic-form df-form" }, providers: [EventBus, SchemaRegistryService, FunctionRegistryService, RootFormRegistryService, FieldContextRegistryService], ngImport: i0, template: `
5650
5718
  @if (formModeDetection().mode === 'paged') {
5651
5719
  <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
5652
5720
  } @else {
@@ -5656,7 +5724,7 @@ class DynamicForm {
5656
5724
  }
5657
5725
  `, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}:host>*{grid-column:1/-1}:host.df-form-spacing-custom{--df-grid-row-gap: var(--form-custom-spacing, 1.5rem)}:host.df-form-gap-none{--df-grid-row-gap: 0}:host.df-form-gap-xs{--df-grid-row-gap: .25rem}:host.df-form-gap-sm{--df-grid-row-gap: .5rem}:host.df-form-gap-md{--df-grid-row-gap: 1rem}:host.df-form-gap-lg{--df-grid-row-gap: 1.5rem}:host.df-form-gap-xl{--df-grid-row-gap: 2rem}:host.df-form-paged{--df-grid-row-gap: 0}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: PageOrchestratorComponent, selector: "div[page-orchestrator]", inputs: ["pageFields", "form", "fieldSignalContext"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5658
5726
  }
5659
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: DynamicForm, decorators: [{
5727
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: DynamicForm, decorators: [{
5660
5728
  type: Component,
5661
5729
  args: [{ selector: 'form[dynamic-form]', imports: [NgComponentOutlet, PageOrchestratorComponent], template: `
5662
5730
  @if (formModeDetection().mode === 'paged') {
@@ -5668,6 +5736,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImpor
5668
5736
  }
5669
5737
  `, providers: [EventBus, SchemaRegistryService, FunctionRegistryService, RootFormRegistryService, FieldContextRegistryService], host: {
5670
5738
  class: 'df-dynamic-form df-form',
5739
+ novalidate: '', // Disable browser validation - Angular Signal Forms handles validation
5671
5740
  '[class.disabled]': 'disabled()',
5672
5741
  '[class.df-form-paged]': 'formModeDetection().mode === "paged"',
5673
5742
  '[class.df-form-non-paged]': 'formModeDetection().mode === "non-paged"',
@@ -5759,7 +5828,6 @@ function pageFieldMapper(fieldDef) {
5759
5828
  function textFieldMapper(fieldDef) {
5760
5829
  const fieldContextRegistry = inject(FieldContextRegistryService);
5761
5830
  const functionRegistry = inject(FunctionRegistryService);
5762
- const logger = inject(DYNAMIC_FORM_LOGGER);
5763
5831
  // Build base inputs (static, from field definition)
5764
5832
  const baseInputs = buildBaseInputs(fieldDef);
5765
5833
  // Create computed signal for hidden state based on logic configuration
@@ -5814,6 +5882,13 @@ function textFieldMapper(fieldDef) {
5814
5882
  * { type: 'page', key: 'step1', fields: [...] }
5815
5883
  * ```
5816
5884
  */
5885
+ /**
5886
+ * Built-in field types provided by the dynamic form library.
5887
+ *
5888
+ * Each field type is validated at compile time using satisfies, ensuring
5889
+ * type safety of the mapper function while allowing the array to be typed
5890
+ * as FieldTypeDefinition[] for consumer flexibility.
5891
+ */
5817
5892
  const BUILT_IN_FIELDS = [
5818
5893
  {
5819
5894
  name: 'row',
@@ -5835,25 +5910,70 @@ const BUILT_IN_FIELDS = [
5835
5910
  },
5836
5911
  {
5837
5912
  name: 'page',
5838
- loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs'),
5913
+ loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-rdGD8A9d.mjs'),
5839
5914
  mapper: pageFieldMapper,
5840
5915
  valueHandling: 'flatten',
5841
5916
  },
5842
5917
  {
5843
5918
  name: 'text',
5844
- loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-CiYq73aF.mjs'),
5919
+ loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-CDxka2KD.mjs'),
5845
5920
  mapper: textFieldMapper,
5846
5921
  valueHandling: 'exclude',
5847
5922
  },
5923
+ {
5924
+ name: 'hidden',
5925
+ // Componentless field - no loadComponent or mapper needed
5926
+ valueHandling: 'include',
5927
+ },
5848
5928
  ];
5849
5929
 
5930
+ /**
5931
+ * Type guard to check if a value is a DynamicFormFeature
5932
+ */
5933
+ function isDynamicFormFeature(value) {
5934
+ return (typeof value === 'object' &&
5935
+ value !== null &&
5936
+ 'ɵkind' in value &&
5937
+ typeof value.ɵkind === 'string' &&
5938
+ 'ɵproviders' in value &&
5939
+ Array.isArray(value.ɵproviders));
5940
+ }
5941
+ /**
5942
+ * Helper to create a feature with proper typing
5943
+ */
5944
+ function createFeature(kind, providers) {
5945
+ return {
5946
+ ɵkind: kind,
5947
+ ɵproviders: providers,
5948
+ };
5949
+ }
5950
+
5951
+ /**
5952
+ * Console-based logger implementation.
5953
+ */
5954
+ class ConsoleLogger {
5955
+ prefix = '[Dynamic Forms]';
5956
+ debug(message, ...args) {
5957
+ console.debug(this.prefix, message, ...args);
5958
+ }
5959
+ info(message, ...args) {
5960
+ console.info(this.prefix, message, ...args);
5961
+ }
5962
+ warn(message, ...args) {
5963
+ console.warn(this.prefix, message, ...args);
5964
+ }
5965
+ error(message, ...args) {
5966
+ console.error(this.prefix, message, ...args);
5967
+ }
5968
+ }
5969
+
5850
5970
  /**
5851
5971
  * Provider function to configure the dynamic form system with field types and options.
5852
5972
  *
5853
5973
  * This function creates environment providers that can be used at application or route level
5854
5974
  * to register field types. It provides type-safe field registration with automatic type inference.
5855
5975
  *
5856
- * @param items - Field type definitions and/or features (like withLogger)
5976
+ * @param items - Field type definitions and/or features (like withLoggerConfig)
5857
5977
  * @returns Environment providers for dependency injection with type inference
5858
5978
  *
5859
5979
  * @example
@@ -5868,12 +5988,12 @@ const BUILT_IN_FIELDS = [
5868
5988
  *
5869
5989
  * @example
5870
5990
  * ```typescript
5871
- * // With logging configuration
5991
+ * // Disable logging
5872
5992
  * bootstrapApplication(AppComponent, {
5873
5993
  * providers: [
5874
5994
  * provideDynamicForm(
5875
5995
  * ...withMaterialFields(),
5876
- * withLogger({ level: LogLevel.Debug })
5996
+ * withLoggerConfig(false)
5877
5997
  * )
5878
5998
  * ]
5879
5999
  * });
@@ -5881,18 +6001,6 @@ const BUILT_IN_FIELDS = [
5881
6001
  *
5882
6002
  * @example
5883
6003
  * ```typescript
5884
- * // Route-level setup
5885
- * const routes: Routes = [
5886
- * {
5887
- * path: 'form',
5888
- * component: FormComponent,
5889
- * providers: [provideDynamicForm(...withMaterialFields())]
5890
- * }
5891
- * ];
5892
- * ```
5893
- *
5894
- * @example
5895
- * ```typescript
5896
6004
  * // Custom field types with type inference
5897
6005
  * import { CustomFieldType, AnotherFieldType } from './custom-fields';
5898
6006
  *
@@ -5922,14 +6030,20 @@ function provideDynamicForm(...items) {
5922
6030
  });
5923
6031
  // Extract providers from features
5924
6032
  const featureProviders = [];
6033
+ const hasLoggerFeature = features.some((feature) => feature.ɵkind === 'logger');
5925
6034
  features.forEach((feature) => {
5926
6035
  featureProviders.push(...feature.ɵproviders);
5927
6036
  });
6037
+ // Default logger provider (ConsoleLogger) if no logger feature was provided
6038
+ const defaultLoggerProvider = hasLoggerFeature ? [] : [{ provide: DynamicFormLogger, useValue: new ConsoleLogger() }];
5928
6039
  return makeEnvironmentProviders([
6040
+ ...defaultLoggerProvider,
6041
+ // Always provide default Signal Forms classes (ng-touched, ng-invalid, etc.)
6042
+ provideSignalFormsConfig({ classes: NG_STATUS_CLASSES }),
5929
6043
  {
5930
6044
  provide: FIELD_REGISTRY,
5931
6045
  useFactory: () => {
5932
- const logger = inject(DYNAMIC_FORM_LOGGER);
6046
+ const logger = inject(DynamicFormLogger);
5933
6047
  const registry = new Map();
5934
6048
  // Add custom field types
5935
6049
  fields.forEach((fieldType) => {
@@ -5946,6 +6060,55 @@ function provideDynamicForm(...items) {
5946
6060
  ]);
5947
6061
  }
5948
6062
 
6063
+ /**
6064
+ * No-operation logger implementation.
6065
+ * All methods are no-ops - used in production by default.
6066
+ */
6067
+ class NoopLogger {
6068
+ debug() {
6069
+ // Intentionally empty
6070
+ }
6071
+ info() {
6072
+ // Intentionally empty
6073
+ }
6074
+ warn() {
6075
+ // Intentionally empty
6076
+ }
6077
+ error() {
6078
+ // Intentionally empty
6079
+ }
6080
+ }
6081
+
6082
+ /**
6083
+ * Configure logging for dynamic forms.
6084
+ *
6085
+ * By default, logging is enabled (ConsoleLogger). Use this feature to disable logging
6086
+ * or configure it conditionally.
6087
+ *
6088
+ * @example
6089
+ * ```typescript
6090
+ * // Disable logging
6091
+ * provideDynamicForm(
6092
+ * ...withMaterialFields(),
6093
+ * withLoggerConfig(false)
6094
+ * )
6095
+ *
6096
+ * // Conditional logging (e.g., only in dev mode)
6097
+ * provideDynamicForm(
6098
+ * ...withMaterialFields(),
6099
+ * withLoggerConfig(() => isDevMode())
6100
+ * )
6101
+ * ```
6102
+ *
6103
+ * @param enabled - Whether logging is enabled. Can be a boolean or a function that returns a boolean. Defaults to true.
6104
+ * @returns A DynamicFormFeature that configures logging
6105
+ */
6106
+ function withLoggerConfig(enabled = true) {
6107
+ const isEnabled = typeof enabled === 'function' ? enabled() : enabled;
6108
+ const logger = isEnabled ? new ConsoleLogger() : new NoopLogger();
6109
+ return createFeature('logger', [{ provide: DynamicFormLogger, useValue: logger }]);
6110
+ }
6111
+
5949
6112
  /**
5950
6113
  * No-op logger for when no logger is provided.
5951
6114
  * Used as fallback in button logic evaluation to avoid breaking downstream packages.
@@ -6222,6 +6385,51 @@ function dynamicTextToObservable(value, injector) {
6222
6385
  return of(String(value));
6223
6386
  }
6224
6387
 
6388
+ /**
6389
+ * Applies meta attributes to a DOM element and tracks which attributes were applied.
6390
+ *
6391
+ * This utility handles:
6392
+ * - Removing previously applied attributes that are no longer in meta
6393
+ * - Setting new attributes from meta
6394
+ * - Converting all values to strings via String()
6395
+ *
6396
+ * @param element - The DOM element to apply attributes to
6397
+ * @param meta - The meta object containing attributes to apply
6398
+ * @param previouslyApplied - Set of attribute names that were previously applied
6399
+ * @returns A new Set containing the attribute names that were applied
6400
+ *
6401
+ * @example
6402
+ * ```typescript
6403
+ * private appliedAttrs = new Set<string>();
6404
+ *
6405
+ * explicitEffect([this.meta], ([meta]) => {
6406
+ * const input = this.el.nativeElement.querySelector('input');
6407
+ * if (input) {
6408
+ * this.appliedAttrs = applyMetaToElement(input, meta, this.appliedAttrs);
6409
+ * }
6410
+ * });
6411
+ * ```
6412
+ */
6413
+ function applyMetaToElement(element, meta, previouslyApplied) {
6414
+ const newApplied = new Set();
6415
+ // Remove old attributes no longer in meta
6416
+ for (const attr of previouslyApplied) {
6417
+ if (!meta || !(attr in meta) || meta[attr] === undefined) {
6418
+ element.removeAttribute(attr);
6419
+ }
6420
+ }
6421
+ if (!meta)
6422
+ return newApplied;
6423
+ // Apply new attributes
6424
+ for (const [key, value] of Object.entries(meta)) {
6425
+ if (value === undefined || value === null)
6426
+ continue;
6427
+ element.setAttribute(key, String(value));
6428
+ newApplied.add(key);
6429
+ }
6430
+ return newApplied;
6431
+ }
6432
+
6225
6433
  /**
6226
6434
  * Pipe that handles dynamic text resolution with support for static strings,
6227
6435
  * Observables, and Signals.
@@ -6262,10 +6470,10 @@ class DynamicTextPipe {
6262
6470
  }
6263
6471
  return of(value || '');
6264
6472
  }
6265
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
6266
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.5", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
6473
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
6474
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.8", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
6267
6475
  }
6268
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: DynamicTextPipe, decorators: [{
6476
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: DynamicTextPipe, decorators: [{
6269
6477
  type: Pipe,
6270
6478
  args: [{
6271
6479
  name: 'dynamicText',
@@ -6311,7 +6519,7 @@ class RowFieldComponent {
6311
6519
  fieldRegistry = injectFieldRegistry();
6312
6520
  injector = inject(Injector);
6313
6521
  eventBus = inject(EventBus);
6314
- logger = inject(DYNAMIC_FORM_LOGGER);
6522
+ logger = inject(DynamicFormLogger);
6315
6523
  // ─────────────────────────────────────────────────────────────────────────────
6316
6524
  // Inputs
6317
6525
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6368,14 +6576,14 @@ class RowFieldComponent {
6368
6576
  }
6369
6577
  });
6370
6578
  }
6371
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: RowFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6372
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.5", type: RowFieldComponent, isStandalone: true, selector: "div[row-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "class.disabled": "disabled()", "id": "`${key()}`", "attr.data-testid": "key()" }, classAttribute: "df-field df-row" }, ngImport: i0, template: `
6579
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: RowFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6580
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: RowFieldComponent, isStandalone: true, selector: "div[row-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "class.disabled": "disabled()", "id": "`${key()}`", "attr.data-testid": "key()" }, classAttribute: "df-field df-row" }, ngImport: i0, template: `
6373
6581
  @for (field of resolvedFields(); track field.key) {
6374
6582
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
6375
6583
  }
6376
6584
  `, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:flex;flex-wrap:wrap;gap:var(--df-grid-gap);align-items:flex-start;width:100%}:host ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto}:host ::ng-deep>.df-col-1{flex:0 0 calc(8.3333333333% - var(--df-grid-gap) * .9166666667);max-width:calc(8.3333333333% - var(--df-grid-gap) * .9166666667)}:host ::ng-deep>.df-col-2{flex:0 0 calc(16.6666666667% - var(--df-grid-gap) * .8333333333);max-width:calc(16.6666666667% - var(--df-grid-gap) * .8333333333)}:host ::ng-deep>.df-col-3{flex:0 0 calc(25% - var(--df-grid-gap) * .75);max-width:calc(25% - var(--df-grid-gap) * .75)}:host ::ng-deep>.df-col-4{flex:0 0 calc(33.3333333333% - var(--df-grid-gap) * .6666666667);max-width:calc(33.3333333333% - var(--df-grid-gap) * .6666666667)}:host ::ng-deep>.df-col-5{flex:0 0 calc(41.6666666667% - var(--df-grid-gap) * .5833333333);max-width:calc(41.6666666667% - var(--df-grid-gap) * .5833333333)}:host ::ng-deep>.df-col-6{flex:0 0 calc(50% - var(--df-grid-gap) * .5);max-width:calc(50% - var(--df-grid-gap) * .5)}:host ::ng-deep>.df-col-7{flex:0 0 calc(58.3333333333% - var(--df-grid-gap) * .4166666667);max-width:calc(58.3333333333% - var(--df-grid-gap) * .4166666667)}:host ::ng-deep>.df-col-8{flex:0 0 calc(66.6666666667% - var(--df-grid-gap) * .3333333333);max-width:calc(66.6666666667% - var(--df-grid-gap) * .3333333333)}:host ::ng-deep>.df-col-9{flex:0 0 calc(75% - var(--df-grid-gap) * .25);max-width:calc(75% - var(--df-grid-gap) * .25)}:host ::ng-deep>.df-col-10{flex:0 0 calc(83.3333333333% - var(--df-grid-gap) * .1666666667);max-width:calc(83.3333333333% - var(--df-grid-gap) * .1666666667)}:host ::ng-deep>.df-col-11{flex:0 0 calc(91.6666666667% - var(--df-grid-gap) * .0833333333);max-width:calc(91.6666666667% - var(--df-grid-gap) * .0833333333)}:host ::ng-deep>.df-col-12{flex:0 0 calc(100% - var(--df-grid-gap) * 0);max-width:calc(100% - var(--df-grid-gap) * 0)}:host.df-gap-custom{--df-grid-gap: var(--row-custom-gap, 1rem)}@media(max-width:576px){:host.df-row-mobile-stack{flex-direction:column}:host.df-row-mobile-stack ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}}@media(min-width:577px)and (max-width:768px){:host.df-row-tablet-2col ::ng-deep>*{flex:0 0 calc(50% - var(--df-grid-gap) / 2);max-width:calc(50% - var(--df-grid-gap) / 2)}}@media(max-width:576px){:host{flex-direction:column}:host ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}:host.df-row-mobile-keep-cols{flex-direction:row}:host.df-row-mobile-keep-cols ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto!important;max-width:none!important}}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6377
6585
  }
6378
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: RowFieldComponent, decorators: [{
6586
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: RowFieldComponent, decorators: [{
6379
6587
  type: Component,
6380
6588
  args: [{ selector: 'div[row-field]', imports: [NgComponentOutlet], template: `
6381
6589
  @for (field of resolvedFields(); track field.key) {
@@ -6411,7 +6619,7 @@ class GroupFieldComponent {
6411
6619
  parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
6412
6620
  injector = inject(Injector);
6413
6621
  eventBus = inject(EventBus);
6414
- logger = inject(DYNAMIC_FORM_LOGGER);
6622
+ logger = inject(DynamicFormLogger);
6415
6623
  // ─────────────────────────────────────────────────────────────────────────────
6416
6624
  // Memoized Functions
6417
6625
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6490,7 +6698,7 @@ class GroupFieldComponent {
6490
6698
  const groupKey = this.field().key;
6491
6699
  const child = parentForm[groupKey];
6492
6700
  if (!child) {
6493
- throw new Error(`[Dynamic Forms] Group field "${groupKey}" not found in parent form. ` + `Ensure the parent form schema includes this group field.`);
6701
+ throw new DynamicFormError(`Group field "${groupKey}" not found in parent form. ` + `Ensure the parent form schema includes this group field.`);
6494
6702
  }
6495
6703
  return child;
6496
6704
  }, ...(ngDevMode ? [{ debugName: "nestedFieldTree" }] : []));
@@ -6545,14 +6753,14 @@ class GroupFieldComponent {
6545
6753
  }
6546
6754
  });
6547
6755
  }
6548
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6549
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.5", type: GroupFieldComponent, isStandalone: true, selector: "fieldset[group-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted" }, host: { attributes: { "role": "group" }, properties: { "class.disabled": "disabled()", "id": "`${key()}`", "attr.data-testid": "key()" }, classAttribute: "df-field df-group" }, ngImport: i0, template: `
6756
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6757
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: GroupFieldComponent, isStandalone: true, selector: "fieldset[group-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted" }, host: { attributes: { "role": "group" }, properties: { "class.disabled": "disabled()", "id": "`${key()}`", "attr.data-testid": "key()" }, classAttribute: "df-field df-group" }, ngImport: i0, template: `
6550
6758
  @for (field of resolvedFields(); track field.key) {
6551
6759
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
6552
6760
  }
6553
6761
  `, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{border:none;margin:0;padding:0;min-width:0;display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}:host>*{grid-column:1/-1}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6554
6762
  }
6555
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: GroupFieldComponent, decorators: [{
6763
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: GroupFieldComponent, decorators: [{
6556
6764
  type: Component,
6557
6765
  args: [{ selector: 'fieldset[group-field]', imports: [NgComponentOutlet], template: `
6558
6766
  @for (field of resolvedFields(); track field.key) {
@@ -6754,6 +6962,10 @@ function resolveArrayItem(options) {
6754
6962
  if (destroyRef.destroyed) {
6755
6963
  return undefined;
6756
6964
  }
6965
+ // Componentless fields (e.g., hidden) return undefined - nothing to render
6966
+ if (!component) {
6967
+ return undefined;
6968
+ }
6757
6969
  const itemId = generateArrayItemId();
6758
6970
  const indexSignal = linkedSignal(() => {
6759
6971
  const order = itemOrderSignal();
@@ -6769,16 +6981,19 @@ function resolveArrayItem(options) {
6769
6981
  registry,
6770
6982
  arrayField,
6771
6983
  });
6984
+ // Array item templates should always have inputs (componentless fields are handled above)
6985
+ if (!inputs) {
6986
+ return undefined;
6987
+ }
6772
6988
  return {
6773
6989
  id: itemId,
6774
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
6775
- component: component,
6990
+ component,
6776
6991
  injector,
6777
6992
  inputs,
6778
6993
  };
6779
6994
  }), catchError((error) => {
6780
6995
  if (!destroyRef.destroyed) {
6781
- const logger = parentInjector.get(DYNAMIC_FORM_LOGGER);
6996
+ const logger = parentInjector.get(DynamicFormLogger);
6782
6997
  logger.error(`Failed to load component for field type '${template.type}' at index ${index} ` +
6783
6998
  `within array '${arrayField.key}'. Ensure the field type is registered in your field registry.`, error);
6784
6999
  }
@@ -6802,7 +7017,7 @@ class ArrayFieldComponent {
6802
7017
  parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
6803
7018
  parentInjector = inject(Injector);
6804
7019
  eventBus = inject(EventBus);
6805
- logger = inject(DYNAMIC_FORM_LOGGER);
7020
+ logger = inject(DynamicFormLogger);
6806
7021
  // ─────────────────────────────────────────────────────────────────────────────
6807
7022
  // Inputs
6808
7023
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6978,14 +7193,14 @@ class ArrayFieldComponent {
6978
7193
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6979
7194
  parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
6980
7195
  }
6981
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6982
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.5", type: ArrayFieldComponent, isStandalone: true, selector: "array-field", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()" }, classAttribute: "df-field df-array" }, ngImport: i0, template: `
7196
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7197
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: ArrayFieldComponent, isStandalone: true, selector: "array-field", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()" }, classAttribute: "df-field df-array" }, ngImport: i0, template: `
6983
7198
  @for (item of resolvedItems(); track item.id) {
6984
7199
  <ng-container *ngComponentOutlet="item.component; injector: item.injector; inputs: item.inputs()" />
6985
7200
  }
6986
7201
  `, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}:host>*{grid-column:1/-1}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6987
7202
  }
6988
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: ArrayFieldComponent, decorators: [{
7203
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: ArrayFieldComponent, decorators: [{
6989
7204
  type: Component,
6990
7205
  args: [{ selector: 'array-field', imports: [NgComponentOutlet], template: `
6991
7206
  @for (item of resolvedItems(); track item.id) {
@@ -7081,16 +7296,31 @@ function extractErrorParams(error) {
7081
7296
  params.min = error.min;
7082
7297
  if ('max' in error)
7083
7298
  params.max = error.max;
7084
- if ('requiredLength' in error)
7085
- params.requiredLength = error.requiredLength;
7086
- if ('actualLength' in error)
7087
- params.actualLength = error.actualLength;
7088
7299
  if ('pattern' in error)
7089
7300
  params.pattern = error.pattern;
7090
7301
  if ('actual' in error)
7091
7302
  params.actual = error.actual;
7092
7303
  if ('expected' in error)
7093
7304
  params.expected = error.expected;
7305
+ // Handle length validation parameters
7306
+ // Angular Signal Forms uses 'minLength'/'maxLength' properties
7307
+ // Angular Classic Forms uses 'requiredLength'/'actualLength' properties
7308
+ // We support both formats and normalize to 'requiredLength' for message templates
7309
+ if ('requiredLength' in error) {
7310
+ params.requiredLength = error.requiredLength;
7311
+ }
7312
+ else if ('minLength' in error) {
7313
+ // Angular Signal Forms: minLength property maps to requiredLength for templates
7314
+ params.requiredLength = error.minLength;
7315
+ params.minLength = error.minLength;
7316
+ }
7317
+ else if ('maxLength' in error) {
7318
+ // Angular Signal Forms: maxLength property maps to requiredLength for templates
7319
+ params.requiredLength = error.maxLength;
7320
+ params.maxLength = error.maxLength;
7321
+ }
7322
+ if ('actualLength' in error)
7323
+ params.actualLength = error.actualLength;
7094
7324
  // Include all custom properties from error object (for custom validators)
7095
7325
  // Skip 'kind' as it's the error type identifier, not a parameter
7096
7326
  Object.keys(error).forEach((key) => {
@@ -7132,5 +7362,5 @@ function extractErrorParams(error) {
7132
7362
  * Generated bundle index. Do not edit.
7133
7363
  */
7134
7364
 
7135
- export { omit as $, AddArrayItemEvent as A, buildBaseInputs as B, arrayFieldMapper as C, DYNAMIC_FORM_LOGGER as D, EventBus as E, FormClearEvent as F, groupFieldMapper as G, pageFieldMapper as H, rowFieldMapper as I, textFieldMapper as J, FIELD_REGISTRY as K, LogLevel as L, ARRAY_CONTEXT as M, NoopLogger as N, FIELD_SIGNAL_CONTEXT as O, PageChangeEvent as P, dynamicTextToObservable as Q, RemoveArrayItemEvent as R, SubmitEvent as S, ArrayFieldComponent as T, GroupFieldComponent as U, RowFieldComponent as V, applyValidator as W, applyValidators as X, getArrayLength as Y, resolveTokens as Z, BUILT_IN_FIELDS as _, DynamicTextPipe as a, interpolateParams as a0, DynamicForm as b, createFieldResolutionPipe as c, derivedFromDeferred as d, emitComponentInitialized as e, isFormStateCondition as f, resolveNextButtonDisabled as g, isCheckedField as h, injectFieldRegistry as i, isValueField as j, isRowField as k, isArrayField as l, isContainerField as m, isDisplayOnlyField as n, isGroupField as o, provideDynamicForm as p, isLeafField as q, resolveSubmitButtonDisabled as r, isPageField as s, isValueBearingField as t, FormResetEvent as u, validatePageNesting as v, withLogger as w, NextPageEvent as x, PreviousPageEvent as y, baseFieldMapper as z };
7136
- //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-B_676NHl.mjs.map
7365
+ export { BUILT_IN_FIELDS as $, AddArrayItemEvent as A, baseFieldMapper as B, ConsoleLogger as C, DynamicFormLogger as D, EventBus as E, FormClearEvent as F, buildBaseInputs as G, arrayFieldMapper as H, groupFieldMapper as I, pageFieldMapper as J, rowFieldMapper as K, textFieldMapper as L, FIELD_REGISTRY as M, NoopLogger as N, ARRAY_CONTEXT as O, PageChangeEvent as P, FIELD_SIGNAL_CONTEXT as Q, RemoveArrayItemEvent as R, SubmitEvent as S, dynamicTextToObservable as T, ArrayFieldComponent as U, GroupFieldComponent as V, RowFieldComponent as W, applyValidator as X, applyValidators as Y, getArrayLength as Z, resolveTokens as _, DynamicTextPipe as a, omit as a0, interpolateParams as a1, applyMetaToElement as a2, DynamicForm as b, createFieldResolutionPipe as c, derivedFromDeferred as d, emitComponentInitialized as e, isFormStateCondition as f, resolveNextButtonDisabled as g, isCheckedField as h, injectFieldRegistry as i, isValueField as j, isRowField as k, isArrayField as l, isContainerField as m, isDisplayOnlyField as n, isGroupField as o, provideDynamicForm as p, isLeafField as q, resolveSubmitButtonDisabled as r, isPageField as s, isValueBearingField as t, FormResetEvent as u, validatePageNesting as v, withLoggerConfig as w, NextPageEvent as x, PreviousPageEvent as y, DynamicFormError as z };
7366
+ //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-CMSmISSp.mjs.map