@ng-forge/dynamic-forms 0.1.2 → 0.2.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 (19) hide show
  1. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +313 -0
  2. package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -0
  3. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-BWj75LVN.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-CZBdtTAU.mjs} +977 -1198
  4. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CZBdtTAU.mjs.map +1 -0
  5. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-ZVrJUMNb.mjs → ng-forge-dynamic-forms-page-field.component-BM32StxL.mjs} +18 -25
  6. package/fesm2022/ng-forge-dynamic-forms-page-field.component-BM32StxL.mjs.map +1 -0
  7. package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-CbctXjtc.mjs +31 -0
  8. package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-CbctXjtc.mjs.map +1 -0
  9. package/fesm2022/ng-forge-dynamic-forms-testing.mjs +971 -0
  10. package/fesm2022/ng-forge-dynamic-forms-testing.mjs.map +1 -0
  11. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BKTbxAKn.mjs → ng-forge-dynamic-forms-text-field.component-C0I8jf9T.mjs} +5 -5
  12. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BKTbxAKn.mjs.map → ng-forge-dynamic-forms-text-field.component-C0I8jf9T.mjs.map} +1 -1
  13. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  14. package/package.json +12 -4
  15. package/types/ng-forge-dynamic-forms-integration.d.ts +327 -0
  16. package/types/ng-forge-dynamic-forms-testing.d.ts +566 -0
  17. package/types/ng-forge-dynamic-forms.d.ts +217 -495
  18. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BWj75LVN.mjs.map +0 -1
  19. package/fesm2022/ng-forge-dynamic-forms-page-field.component-ZVrJUMNb.mjs.map +0 -1
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
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, pattern, maxLength, minLength, max, min, email, 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 } from '@angular/forms/signals';
5
5
  import { toObservable, toSignal, takeUntilDestroyed, outputFromObservable } from '@angular/core/rxjs-interop';
6
- import { defer, combineLatest, from, map, catchError, of, isObservable, firstValueFrom, switchMap, EMPTY, Subject, filter, pipe, forkJoin, scan as scan$1 } from 'rxjs';
7
- import { scan, map as map$1, filter as filter$1, take, switchMap as switchMap$1, shareReplay } from 'rxjs/operators';
6
+ import { defer, combineLatest, from, map, catchError, of, pipe, switchMap, forkJoin, scan, isObservable, firstValueFrom, EMPTY, Subject, filter } from 'rxjs';
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
9
 
10
10
  /**
@@ -73,30 +73,14 @@ function derivedFromDeferred(source, operator, options) {
73
73
  * // Returns: ''
74
74
  * ```
75
75
  *
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<any>;
88
- * getGridClassString = getGridClassString;
89
- * }
90
- * ```
91
- *
92
76
  * @public
93
77
  */
94
78
  function getGridClassString(fieldDef) {
95
- const classes = [];
96
- if (typeof fieldDef.col === 'number' && fieldDef.col > 0 && fieldDef.col <= 12) {
97
- classes.push(`df-col-${fieldDef.col}`);
79
+ const col = fieldDef.col;
80
+ if (typeof col === 'number' && Number.isInteger(col) && col > 0 && col <= 12) {
81
+ return `df-col-${col}`;
98
82
  }
99
- return classes.join(' ');
83
+ return '';
100
84
  }
101
85
 
102
86
  /**
@@ -228,6 +212,19 @@ function reconcileFields(prev, curr) {
228
212
  return field;
229
213
  });
230
214
  }
215
+ /**
216
+ * Creates an RxJS pipe for resolving field definitions to rendered components.
217
+ * Used by container components (page, group) to resolve their child fields.
218
+ */
219
+ function createFieldResolutionPipe(getContext) {
220
+ return pipe(switchMap((fields) => {
221
+ if (!fields || fields.length === 0) {
222
+ return of([]);
223
+ }
224
+ const context = getContext();
225
+ return forkJoin(fields.map((f) => resolveField(f, context)));
226
+ }), map((fields) => fields.filter((f) => f !== undefined)), scan(reconcileFields, []));
227
+ }
231
228
 
232
229
  /**
233
230
  * Wraps a submission action to handle both Promise and Observable returns.
@@ -236,7 +233,7 @@ function reconcileFields(prev, curr) {
236
233
  * @param action - The submission action function
237
234
  * @returns A wrapped function that always returns a Promise
238
235
  */
239
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
236
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Form tree type is dynamic, action signatures vary
240
237
  function wrapSubmissionAction(action) {
241
238
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
242
239
  return (formTree) => {
@@ -332,7 +329,7 @@ function omit(obj, keys) {
332
329
  */
333
330
  function keyBy(array, key) {
334
331
  return array.reduce((acc, item) => {
335
- acc[item[key]] = item;
332
+ acc[String(item[key])] = item;
336
333
  return acc;
337
334
  }, {});
338
335
  }
@@ -357,29 +354,49 @@ function mapValues(obj, fn) {
357
354
  }, {});
358
355
  }
359
356
  /**
360
- * Memoizes a function with optional custom cache key resolver
361
- * Native replacement for lodash memoize()
357
+ * Options for memoize function
358
+ */
359
+ /** Default maximum cache size for memoized functions */
360
+ const DEFAULT_MEMOIZE_MAX_SIZE = 100;
361
+ /**
362
+ * Memoizes a function with LRU cache eviction and optional custom cache key resolver.
362
363
  *
363
364
  * @param fn - Function to memoize
364
- * @param resolver - Optional function to generate cache key from arguments
365
+ * @param resolverOrOptions - Optional key resolver function or options object
365
366
  * @returns Memoized function
366
367
  *
367
368
  * @example
368
369
  * ```typescript
369
370
  * const expensive = (a: number, b: number) => a + b;
370
- * const memoized = memoize(expensive, (a, b) => `${a}-${b}`);
371
- * memoized(1, 2); // Computed
372
- * memoized(1, 2); // Cached
371
+ * const memoized = memoize(expensive); // Uses default maxSize of 100
372
+ *
373
+ * // With custom resolver
374
+ * const withResolver = memoize(expensive, (a, b) => `${a}-${b}`);
375
+ *
376
+ * // With custom maxSize
377
+ * const small = memoize(expensive, { maxSize: 10 });
373
378
  * ```
374
379
  */
375
- function memoize(fn, resolver) {
380
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
381
+ function memoize(fn, resolverOrOptions) {
382
+ const options = typeof resolverOrOptions === 'function' ? { resolver: resolverOrOptions } : (resolverOrOptions ?? {});
383
+ const { resolver, maxSize = DEFAULT_MEMOIZE_MAX_SIZE } = options;
376
384
  const cache = new Map();
377
385
  return ((...args) => {
378
386
  const key = resolver ? resolver(...args) : JSON.stringify(args);
379
- if (cache.has(key)) {
380
- return cache.get(key);
387
+ const cachedValue = cache.get(key);
388
+ if (cachedValue !== undefined) {
389
+ cache.delete(key);
390
+ cache.set(key, cachedValue);
391
+ return cachedValue;
381
392
  }
382
393
  const result = fn(...args);
394
+ if (cache.size >= maxSize) {
395
+ const oldestKey = cache.keys().next().value;
396
+ if (oldestKey !== undefined) {
397
+ cache.delete(oldestKey);
398
+ }
399
+ }
383
400
  cache.set(key, result);
384
401
  return result;
385
402
  });
@@ -421,15 +438,27 @@ function isEqual(a, b) {
421
438
  }
422
439
  // Handle objects
423
440
  if (typeof a === 'object' && typeof b === 'object') {
424
- const keysA = Object.keys(a);
425
- const keysB = Object.keys(b);
441
+ const objA = a;
442
+ const objB = b;
443
+ const keysA = Object.keys(objA);
444
+ const keysB = Object.keys(objB);
426
445
  if (keysA.length !== keysB.length)
427
446
  return false;
428
- return keysA.every((key) => keysB.includes(key) && isEqual(a[key], b[key]));
447
+ return keysA.every((key) => keysB.includes(key) && isEqual(objA[key], objB[key]));
429
448
  }
430
449
  // Primitives - use Object.is to handle NaN correctly (NaN === NaN is false, but Object.is(NaN, NaN) is true)
431
450
  return Object.is(a, b);
432
451
  }
452
+ /**
453
+ * Normalizes a fields collection to an array.
454
+ * Handles both array format and object format (keyed by field key).
455
+ *
456
+ * @param fields - Fields as array or object
457
+ * @returns Fields as array
458
+ */
459
+ function normalizeFieldsArray(fields) {
460
+ return Array.isArray(fields) ? [...fields] : Object.values(fields);
461
+ }
433
462
 
434
463
  /**
435
464
  * Injection token for providing field signal context to mappers and components.
@@ -490,6 +519,21 @@ const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT', {
490
519
  */
491
520
  const ARRAY_CONTEXT = new InjectionToken('ARRAY_CONTEXT');
492
521
 
522
+ /**
523
+ * Base error class for all Dynamic Forms errors.
524
+ *
525
+ * This class centralizes the `[Dynamic Forms]` prefix, ensuring consistent
526
+ * error messaging across the library without requiring each error site to
527
+ * manually include the prefix.
528
+ */
529
+ class DynamicFormError extends Error {
530
+ static PREFIX = '[Dynamic Forms]';
531
+ constructor(message) {
532
+ super(`${DynamicFormError.PREFIX} ${message}`);
533
+ this.name = 'DynamicFormError';
534
+ }
535
+ }
536
+
493
537
  /**
494
538
  * Injection token for the global field type registry.
495
539
  *
@@ -619,20 +663,21 @@ function injectFieldRegistry() {
619
663
  async loadTypeComponent(name) {
620
664
  const fieldType = registry.get(name);
621
665
  if (!fieldType) {
622
- throw new Error(`[Dynamic Forms] Field type "${name}" is not registered`);
666
+ throw new DynamicFormError(`Field type "${name}" is not registered`);
623
667
  }
624
668
  // Handle async loading
625
669
  if (fieldType.loadComponent) {
626
670
  try {
627
671
  const result = await fieldType.loadComponent();
628
672
  // Handle ES module imports - extract default export if needed
629
- return result?.default || result;
673
+ const moduleResult = result;
674
+ return (typeof moduleResult === 'object' && 'default' in moduleResult && moduleResult.default) || result;
630
675
  }
631
676
  catch (error) {
632
- throw new Error(`[Dynamic Forms] Failed to load component for field type "${name}": ${error}`);
677
+ throw new DynamicFormError(`Failed to load component for field type "${name}": ${error}`);
633
678
  }
634
679
  }
635
- throw new Error(`[Dynamic Forms] Field type "${name}" has no component or loadComponent function`);
680
+ throw new DynamicFormError(`Field type "${name}" has no component or loadComponent function`);
636
681
  },
637
682
  /**
638
683
  * Gets all registered field type definitions.
@@ -691,6 +736,14 @@ function injectFieldRegistry() {
691
736
  };
692
737
  }
693
738
 
739
+ /**
740
+ * Injection token for the dynamic forms logger.
741
+ *
742
+ * Provided by provideDynamicForm() with ConsoleLogger as default.
743
+ * Override with withLoggerConfig(false) to disable logging.
744
+ */
745
+ const DynamicFormLogger = new InjectionToken('DynamicFormLogger');
746
+
694
747
  /**
695
748
  * Registry service that maintains references to root form fields.
696
749
  * This provides a type-safe way to access root form values from child fields
@@ -848,10 +901,10 @@ class RootFormRegistryService {
848
901
  this._formValueSignals.set(new Map());
849
902
  this.formContexts.clear();
850
903
  }
851
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: RootFormRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
852
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: RootFormRegistryService });
904
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RootFormRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
905
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RootFormRegistryService });
853
906
  }
854
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: RootFormRegistryService, decorators: [{
907
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RootFormRegistryService, decorators: [{
855
908
  type: Injectable
856
909
  }] });
857
910
 
@@ -867,6 +920,7 @@ function isChildFieldContext(context) {
867
920
  */
868
921
  class FieldContextRegistryService {
869
922
  rootFormRegistry = inject(RootFormRegistryService);
923
+ logger = inject(DynamicFormLogger);
870
924
  /**
871
925
  * Creates an evaluation context for a field by combining:
872
926
  * - The field's current value
@@ -895,43 +949,25 @@ class FieldContextRegistryService {
895
949
  // - Validators still run when their own field value changes (via fieldValue)
896
950
  // - Cross-field validation still works, just without cascading re-evaluation
897
951
  const formValue = untracked(() => this.rootFormRegistry.getFormValue(formId));
898
- // Get field path from form context
899
- const formContext = this.rootFormRegistry.getFormContext(formId);
900
- const fieldPath = this.extractFieldPath(fieldContext, formContext);
952
+ const fieldPath = this.extractFieldPath(fieldContext);
901
953
  return {
902
954
  fieldValue,
903
955
  formValue,
904
956
  fieldPath,
905
957
  customFunctions: customFunctions || {},
958
+ logger: this.logger,
906
959
  };
907
960
  }
908
961
  /**
909
- * Registers a field path mapping for a specific form.
910
- * This can be called when fields are created to maintain path information.
911
- */
912
- registerFieldPath(fieldKey, parentPath, formId = 'default') {
913
- const context = this.rootFormRegistry.getFormContext(formId);
914
- const fullPath = parentPath ? `${parentPath}.${fieldKey}` : String(fieldKey);
915
- context.fieldPaths = context.fieldPaths || {};
916
- context.fieldPaths[String(fieldKey)] = fullPath;
917
- this.rootFormRegistry.registerFormContext(context, formId);
918
- }
919
- /**
920
- * Extracts the field path for a given field context.
921
- * This is a simplified implementation that could be enhanced
922
- * based on the specific needs of the form library.
962
+ * Extracts the field path (key) for a given field context.
923
963
  */
924
- extractFieldPath(fieldContext, formContext) {
925
- // Check if the field context has a key property (for child fields)
926
- const extendedContext = fieldContext;
927
- if (isChildFieldContext(extendedContext)) {
964
+ extractFieldPath(fieldContext) {
965
+ if (isChildFieldContext(fieldContext)) {
928
966
  try {
929
- const key = extendedContext.key();
930
- const fieldPaths = formContext.fieldPaths || {};
931
- return fieldPaths[String(key)] || String(key);
967
+ return String(fieldContext.key());
932
968
  }
933
969
  catch (error) {
934
- console.warn('[Dynamic Forms] Unable to extract field key:', error);
970
+ this.logger.warn('Unable to extract field key:', error);
935
971
  }
936
972
  }
937
973
  // For root fields or when key is not available
@@ -957,25 +993,45 @@ class FieldContextRegistryService {
957
993
  * hoisted to form-level using validateTree.
958
994
  */
959
995
  createReactiveEvaluationContext(fieldContext, customFunctions, formId = 'default') {
960
- // Read field value reactively (creates a dependency)
961
996
  const fieldValue = fieldContext.value();
962
- // Get form value using the new unified method that prefers the direct signal
963
- // This creates reactive dependencies on the form value signal
964
997
  const formValue = this.rootFormRegistry.getFormValue(formId);
965
- // Get field path from form context
966
- const formContext = this.rootFormRegistry.getFormContext(formId);
967
- const fieldPath = this.extractFieldPath(fieldContext, formContext);
998
+ const fieldPath = this.extractFieldPath(fieldContext);
968
999
  return {
969
1000
  fieldValue,
970
1001
  formValue,
971
1002
  fieldPath,
972
1003
  customFunctions: customFunctions || {},
1004
+ logger: this.logger,
1005
+ };
1006
+ }
1007
+ /**
1008
+ * Creates an evaluation context for display-only components (text fields, pages)
1009
+ * that don't have their own FieldContext.
1010
+ *
1011
+ * This is useful for:
1012
+ * - Text fields (display-only, not part of form schema)
1013
+ * - Pages (containers that need to evaluate visibility logic)
1014
+ *
1015
+ * Uses reactive form value access to allow logic re-evaluation when form values change.
1016
+ *
1017
+ * @param fieldPath - The key/path of the display-only component
1018
+ * @param customFunctions - Optional custom functions for expression evaluation
1019
+ * @param formId - Optional form identifier (defaults to 'default')
1020
+ */
1021
+ createDisplayOnlyContext(fieldPath, customFunctions, formId = 'default') {
1022
+ const formValue = this.rootFormRegistry.getFormValue(formId);
1023
+ return {
1024
+ fieldValue: undefined,
1025
+ formValue,
1026
+ fieldPath,
1027
+ customFunctions: customFunctions || {},
1028
+ logger: this.logger,
973
1029
  };
974
1030
  }
975
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
976
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FieldContextRegistryService });
1031
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1032
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: FieldContextRegistryService });
977
1033
  }
978
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FieldContextRegistryService, decorators: [{
1034
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: FieldContextRegistryService, decorators: [{
979
1035
  type: Injectable
980
1036
  }] });
981
1037
 
@@ -1315,10 +1371,10 @@ class FunctionRegistryService {
1315
1371
  this.clearAsyncValidators();
1316
1372
  this.clearHttpValidators();
1317
1373
  }
1318
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1319
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FunctionRegistryService });
1374
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1375
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: FunctionRegistryService });
1320
1376
  }
1321
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FunctionRegistryService, decorators: [{
1377
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: FunctionRegistryService, decorators: [{
1322
1378
  type: Injectable
1323
1379
  }] });
1324
1380
 
@@ -1461,12 +1517,11 @@ var TokenType;
1461
1517
  TokenType["RBRACKET"] = "RBRACKET";
1462
1518
  // Special
1463
1519
  TokenType["EOF"] = "EOF";
1464
- TokenType["TYPEOF"] = "TYPEOF";
1465
1520
  })(TokenType || (TokenType = {}));
1466
1521
  /**
1467
1522
  * Parser error with position information
1468
1523
  */
1469
- class ExpressionParserError extends Error {
1524
+ class ExpressionParserError extends DynamicFormError {
1470
1525
  position;
1471
1526
  expression;
1472
1527
  constructor(message, position, expression) {
@@ -1564,7 +1619,7 @@ class Tokenizer {
1564
1619
  this.position++;
1565
1620
  }
1566
1621
  }
1567
- throw new ExpressionParserError('[Dynamic Forms] Unterminated string literal', start, this.expression);
1622
+ throw new ExpressionParserError('Unterminated string literal', start, this.expression);
1568
1623
  }
1569
1624
  readNumber() {
1570
1625
  const start = this.position;
@@ -1596,8 +1651,6 @@ class Tokenizer {
1596
1651
  return TokenType.NULL;
1597
1652
  case 'undefined':
1598
1653
  return TokenType.UNDEFINED;
1599
- case 'typeof':
1600
- return TokenType.TYPEOF;
1601
1654
  default:
1602
1655
  return TokenType.IDENTIFIER;
1603
1656
  }
@@ -1671,7 +1724,7 @@ class Tokenizer {
1671
1724
  case ']':
1672
1725
  return { type: TokenType.RBRACKET, value: ']', position: start };
1673
1726
  default:
1674
- throw new ExpressionParserError(`[Dynamic Forms] Unexpected character: ${char}`, start, this.expression);
1727
+ throw new ExpressionParserError(`Unexpected character: ${char}`, start, this.expression);
1675
1728
  }
1676
1729
  }
1677
1730
  }
@@ -1695,7 +1748,7 @@ class Parser {
1695
1748
  this.tokens = tokenizer.tokenize();
1696
1749
  this.current = 0;
1697
1750
  if (this.isAtEnd()) {
1698
- throw new ExpressionParserError('[Dynamic Forms] Empty expression', 0, this.expression);
1751
+ throw new ExpressionParserError('Empty expression', 0, this.expression);
1699
1752
  }
1700
1753
  return this.parseExpression();
1701
1754
  }
@@ -1757,7 +1810,7 @@ class Parser {
1757
1810
  return node;
1758
1811
  }
1759
1812
  parseUnary() {
1760
- if (this.match(TokenType.NOT, TokenType.MINUS, TokenType.PLUS, TokenType.TYPEOF)) {
1813
+ if (this.match(TokenType.NOT, TokenType.MINUS, TokenType.PLUS)) {
1761
1814
  const operator = this.previous().value;
1762
1815
  const operand = this.parseUnary();
1763
1816
  return { type: 'UnaryOp', operator, operand };
@@ -1770,7 +1823,7 @@ class Parser {
1770
1823
  if (this.match(TokenType.DOT)) {
1771
1824
  // Member access: obj.property
1772
1825
  if (!this.check(TokenType.IDENTIFIER)) {
1773
- throw new ExpressionParserError('[Dynamic Forms] Expected property name after "."', this.peek().position, this.expression);
1826
+ throw new ExpressionParserError('Expected property name after "."', this.peek().position, this.expression);
1774
1827
  }
1775
1828
  const property = this.advance().value;
1776
1829
  node = { type: 'MemberAccess', object: node, property };
@@ -1823,7 +1876,7 @@ class Parser {
1823
1876
  this.consume(TokenType.RPAREN, 'Expected ")" after expression');
1824
1877
  return expr;
1825
1878
  }
1826
- throw new ExpressionParserError(`[Dynamic Forms] Unexpected token: ${this.peek().value}`, this.peek().position, this.expression);
1879
+ throw new ExpressionParserError(`Unexpected token: ${this.peek().value}`, this.peek().position, this.expression);
1827
1880
  }
1828
1881
  parseArgumentList() {
1829
1882
  const args = [];
@@ -1874,7 +1927,7 @@ class Parser {
1874
1927
  consume(type, message) {
1875
1928
  if (this.check(type))
1876
1929
  return this.advance();
1877
- throw new ExpressionParserError(`[Dynamic Forms] ${message}`, this.peek().position, this.expression);
1930
+ throw new ExpressionParserError(message, this.peek().position, this.expression);
1878
1931
  }
1879
1932
  }
1880
1933
 
@@ -2026,14 +2079,14 @@ class Evaluator {
2026
2079
  case 'ArrayLiteral':
2027
2080
  return this.evaluateArrayLiteral(node);
2028
2081
  default:
2029
- throw new ExpressionParserError(`[Dynamic Forms] Unknown node type: ${node.type}`, 0, this.expression);
2082
+ throw new ExpressionParserError(`Unknown node type: ${node.type}`, 0, this.expression);
2030
2083
  }
2031
2084
  }
2032
2085
  evaluateIdentifier(name) {
2033
- if (!(name in this.scope)) {
2034
- return undefined;
2086
+ if (name in this.scope) {
2087
+ return this.scope[name];
2035
2088
  }
2036
- return this.scope[name];
2089
+ return undefined;
2037
2090
  }
2038
2091
  evaluateMemberAccess(node) {
2039
2092
  const obj = this.evaluate(node.object);
@@ -2042,7 +2095,7 @@ class Evaluator {
2042
2095
  }
2043
2096
  // Block access to dangerous properties that could leak information or enable attacks
2044
2097
  if (BLOCKED_PROPERTIES.has(node.property)) {
2045
- throw new ExpressionParserError(`[Dynamic Forms] Property "${node.property}" is not accessible for security reasons`, 0, this.expression);
2098
+ throw new ExpressionParserError(`Property "${node.property}" is not accessible for security reasons`, 0, this.expression);
2046
2099
  }
2047
2100
  // Allow property access on plain objects and primitives
2048
2101
  if (typeof obj === 'object' || typeof obj === 'string' || typeof obj === 'number') {
@@ -2051,6 +2104,16 @@ class Evaluator {
2051
2104
  return undefined;
2052
2105
  }
2053
2106
  evaluateBinaryOp(node) {
2107
+ // Handle short-circuit evaluation for logical operators
2108
+ if (node.operator === '&&') {
2109
+ const left = this.evaluate(node.left);
2110
+ return left ? this.evaluate(node.right) : left;
2111
+ }
2112
+ if (node.operator === '||') {
2113
+ const left = this.evaluate(node.left);
2114
+ return left ? left : this.evaluate(node.right);
2115
+ }
2116
+ // For all other operators, evaluate both sides
2054
2117
  const left = this.evaluate(node.left);
2055
2118
  const right = this.evaluate(node.right);
2056
2119
  switch (node.operator) {
@@ -2062,8 +2125,12 @@ class Evaluator {
2062
2125
  case '*':
2063
2126
  return left * right;
2064
2127
  case '/':
2128
+ if (right === 0)
2129
+ return null;
2065
2130
  return left / right;
2066
2131
  case '%':
2132
+ if (right === 0)
2133
+ return null;
2067
2134
  return left % right;
2068
2135
  // Comparison - using == and != intentionally for loose equality
2069
2136
  case '==':
@@ -2082,13 +2149,8 @@ class Evaluator {
2082
2149
  return left >= right;
2083
2150
  case '<=':
2084
2151
  return left <= right;
2085
- // Logical
2086
- case '&&':
2087
- return left && right;
2088
- case '||':
2089
- return left || right;
2090
2152
  default:
2091
- throw new ExpressionParserError(`[Dynamic Forms] Unknown binary operator: ${node.operator}`, 0, this.expression);
2153
+ throw new ExpressionParserError(`Unknown binary operator: ${node.operator}`, 0, this.expression);
2092
2154
  }
2093
2155
  }
2094
2156
  evaluateUnaryOp(node) {
@@ -2100,26 +2162,24 @@ class Evaluator {
2100
2162
  return -operand;
2101
2163
  case '+':
2102
2164
  return +operand;
2103
- case 'typeof':
2104
- return typeof operand;
2105
2165
  default:
2106
- throw new ExpressionParserError(`[Dynamic Forms] Unknown unary operator: ${node.operator}`, 0, this.expression);
2166
+ throw new ExpressionParserError(`Unknown unary operator: ${node.operator}`, 0, this.expression);
2107
2167
  }
2108
2168
  }
2109
2169
  evaluateCallExpression(node) {
2110
2170
  // Only allow method calls (member access), not arbitrary function calls
2111
2171
  if (node.callee.type !== 'MemberAccess') {
2112
- throw new ExpressionParserError('[Dynamic Forms] Only method calls are allowed, not arbitrary function calls', 0, this.expression);
2172
+ throw new ExpressionParserError('Only method calls are allowed, not arbitrary function calls', 0, this.expression);
2113
2173
  }
2114
2174
  const obj = this.evaluate(node.callee.object);
2115
2175
  const methodName = node.callee.property;
2116
2176
  // Block access to dangerous properties (must check before method call)
2117
2177
  if (BLOCKED_PROPERTIES.has(methodName)) {
2118
- throw new ExpressionParserError(`[Dynamic Forms] Property "${methodName}" is not accessible for security reasons`, 0, this.expression);
2178
+ throw new ExpressionParserError(`Property "${methodName}" is not accessible for security reasons`, 0, this.expression);
2119
2179
  }
2120
- // Check if the method is in the whitelist
2180
+ // Check if the method is in the instance method whitelist
2121
2181
  if (!this.isMethodSafe(obj, methodName)) {
2122
- throw new ExpressionParserError(`[Dynamic Forms] Method "${methodName}" is not allowed for security reasons`, 0, this.expression);
2182
+ throw new ExpressionParserError(`Method "${methodName}" is not allowed for security reasons`, 0, this.expression);
2123
2183
  }
2124
2184
  // Evaluate arguments
2125
2185
  const args = node.arguments.map((arg) => this.evaluate(arg));
@@ -2237,7 +2297,7 @@ class ExpressionParser {
2237
2297
  throw error;
2238
2298
  }
2239
2299
  // Wrap other errors for consistency
2240
- throw new ExpressionParserError(`[Dynamic Forms] Error evaluating expression: ${error instanceof Error ? error.message : String(error)}`, 0, expression);
2300
+ throw new ExpressionParserError(`Error evaluating expression: ${error instanceof Error ? error.message : String(error)}`, 0, expression);
2241
2301
  }
2242
2302
  }
2243
2303
  /**
@@ -2299,7 +2359,7 @@ function evaluateJavaScriptExpression(expression, context) {
2299
2359
  return !!result;
2300
2360
  }
2301
2361
  catch (error) {
2302
- console.error('[Dynamic Forms] Error evaluating JavaScript expression:', expression.expression, error);
2362
+ context.logger.error('Error evaluating JavaScript expression:', expression.expression, error);
2303
2363
  return false;
2304
2364
  }
2305
2365
  }
@@ -2308,14 +2368,14 @@ function evaluateCustomFunction(expression, context) {
2308
2368
  return false;
2309
2369
  const customFn = context.customFunctions?.[expression.expression];
2310
2370
  if (!customFn) {
2311
- console.error('[Dynamic Forms] Custom function not found:', expression.expression);
2371
+ context.logger.error('Custom function not found:', expression.expression);
2312
2372
  return false;
2313
2373
  }
2314
2374
  try {
2315
2375
  return !!customFn(context);
2316
2376
  }
2317
2377
  catch (error) {
2318
- console.error('[Dynamic Forms] Error executing custom function:', expression.expression, error);
2378
+ context.logger.error('Error executing custom function:', expression.expression, error);
2319
2379
  return false;
2320
2380
  }
2321
2381
  }
@@ -2332,6 +2392,56 @@ function evaluateOrCondition(expression, context) {
2332
2392
  return expression.conditions.some((condition) => evaluateCondition(condition, context));
2333
2393
  }
2334
2394
 
2395
+ /**
2396
+ * Cache for memoized logic functions, keyed by injection context.
2397
+ * Uses WeakMap with FunctionRegistryService as outer key to automatically clean up.
2398
+ * Inner structure: Map<FieldContextRegistryService, Map<expressionKey, LogicFn>>
2399
+ */
2400
+ const logicFunctionCache = new WeakMap();
2401
+ /**
2402
+ * Serializes a value to a deterministic string for cache key generation.
2403
+ * Unlike JSON.stringify, this sorts object keys to ensure consistent output
2404
+ * regardless of property insertion order.
2405
+ */
2406
+ function stableStringify(value) {
2407
+ if (value === null || value === undefined) {
2408
+ return String(value);
2409
+ }
2410
+ if (typeof value !== 'object') {
2411
+ return JSON.stringify(value);
2412
+ }
2413
+ if (Array.isArray(value)) {
2414
+ return '[' + value.map(stableStringify).join(',') + ']';
2415
+ }
2416
+ const obj = value;
2417
+ const sortedKeys = Object.keys(obj).sort();
2418
+ const pairs = sortedKeys.map((key) => JSON.stringify(key) + ':' + stableStringify(obj[key]));
2419
+ return '{' + pairs.join(',') + '}';
2420
+ }
2421
+ /**
2422
+ * Serializes a conditional expression to a deterministic string key for caching.
2423
+ * Uses stable stringification to ensure equivalent objects produce the same key
2424
+ * regardless of property order.
2425
+ */
2426
+ function serializeExpression(expression) {
2427
+ return stableStringify(expression);
2428
+ }
2429
+ /**
2430
+ * Gets or creates the cache map for a specific injection context.
2431
+ */
2432
+ function getContextCache(ctx) {
2433
+ let outerCache = logicFunctionCache.get(ctx.functionRegistry);
2434
+ if (!outerCache) {
2435
+ outerCache = new WeakMap();
2436
+ logicFunctionCache.set(ctx.functionRegistry, outerCache);
2437
+ }
2438
+ let innerCache = outerCache.get(ctx.fieldContextRegistry);
2439
+ if (!innerCache) {
2440
+ innerCache = new Map();
2441
+ outerCache.set(ctx.fieldContextRegistry, innerCache);
2442
+ }
2443
+ return innerCache;
2444
+ }
2335
2445
  /**
2336
2446
  * Create a logic function from a conditional expression.
2337
2447
  *
@@ -2343,37 +2453,78 @@ function evaluateOrCondition(expression, context) {
2343
2453
  * NOTE: For validators, use createEvaluationContext directly (with untracked) to prevent
2344
2454
  * infinite reactive loops. Validators with cross-field dependencies should be hoisted
2345
2455
  * to form-level using validateTree.
2456
+ *
2457
+ * @param expression The conditional expression to evaluate
2458
+ * @returns A LogicFn that evaluates the condition in the context of a field
2346
2459
  */
2347
2460
  function createLogicFunction(expression) {
2348
2461
  // Inject services during factory creation, not during execution
2349
2462
  const functionRegistry = inject(FunctionRegistryService);
2350
2463
  const fieldContextRegistry = inject(FieldContextRegistryService);
2351
- return (ctx) => {
2464
+ // Get cache for this injection context
2465
+ const contextCache = getContextCache({ functionRegistry, fieldContextRegistry });
2466
+ // Generate cache key from serialized expression
2467
+ const cacheKey = serializeExpression(expression);
2468
+ // Check cache first
2469
+ const cached = contextCache.get(cacheKey);
2470
+ if (cached) {
2471
+ return cached;
2472
+ }
2473
+ const fn = (ctx) => {
2352
2474
  // Create REACTIVE evaluation context for logic functions
2353
2475
  // This allows logic to re-evaluate when dependent fields change
2354
2476
  const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());
2355
2477
  return evaluateCondition(expression, evaluationContext);
2356
2478
  };
2479
+ // Cache the function
2480
+ contextCache.set(cacheKey, fn);
2481
+ return fn;
2357
2482
  }
2358
2483
 
2359
2484
  /**
2360
- * Create a dynamic value function from an expression string
2361
- * Uses secure AST-based parsing instead of eval() or new Function()
2485
+ * Cache for memoized dynamic value functions, keyed by service instance.
2486
+ * Uses WeakMap to automatically clean up when service is garbage collected.
2487
+ * Inner Map is keyed by expression string.
2488
+ */
2489
+ const dynamicValueFunctionCache = new WeakMap();
2490
+ /**
2491
+ * Create a dynamic value function from an expression string.
2492
+ * Uses secure AST-based parsing instead of eval() or new Function().
2493
+ *
2494
+ * @param expression The expression string to evaluate
2495
+ * @returns A LogicFn that evaluates the expression in the context of a field
2362
2496
  */
2363
2497
  function createDynamicValueFunction(expression) {
2364
- return (ctx) => {
2498
+ // Inject services during factory creation, not during execution
2499
+ // This captures the service instance in the closure
2500
+ const fieldContextRegistry = inject(FieldContextRegistryService);
2501
+ const logger = inject(DynamicFormLogger);
2502
+ // Get or create cache for this injection context
2503
+ let contextCache = dynamicValueFunctionCache.get(fieldContextRegistry);
2504
+ if (!contextCache) {
2505
+ contextCache = new Map();
2506
+ dynamicValueFunctionCache.set(fieldContextRegistry, contextCache);
2507
+ }
2508
+ // Check cache first
2509
+ const cached = contextCache.get(expression);
2510
+ if (cached) {
2511
+ return cached;
2512
+ }
2513
+ const fn = (ctx) => {
2365
2514
  // Create evaluation context using the registry-based approach
2366
- const fieldContextRegistry = inject(FieldContextRegistryService);
2367
2515
  const evaluationContext = fieldContextRegistry.createEvaluationContext(ctx);
2368
2516
  try {
2369
- // Use secure AST-based expression parser
2517
+ // Use secure AST-based expression parser (already has LRU cache)
2370
2518
  return ExpressionParser.evaluate(expression, evaluationContext);
2371
2519
  }
2372
2520
  catch (error) {
2373
- console.error('[Dynamic Forms] Error evaluating dynamic expression:', expression, error);
2521
+ logger.error('Error evaluating dynamic expression:', expression, error);
2374
2522
  return undefined;
2375
2523
  }
2376
2524
  };
2525
+ // Cache the function
2526
+ contextCache.set(expression, fn);
2527
+ return fn;
2377
2528
  }
2378
2529
 
2379
2530
  /**
@@ -2601,136 +2752,98 @@ function isCrossFieldSchema(config, context) {
2601
2752
  return isCrossFieldExpression(config.condition, context);
2602
2753
  }
2603
2754
 
2604
- /**
2605
- * Helper to create conditional logic function from when expression.
2606
- * Returns undefined if no when condition is specified.
2607
- * Reduces duplication across validator types.
2608
- */
2609
- function createConditionalLogic(when) {
2610
- return when ? createLogicFunction(when) : undefined;
2755
+ function applyEmailValidator(path) {
2756
+ email(path);
2611
2757
  }
2612
- /**
2613
- * Safely cast a SchemaPathTree to SchemaPath with Supported rules.
2614
- *
2615
- * This is safe when TValue is not an AbstractControl, because:
2616
- * - SchemaPathTree<T> for non-AbstractControl is: SchemaPath<T, Supported> & {...nested}
2617
- * - The intersection type is structurally compatible with SchemaPath<T, Supported>
2618
- * - We only work with signal forms, never reactive forms (AbstractControl)
2619
- *
2620
- * TypeScript can't automatically narrow this because SchemaPathTree is defined as a
2621
- * conditional type, but at runtime the value is always the correct type for our use case.
2622
- */
2623
- function toSupportedPath$2(path) {
2624
- // The cast is safe because SchemaPathTree extends SchemaPath, and we constrain TValue
2625
- // to exclude AbstractControl in our public APIs (we never pass reactive form controls)
2626
- return path;
2758
+ function applyMinValidator(path, value, expression) {
2759
+ if (expression) {
2760
+ min(path, createDynamicValueFunction(expression));
2761
+ }
2762
+ else {
2763
+ min(path, value);
2764
+ }
2627
2765
  }
2628
- /**
2629
- * Apply validator configuration to field path, following the logic pattern.
2630
- *
2631
- * Accepts both SchemaPath and SchemaPathTree to support being called from both
2632
- * individual field mappings and schema functions.
2633
- *
2634
- * Note: ValidatorConfig is runtime configuration that doesn't encode the field's
2635
- * TypeScript type. The type assertions to specific types (string, number) are safe
2636
- * because Angular's validators perform runtime value checks regardless of the static
2637
- * type parameter.
2638
- *
2639
- * Cross-field validators (those referencing formValue.*) are skipped at field level.
2640
- * They are collected by the parent form component using collectCrossFieldEntries
2641
- * and executed at form level using validateTree.
2642
- *
2643
- * @param config The validator configuration
2644
- * @param fieldPath The field path to apply the validator to
2645
- * @param fieldKey Optional field key (used for logging)
2646
- */
2647
- function applyValidator(config, fieldPath, fieldKey) {
2648
- const path = toSupportedPath$2(fieldPath);
2649
- // Check if this is a built-in validator with a cross-field expression
2650
- // These are collected at form level - skip here
2651
- if (isCrossFieldBuiltInValidator(config)) {
2652
- // Cross-field validators are handled at form level via validateTree
2653
- return;
2766
+ function applyMaxValidator(path, value, expression) {
2767
+ if (expression) {
2768
+ max(path, createDynamicValueFunction(expression));
2654
2769
  }
2655
- // Check if the validator has a cross-field `when` condition
2656
- // These are collected at form level - skip here
2657
- if (hasCrossFieldWhenCondition(config)) {
2658
- // Cross-field validators are handled at form level via validateTree
2770
+ else {
2771
+ max(path, value);
2772
+ }
2773
+ }
2774
+ function applyMinLengthValidator(path, value, expression) {
2775
+ if (expression) {
2776
+ minLength(path, createDynamicValueFunction(expression));
2777
+ }
2778
+ else {
2779
+ minLength(path, value);
2780
+ }
2781
+ }
2782
+ function applyMaxLengthValidator(path, value, expression) {
2783
+ if (expression) {
2784
+ maxLength(path, createDynamicValueFunction(expression));
2785
+ }
2786
+ else {
2787
+ maxLength(path, value);
2788
+ }
2789
+ }
2790
+ function applyPatternValidator(path, value, expression) {
2791
+ if (expression) {
2792
+ pattern(path, createDynamicValueFunction(expression));
2793
+ }
2794
+ else {
2795
+ pattern(path, value);
2796
+ }
2797
+ }
2798
+ function createConditionalLogic(when) {
2799
+ return when ? createLogicFunction(when) : undefined;
2800
+ }
2801
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic forms require any at the Angular API boundary
2802
+ function applyValidator(config, fieldPath) {
2803
+ const path = fieldPath;
2804
+ if (isCrossFieldBuiltInValidator(config) || hasCrossFieldWhenCondition(config)) {
2659
2805
  return;
2660
2806
  }
2661
2807
  switch (config.type) {
2662
2808
  case 'required':
2663
2809
  if (config.when) {
2664
- const whenLogic = createLogicFunction(config.when);
2665
- required(path, { when: whenLogic });
2810
+ required(path, { when: createLogicFunction(config.when) });
2666
2811
  }
2667
2812
  else {
2668
2813
  required(path);
2669
2814
  }
2670
2815
  break;
2671
2816
  case 'email':
2672
- email(fieldPath);
2817
+ applyEmailValidator(fieldPath);
2673
2818
  break;
2674
2819
  case 'min':
2675
2820
  if (typeof config.value === 'number') {
2676
- if (config.expression) {
2677
- const dynamicMin = createDynamicValueFunction(config.expression);
2678
- min(fieldPath, dynamicMin);
2679
- }
2680
- else {
2681
- min(fieldPath, config.value);
2682
- }
2821
+ applyMinValidator(fieldPath, config.value, config.expression);
2683
2822
  }
2684
2823
  break;
2685
2824
  case 'max':
2686
2825
  if (typeof config.value === 'number') {
2687
- if (config.expression) {
2688
- const dynamicMax = createDynamicValueFunction(config.expression);
2689
- max(fieldPath, dynamicMax);
2690
- }
2691
- else {
2692
- max(fieldPath, config.value);
2693
- }
2826
+ applyMaxValidator(fieldPath, config.value, config.expression);
2694
2827
  }
2695
2828
  break;
2696
2829
  case 'minLength':
2697
2830
  if (typeof config.value === 'number') {
2698
- if (config.expression) {
2699
- const dynamicMinLength = createDynamicValueFunction(config.expression);
2700
- minLength(fieldPath, dynamicMinLength);
2701
- }
2702
- else {
2703
- minLength(fieldPath, config.value);
2704
- }
2831
+ applyMinLengthValidator(fieldPath, config.value, config.expression);
2705
2832
  }
2706
2833
  break;
2707
2834
  case 'maxLength':
2708
2835
  if (typeof config.value === 'number') {
2709
- if (config.expression) {
2710
- const dynamicMaxLength = createDynamicValueFunction(config.expression);
2711
- // MaxLength validator expects SchemaPath<string>
2712
- maxLength(fieldPath, dynamicMaxLength);
2713
- }
2714
- else {
2715
- maxLength(fieldPath, config.value);
2716
- }
2836
+ applyMaxLengthValidator(fieldPath, config.value, config.expression);
2717
2837
  }
2718
2838
  break;
2719
2839
  case 'pattern':
2720
2840
  if (config.value instanceof RegExp || typeof config.value === 'string') {
2721
2841
  const regexPattern = typeof config.value === 'string' ? new RegExp(config.value) : config.value;
2722
- if (config.expression) {
2723
- const dynamicPattern = createDynamicValueFunction(config.expression);
2724
- // Pattern validator expects SchemaPath<string>
2725
- pattern(fieldPath, dynamicPattern);
2726
- }
2727
- else {
2728
- pattern(fieldPath, regexPattern);
2729
- }
2842
+ applyPatternValidator(fieldPath, regexPattern, config.expression);
2730
2843
  }
2731
2844
  break;
2732
2845
  case 'custom':
2733
- applyCustomValidator(config, path, fieldKey);
2846
+ applyCustomValidator(config, path);
2734
2847
  break;
2735
2848
  case 'customAsync':
2736
2849
  applyAsyncValidator(config, path);
@@ -2740,95 +2853,70 @@ function applyValidator(config, fieldPath, fieldKey) {
2740
2853
  break;
2741
2854
  }
2742
2855
  }
2743
- /**
2744
- * Apply custom validator to field path using Angular's public validate() API
2745
- * Supports both function-based and expression-based validators.
2746
- *
2747
- * Cross-field validators (those referencing formValue.*) are skipped at field level.
2748
- * They are collected at form level and executed via validateTree.
2749
- *
2750
- * @param config The custom validator configuration
2751
- * @param fieldPath The field path to apply validation to
2752
- * @param fieldKey Optional field key (used for logging)
2753
- */
2754
- function applyCustomValidator(config, fieldPath, fieldKey) {
2755
- // Check if this is a cross-field validator - skip at field level
2856
+ function applyCustomValidator(config, fieldPath) {
2756
2857
  if (isCrossFieldValidator(config)) {
2757
- // Cross-field validators are handled at form level via validateTree
2758
2858
  return;
2759
2859
  }
2760
- // Non-cross-field validators: apply at field level as before
2761
2860
  let validatorFn;
2762
2861
  if (config.expression) {
2763
- // Expression-based validator
2764
2862
  validatorFn = createExpressionValidator(config);
2765
2863
  }
2766
2864
  else if (config.functionName) {
2767
- // Function-based validator
2768
2865
  validatorFn = createFunctionValidator(config);
2769
2866
  }
2770
2867
  else {
2771
- console.warn('[Dynamic Forms] Custom validator must have either "expression" or "functionName"');
2868
+ const logger = inject(DynamicFormLogger);
2869
+ logger.warn('Custom validator must have either "expression" or "functionName"');
2772
2870
  return;
2773
2871
  }
2774
- // Apply with conditional logic if specified
2775
2872
  const whenLogic = createConditionalLogic(config.when);
2776
- if (whenLogic) {
2777
- const conditionalValidator = (ctx) => {
2778
- if (!whenLogic(ctx)) {
2779
- return null; // Condition not met, skip validation
2780
- }
2781
- return validatorFn(ctx);
2782
- };
2783
- validate(fieldPath, conditionalValidator);
2784
- }
2785
- else {
2786
- validate(fieldPath, validatorFn);
2787
- }
2873
+ validate(fieldPath, (ctx) => {
2874
+ if (whenLogic && !whenLogic(ctx))
2875
+ return null;
2876
+ return validatorFn(ctx);
2877
+ });
2788
2878
  }
2789
- /**
2790
- * Create a function-based validator using registered validator functions
2791
- */
2792
2879
  function createFunctionValidator(config) {
2880
+ const logger = inject(DynamicFormLogger);
2881
+ const functionName = config.functionName;
2882
+ if (!functionName) {
2883
+ logger.warn('Custom validator missing functionName');
2884
+ return () => null;
2885
+ }
2793
2886
  const registry = inject(FunctionRegistryService);
2794
- const validatorFn = registry.getValidator(config.functionName);
2887
+ const validatorFn = registry.getValidator(functionName);
2795
2888
  if (!validatorFn) {
2796
- console.warn(`[Dynamic Forms] Custom validator "${config.functionName}" not found in registry. ` +
2889
+ logger.warn(`Custom validator "${functionName}" not found in registry. ` +
2797
2890
  `Ensure it's registered using customFnConfig.validators or check the function name for typos.`);
2798
2891
  return () => null;
2799
2892
  }
2800
- return (ctx) => {
2801
- return validatorFn(ctx, config.params);
2802
- };
2893
+ return (ctx) => validatorFn(ctx, config.params);
2803
2894
  }
2804
- /**
2805
- * Create an expression-based validator using secure AST evaluation
2806
- */
2807
2895
  function createExpressionValidator(config) {
2896
+ const logger = inject(DynamicFormLogger);
2897
+ const expression = config.expression;
2898
+ if (!expression) {
2899
+ logger.warn('Custom validator missing expression');
2900
+ return () => null;
2901
+ }
2808
2902
  const fieldContextRegistry = inject(FieldContextRegistryService);
2809
2903
  const functionRegistry = inject(FunctionRegistryService);
2810
2904
  return (ctx) => {
2811
2905
  try {
2812
- // Create evaluation context from FieldContext
2813
2906
  const evaluationContext = fieldContextRegistry.createEvaluationContext(ctx, functionRegistry.getCustomFunctions());
2814
- // Evaluate expression securely using AST parser
2815
- const result = ExpressionParser.evaluate(config.expression, evaluationContext);
2816
- // If expression returns truthy, validation passes (no error)
2817
- // If expression returns falsy, validation fails
2907
+ const result = ExpressionParser.evaluate(expression, evaluationContext);
2818
2908
  if (result) {
2819
- return null; // Validation passes
2909
+ return null;
2820
2910
  }
2821
- // Validation failed - return error with kind
2822
2911
  const kind = config.kind || 'custom';
2823
2912
  const validationError = { kind };
2824
- // Evaluate and include errorParams for message interpolation
2825
2913
  if (config.errorParams) {
2826
2914
  Object.entries(config.errorParams).forEach(([key, expression]) => {
2827
2915
  try {
2828
2916
  validationError[key] = ExpressionParser.evaluate(expression, evaluationContext);
2829
2917
  }
2830
2918
  catch (err) {
2831
- console.warn(`[Dynamic Forms] Error evaluating errorParam "${key}":`, expression, err);
2919
+ logger.warn(`Error evaluating errorParam "${key}":`, expression, err);
2832
2920
  }
2833
2921
  });
2834
2922
  }
@@ -2836,8 +2924,8 @@ function createExpressionValidator(config) {
2836
2924
  }
2837
2925
  catch (error) {
2838
2926
  // Gracefully degrade on errors (e.g., typos in field names, undefined functions)
2839
- // Log to console for debugging while keeping form functional
2840
- console.error('[Dynamic Forms] Error evaluating custom validator expression:', config.expression, error);
2927
+ // Log for debugging while keeping form functional
2928
+ logger.error('Error evaluating custom validator expression:', expression, error);
2841
2929
  return { kind: config.kind || 'custom' };
2842
2930
  }
2843
2931
  };
@@ -2852,42 +2940,26 @@ function createExpressionValidator(config) {
2852
2940
  * - onError: Optional handler for resource errors
2853
2941
  */
2854
2942
  function applyAsyncValidator(config, fieldPath) {
2943
+ const logger = inject(DynamicFormLogger);
2855
2944
  const registry = inject(FunctionRegistryService);
2856
- // Get async validator config from registry
2857
2945
  const validatorConfig = registry.getAsyncValidator(config.functionName);
2858
2946
  if (!validatorConfig) {
2859
- console.warn(`[Dynamic Forms] Async validator "${config.functionName}" not found in registry. ` +
2947
+ logger.warn(`Async validator "${config.functionName}" not found in registry. ` +
2860
2948
  `Ensure it's registered using customFnConfig.asyncValidators or check the function name for typos.`);
2861
2949
  return;
2862
2950
  }
2863
- // Build Angular's AsyncValidatorOptions
2951
+ const whenLogic = createConditionalLogic(config.when);
2864
2952
  const asyncOptions = {
2865
2953
  params: (ctx) => {
2954
+ if (whenLogic && !whenLogic(ctx))
2955
+ return undefined;
2866
2956
  return validatorConfig.params(ctx, config.params);
2867
2957
  },
2868
2958
  factory: validatorConfig.factory,
2869
2959
  onSuccess: validatorConfig.onSuccess,
2870
2960
  onError: validatorConfig.onError,
2871
2961
  };
2872
- // Apply with conditional logic if specified
2873
- const whenLogic = createConditionalLogic(config.when);
2874
- if (whenLogic) {
2875
- // For conditional async validators, we wrap the params function
2876
- const conditionalOptions = {
2877
- ...asyncOptions,
2878
- params: (ctx) => {
2879
- if (!whenLogic(ctx)) {
2880
- return undefined; // Skip validation when condition not met
2881
- }
2882
- return asyncOptions.params(ctx);
2883
- },
2884
- };
2885
- // TypeScript needs help with the ResourceRef generic - the types are correct at runtime
2886
- validateAsync(fieldPath, conditionalOptions);
2887
- }
2888
- else {
2889
- validateAsync(fieldPath, asyncOptions);
2890
- }
2962
+ validateAsync(fieldPath, asyncOptions);
2891
2963
  }
2892
2964
  /**
2893
2965
  * Apply HTTP validator to field path using Angular's public validateHttp() API
@@ -2898,88 +2970,40 @@ function applyAsyncValidator(config, fieldPath) {
2898
2970
  * - onError: Optional handler for HTTP errors
2899
2971
  */
2900
2972
  function applyHttpValidator(config, fieldPath) {
2973
+ const logger = inject(DynamicFormLogger);
2901
2974
  const registry = inject(FunctionRegistryService);
2902
- // Get HTTP validator config from registry
2903
2975
  const httpValidatorConfig = registry.getHttpValidator(config.functionName);
2904
2976
  if (!httpValidatorConfig) {
2905
- console.warn(`[Dynamic Forms] HTTP validator "${config.functionName}" not found in registry. ` +
2977
+ logger.warn(`HTTP validator "${config.functionName}" not found in registry. ` +
2906
2978
  `Ensure it's registered using customFnConfig.httpValidators or check the function name for typos.`);
2907
2979
  return;
2908
2980
  }
2909
- // Build Angular's HttpValidatorOptions
2981
+ const whenLogic = createConditionalLogic(config.when);
2910
2982
  const httpOptions = {
2911
2983
  request: (ctx) => {
2984
+ if (whenLogic && !whenLogic(ctx))
2985
+ return undefined;
2912
2986
  return httpValidatorConfig.request(ctx, config.params);
2913
2987
  },
2914
2988
  onSuccess: httpValidatorConfig.onSuccess,
2915
2989
  onError: httpValidatorConfig.onError,
2916
2990
  };
2917
- // Apply with conditional logic if specified
2918
- const whenLogic = createConditionalLogic(config.when);
2919
- if (whenLogic) {
2920
- // For conditional HTTP validators, we wrap the request function
2921
- const conditionalOptions = {
2922
- ...httpOptions,
2923
- request: (ctx) => {
2924
- if (!whenLogic(ctx)) {
2925
- return undefined; // Skip validation when condition not met
2926
- }
2927
- return httpOptions.request(ctx);
2928
- },
2929
- };
2930
- // TypeScript needs help with the HttpResourceRequest generic - the types are correct at runtime
2931
- validateHttp(fieldPath, conditionalOptions);
2932
- }
2933
- else {
2934
- validateHttp(fieldPath, httpOptions);
2935
- }
2991
+ validateHttp(fieldPath, httpOptions);
2936
2992
  }
2937
- /**
2938
- * Apply multiple validators to a field path
2939
- *
2940
- * @param configs Array of validator configurations
2941
- * @param fieldPath The field path to apply validators to
2942
- * @param fieldKey Optional field key (used for logging)
2943
- */
2944
- function applyValidators(configs, fieldPath, fieldKey) {
2945
- configs.forEach((config) => applyValidator(config, fieldPath, fieldKey));
2993
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic forms require any at the Angular API boundary
2994
+ function applyValidators(configs, fieldPath) {
2995
+ configs.forEach((config) => applyValidator(config, fieldPath));
2946
2996
  }
2947
2997
 
2948
- /**
2949
- * Safely cast a SchemaPathTree to SchemaPath with Supported rules.
2950
- * See validator-factory.ts for detailed explanation of why this is safe.
2951
- */
2952
- function toSupportedPath$1(path) {
2953
- return path;
2954
- }
2955
- /**
2956
- * Apply logic configuration to field path.
2957
- * Accepts both SchemaPath and SchemaPathTree for flexibility.
2958
- *
2959
- * Cross-field logic (referencing formValue.*) is handled automatically by
2960
- * createLogicFunction which uses RootFormRegistryService to access the
2961
- * root form value. No special context or pre-computed signals needed.
2962
- *
2963
- * @param config The logic configuration
2964
- * @param fieldPath The Angular Signal Forms path
2965
- */
2966
2998
  function applyLogic(config, fieldPath) {
2967
- const path = toSupportedPath$1(fieldPath);
2968
- // Handle static boolean conditions
2999
+ const path = fieldPath;
2969
3000
  if (typeof config.condition === 'boolean') {
2970
- const staticLogicFn = () => config.condition;
2971
- applyLogicFn(config.type, path, staticLogicFn);
3001
+ applyLogicFn(config.type, path, () => config.condition);
2972
3002
  return;
2973
3003
  }
2974
- // Create logic function from condition expression
2975
- // Cross-field references (formValue.*) are handled automatically via RootFormRegistryService
2976
- const condition = config.condition;
2977
- const logicFn = createLogicFunction(condition);
3004
+ const logicFn = createLogicFunction(config.condition);
2978
3005
  applyLogicFn(config.type, path, logicFn);
2979
3006
  }
2980
- /**
2981
- * Applies a logic function to a path for a specific logic type.
2982
- */
2983
3007
  function applyLogicFn(type, path, logicFn) {
2984
3008
  switch (type) {
2985
3009
  case 'hidden':
@@ -2996,12 +3020,6 @@ function applyLogicFn(type, path, logicFn) {
2996
3020
  break;
2997
3021
  }
2998
3022
  }
2999
- /**
3000
- * Apply multiple logic configurations to a field path
3001
- *
3002
- * @param configs Array of logic configurations
3003
- * @param fieldPath The Angular Signal Forms path
3004
- */
3005
3023
  function applyMultipleLogic(configs, fieldPath) {
3006
3024
  configs.forEach((config) => applyLogic(config, fieldPath));
3007
3025
  }
@@ -3041,24 +3059,41 @@ class SchemaRegistryService {
3041
3059
  }
3042
3060
  return schema;
3043
3061
  }
3044
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: SchemaRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3045
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: SchemaRegistryService });
3062
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3063
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaRegistryService });
3046
3064
  }
3047
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: SchemaRegistryService, decorators: [{
3065
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaRegistryService, decorators: [{
3048
3066
  type: Injectable
3049
3067
  }] });
3050
3068
 
3051
3069
  /**
3052
- * Create a type predicate function from a predicate string
3070
+ * Create a type predicate function from a predicate string.
3071
+ *
3072
+ * Uses the secure ExpressionParser with whitelist-based evaluation instead of
3073
+ * new Function() / eval. This provides security through:
3074
+ * - AST-based parsing (no code execution)
3075
+ * - Whitelisted operators only
3076
+ * - Whitelisted methods only (Array.isArray, typeof, instanceof, etc.)
3077
+ * - No access to global scope beyond whitelisted constructors
3078
+ *
3079
+ * @param predicate - A JavaScript expression that evaluates to boolean. The value to check is available as `value`.
3080
+ * @param logger - Logger for error reporting
3081
+ * @returns A type predicate function
3082
+ *
3083
+ * @example
3084
+ * createTypePredicateFunction('typeof value === "string"')
3085
+ * createTypePredicateFunction('Array.isArray(value)')
3086
+ * createTypePredicateFunction('value instanceof Date')
3053
3087
  */
3054
- function createTypePredicateFunction(predicate) {
3088
+ function createTypePredicateFunction(predicate, logger) {
3055
3089
  return (value) => {
3056
3090
  try {
3057
- const func = new Function('value', `return ${predicate};`);
3058
- return func(value);
3091
+ // Evaluate using secure AST-based parser with value in scope
3092
+ const result = ExpressionParser.evaluate(predicate, { value });
3093
+ return Boolean(result);
3059
3094
  }
3060
3095
  catch (error) {
3061
- console.error('[Dynamic Forms] Error evaluating type predicate:', predicate, error);
3096
+ logger.error('Error evaluating type predicate:', predicate, error);
3062
3097
  return false;
3063
3098
  }
3064
3099
  };
@@ -3074,11 +3109,12 @@ function createTypePredicateFunction(predicate) {
3074
3109
  * 3. The actual schema application happens at runtime via the schema function
3075
3110
  */
3076
3111
  function applySchema(config, fieldPath) {
3112
+ const logger = inject(DynamicFormLogger);
3077
3113
  const schemaRegistry = inject(SchemaRegistryService);
3078
3114
  const schema = schemaRegistry.resolveSchema(config.schema);
3079
3115
  if (!schema) {
3080
3116
  const availableSchemas = Array.from(schemaRegistry.getAllSchemas().keys()).join(', ') || '<none>';
3081
- console.error(`[Dynamic Forms] Schema not found: '${config.schema}'. ` +
3117
+ logger.error(`Schema not found: '${config.schema}'. ` +
3082
3118
  `Available schemas: ${availableSchemas}. ` +
3083
3119
  `Ensure the schema is registered in your schema registry.`);
3084
3120
  return;
@@ -3098,7 +3134,7 @@ function applySchema(config, fieldPath) {
3098
3134
  break;
3099
3135
  case 'applyWhenValue':
3100
3136
  if (config.typePredicate) {
3101
- const predicate = createTypePredicateFunction(config.typePredicate);
3137
+ const predicate = createTypePredicateFunction(config.typePredicate, logger);
3102
3138
  applyWhenValue(path, predicate, schemaFn);
3103
3139
  }
3104
3140
  break;
@@ -3134,6 +3170,7 @@ function createSchemaFunction(schema) {
3134
3170
  /**
3135
3171
  * Type guard for GroupField with proper type narrowing
3136
3172
  */
3173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type
3137
3174
  function isGroupField(field) {
3138
3175
  return field.type === 'group' && 'fields' in field;
3139
3176
  }
@@ -3141,6 +3178,7 @@ function isGroupField(field) {
3141
3178
  /**
3142
3179
  * Type guard for ArrayField with proper type narrowing
3143
3180
  */
3181
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type
3144
3182
  function isArrayField(field) {
3145
3183
  return field.type === 'array' && 'fields' in field;
3146
3184
  }
@@ -3148,6 +3186,7 @@ function isArrayField(field) {
3148
3186
  /**
3149
3187
  * Type guard for RowField with proper type narrowing
3150
3188
  */
3189
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type
3151
3190
  function isRowField(field) {
3152
3191
  return field.type === 'row' && 'fields' in field && Array.isArray(field.fields);
3153
3192
  }
@@ -3193,259 +3232,208 @@ function hasNestedPages(fields) {
3193
3232
  }
3194
3233
 
3195
3234
  /**
3196
- * Safely cast a SchemaPathTree to SchemaPath with Supported rules.
3197
- * See validator-factory.ts for detailed explanation of why this is safe.
3235
+ * Applies string-specific validators to a schema path.
3236
+ * This function is properly typed - no casts needed internally.
3237
+ */
3238
+ function applyStringValidators(path, config) {
3239
+ if (config.email) {
3240
+ email(path);
3241
+ }
3242
+ if (config.minLength !== undefined) {
3243
+ minLength(path, config.minLength);
3244
+ }
3245
+ if (config.maxLength !== undefined) {
3246
+ maxLength(path, config.maxLength);
3247
+ }
3248
+ if (config.pattern) {
3249
+ const regex = typeof config.pattern === 'string' ? new RegExp(config.pattern) : config.pattern;
3250
+ pattern(path, regex);
3251
+ }
3252
+ }
3253
+ /**
3254
+ * Applies number-specific validators to a schema path.
3255
+ * This function is properly typed - no casts needed internally.
3256
+ */
3257
+ function applyNumberValidators(path, config) {
3258
+ if (config.min !== undefined) {
3259
+ min(path, config.min);
3260
+ }
3261
+ if (config.max !== undefined) {
3262
+ max(path, config.max);
3263
+ }
3264
+ }
3265
+ /**
3266
+ * Extracts string validation config from a field definition.
3267
+ * Returns undefined if no string validators are configured.
3268
+ */
3269
+ function getStringValidationConfig(fieldDef) {
3270
+ if (!fieldDef.email && fieldDef.minLength === undefined && fieldDef.maxLength === undefined && !fieldDef.pattern) {
3271
+ return undefined;
3272
+ }
3273
+ return {
3274
+ email: fieldDef.email,
3275
+ minLength: fieldDef.minLength,
3276
+ maxLength: fieldDef.maxLength,
3277
+ pattern: fieldDef.pattern,
3278
+ };
3279
+ }
3280
+ /**
3281
+ * Extracts number validation config from a field definition.
3282
+ * Handles both 'min'/'max' (standard) and 'minValue'/'maxValue' (SliderField).
3283
+ * Returns undefined if no number validators are configured.
3198
3284
  */
3199
- function toSupportedPath(path) {
3200
- return path;
3285
+ function getNumberValidationConfig(fieldDef) {
3286
+ const minVal = fieldDef.min ?? fieldDef.minValue;
3287
+ const maxVal = fieldDef.max ?? fieldDef.maxValue;
3288
+ if (minVal === undefined && maxVal === undefined) {
3289
+ return undefined;
3290
+ }
3291
+ return { min: minVal, max: maxVal };
3201
3292
  }
3202
3293
  /**
3203
- * Single entry point to map field data into form
3204
- * This is the main function that should be called from the dynamic form component
3294
+ * Maps a field definition to the Angular Signal Forms schema.
3205
3295
  *
3206
- * Cross-field logic (formValue.*) is handled automatically by createLogicFunction
3207
- * which uses RootFormRegistryService. No special context needed.
3296
+ * This is the main entry point that should be called from the dynamic form component.
3297
+ * It handles all field types: leaf fields, containers (page, row, group), and arrays.
3208
3298
  *
3209
- * Cross-field validators are skipped at field level - they are collected and
3210
- * applied at form level using validateTree.
3299
+ * Cross-field logic (formValue.*) is handled automatically by createLogicFunction
3300
+ * which uses RootFormRegistryService.
3211
3301
  *
3212
- * @param fieldDef The field definition to map
3213
- * @param fieldPath The Angular Signal Forms schema path
3302
+ * Cross-field validators are skipped at field level and applied at form level via validateTree.
3214
3303
  */
3215
3304
  function mapFieldToForm(fieldDef, fieldPath) {
3216
- // Cast to FieldWithValidation to access validation properties
3217
- const validationField = fieldDef;
3218
- // Special handling for page fields - flatten child fields to root level
3219
- if (isPageField(fieldDef)) {
3220
- mapPageFieldToForm(fieldDef, fieldPath);
3221
- return;
3222
- }
3223
- // Special handling for row fields - flatten child fields to root level (like pages)
3224
- if (isRowField(fieldDef)) {
3225
- mapRowFieldToForm(fieldDef, fieldPath);
3305
+ // Layout containers (page, row) - flatten children to current level
3306
+ if (isPageField(fieldDef) || isRowField(fieldDef)) {
3307
+ mapContainerChildren(fieldDef.fields, fieldPath);
3226
3308
  return;
3227
3309
  }
3228
- // Special handling for group fields - apply child field validations to the parent form schema
3310
+ // Group fields - map children under the group's path
3229
3311
  if (isGroupField(fieldDef)) {
3230
- mapGroupFieldToForm(fieldDef, fieldPath);
3312
+ mapContainerChildren(fieldDef.fields, fieldPath);
3231
3313
  return;
3232
3314
  }
3233
- // Special handling for array fields - apply child field validations to the parent form schema
3315
+ // Array fields - use applyEach for item schema
3234
3316
  if (isArrayField(fieldDef)) {
3235
3317
  mapArrayFieldToForm(fieldDef, fieldPath);
3236
3318
  return;
3237
3319
  }
3238
- // Apply simple validation rules from field properties (backward compatibility)
3239
- applySimpleValidationRules(validationField, fieldPath);
3240
- // Apply advanced validators if they exist
3241
- // Cross-field validators are skipped here (detected by validator-factory)
3242
- // and applied at form level via validateTree
3320
+ // Leaf field - apply validation, logic, and configuration
3321
+ mapLeafField(fieldDef, fieldPath);
3322
+ }
3323
+ /**
3324
+ * Maps children of a container field (page, row, group) to the form schema.
3325
+ */
3326
+ function mapContainerChildren(fields, parentPath) {
3327
+ if (!fields)
3328
+ return;
3329
+ const pathRecord = parentPath;
3330
+ for (const field of fields) {
3331
+ if (!field.key)
3332
+ continue;
3333
+ const childPath = pathRecord[field.key];
3334
+ if (childPath) {
3335
+ mapFieldToForm(field, childPath);
3336
+ }
3337
+ }
3338
+ }
3339
+ /**
3340
+ * Maps a leaf field (value-bearing field) to the form schema.
3341
+ */
3342
+ function mapLeafField(fieldDef, fieldPath) {
3343
+ const validationField = fieldDef;
3344
+ const path = fieldPath;
3345
+ // Apply simple validation rules from field properties
3346
+ applySimpleValidationRules(validationField, path);
3243
3347
  if (validationField.validators) {
3244
- validationField.validators.forEach((validatorConfig) => {
3245
- applyValidator(validatorConfig, fieldPath, fieldDef.key);
3246
- });
3348
+ for (const config of validationField.validators) {
3349
+ applyValidator(config, fieldPath);
3350
+ }
3247
3351
  }
3248
- // Apply logic if it exists
3249
- // Cross-field logic (formValue.*) is handled automatically by createLogicFunction
3352
+ // Apply logic rules
3250
3353
  if (validationField.logic) {
3251
- validationField.logic.forEach((logicConfig) => {
3252
- applyLogic(logicConfig, fieldPath);
3253
- });
3354
+ for (const config of validationField.logic) {
3355
+ applyLogic(config, fieldPath);
3356
+ }
3254
3357
  }
3255
- // Apply schemas if they exist
3358
+ // Apply schemas
3256
3359
  if (validationField.schemas) {
3257
- validationField.schemas.forEach((schemaConfig) => {
3258
- applySchema(schemaConfig, fieldPath);
3259
- });
3360
+ for (const config of validationField.schemas) {
3361
+ applySchema(config, fieldPath);
3362
+ }
3260
3363
  }
3261
- // Handle any additional field-specific configuration
3262
- mapFieldSpecificConfiguration(fieldDef, fieldPath);
3364
+ // Apply field state configuration
3365
+ applyFieldState(fieldDef, path);
3263
3366
  }
3264
3367
  /**
3265
- * Apply simple validation rules from field properties for backward compatibility
3266
- *
3267
- * Note: This function accepts SchemaPath<any> because FieldDef doesn't encode
3268
- * the field's TypeScript type at compile time. The validators are applied based
3269
- * on runtime FieldDef properties (email, min, max, etc.). The type assertions
3270
- * to specific types (string, number) are safe because:
3271
- * 1. Angular's validators perform runtime value checks, not compile-time type checks
3272
- * 2. The FieldDef properties indicate which validator to apply
3273
- * 3. The SchemaPath type parameter is primarily for IDE autocomplete/type inference
3368
+ * Applies simple validation rules from field properties.
3369
+ * Casts are isolated to the boundary between untyped field definitions and typed validator functions.
3274
3370
  */
3275
- function applySimpleValidationRules(fieldDef, fieldPath) {
3276
- const path = toSupportedPath(fieldPath);
3277
- // Required validator
3371
+ function applySimpleValidationRules(fieldDef, path) {
3278
3372
  if (fieldDef.required) {
3279
3373
  required(path);
3280
3374
  }
3281
- // Email validator
3282
- if (fieldDef.email) {
3283
- // Email validator expects SchemaPath<string>
3284
- email(path);
3285
- }
3286
- // Numeric validators (min/max)
3287
- // Support both 'min'/'max' (from FieldWithValidation) and 'minValue'/'maxValue' (from SliderField)
3288
- const minVal = fieldDef.min ?? fieldDef.minValue ?? fieldDef.props?.min;
3289
- const maxVal = fieldDef.max ?? fieldDef.maxValue ?? fieldDef.props?.max;
3290
- if (minVal !== undefined) {
3291
- // Min validator expects SchemaPath<number>
3292
- min(path, minVal);
3293
- }
3294
- if (maxVal !== undefined) {
3295
- // Max validator expects SchemaPath<number>
3296
- max(path, maxVal);
3297
- }
3298
- // String length validators
3299
- if (fieldDef.minLength !== undefined) {
3300
- // MinLength validator expects SchemaPath<string>
3301
- minLength(path, fieldDef.minLength);
3375
+ // String validators - single cast at boundary
3376
+ const stringConfig = getStringValidationConfig(fieldDef);
3377
+ if (stringConfig) {
3378
+ applyStringValidators(path, stringConfig);
3302
3379
  }
3303
- if (fieldDef.maxLength !== undefined) {
3304
- // MaxLength validator expects SchemaPath<string>
3305
- maxLength(path, fieldDef.maxLength);
3306
- }
3307
- // Pattern validator
3308
- if (fieldDef.pattern) {
3309
- const regexPattern = typeof fieldDef.pattern === 'string' ? new RegExp(fieldDef.pattern) : fieldDef.pattern;
3310
- // Pattern validator expects SchemaPath<string>
3311
- pattern(path, regexPattern);
3380
+ // Number validators - single cast at boundary
3381
+ const numberConfig = getNumberValidationConfig(fieldDef);
3382
+ if (numberConfig) {
3383
+ applyNumberValidators(path, numberConfig);
3312
3384
  }
3313
3385
  }
3314
3386
  /**
3315
- * Handle field-specific configuration that doesn't fit into validators/logic/schemas
3387
+ * Applies field state configuration (disabled, readonly, hidden).
3316
3388
  */
3317
- function mapFieldSpecificConfiguration(fieldDef, fieldPath) {
3318
- // Handle disabled state
3389
+ function applyFieldState(fieldDef, path) {
3319
3390
  if (fieldDef.disabled) {
3320
- disabled(toSupportedPath(fieldPath));
3391
+ disabled(path);
3321
3392
  }
3322
- // Handle readonly state
3323
3393
  if (fieldDef.readonly) {
3324
- readonly(toSupportedPath(fieldPath));
3394
+ readonly(path);
3325
3395
  }
3326
- // Handle hidden state (hidden() requires a logic function, so we provide a static true)
3327
3396
  if (fieldDef.hidden) {
3328
- hidden(toSupportedPath(fieldPath), () => true);
3329
- }
3330
- // Handle any additional configuration specific to the field type
3331
- // This can be extended as needed for specific field requirements
3332
- // Example: if field has custom form integration requirements
3333
- if ('customFormConfig' in fieldDef && fieldDef.customFormConfig) {
3334
- console.log('Custom form configuration detected for field:', fieldDef.key);
3335
- // Handle custom configuration here
3397
+ hidden(path, () => true);
3336
3398
  }
3337
3399
  }
3338
3400
  /**
3339
- * Common helper to map child fields to their parent form paths
3340
- * @param fields - Array of child field definitions
3341
- * @param parentPath - The parent field path
3342
- */
3343
- function mapChildFieldsToForm(fields, parentPath) {
3344
- for (const childField of fields) {
3345
- if (!childField.key) {
3346
- continue;
3347
- }
3348
- // Type assertion needed due to dynamic field keys
3349
- const childPath = parentPath[childField.key];
3350
- if (childPath) {
3351
- mapFieldToForm(childField, childPath);
3352
- }
3353
- }
3354
- }
3355
- /**
3356
- * Maps page field children to the root form schema
3357
- * Page fields are layout containers that don't create their own form controls
3358
- * Their children are flattened to the root level of the form
3359
- */
3360
- function mapPageFieldToForm(pageField, rootPath) {
3361
- if (!isPageField(pageField) || !pageField.fields) {
3362
- return;
3363
- }
3364
- const fields = pageField.fields;
3365
- mapChildFieldsToForm(fields, rootPath);
3366
- }
3367
- /**
3368
- * Maps row field children to the root form schema
3369
- * Row fields are layout containers (horizontal) that don't create their own form controls
3370
- * Their children are flattened to the root level of the form, similar to page fields
3371
- */
3372
- function mapRowFieldToForm(rowField, rootPath) {
3373
- if (!isRowField(rowField) || !rowField.fields) {
3374
- return;
3375
- }
3376
- const fields = rowField.fields;
3377
- mapChildFieldsToForm(fields, rootPath);
3378
- }
3379
- /**
3380
- * Maps group field children to the parent form schema
3381
- * This ensures that validation from child fields is applied to the parent form
3382
- */
3383
- function mapGroupFieldToForm(groupField, fieldPath) {
3384
- if (!isGroupField(groupField) || !groupField.fields) {
3385
- return;
3386
- }
3387
- const fields = groupField.fields;
3388
- mapChildFieldsToForm(fields, fieldPath);
3389
- }
3390
- /**
3391
- * Maps array field to the parent form schema using applyEach.
3392
- *
3393
- * Array fields are fundamentally different from groups:
3394
- * - The fields array is a TEMPLATE (single field definition), not instances
3395
- * - Array items are created/removed dynamically at runtime
3396
- * - applyEach creates proper FieldTree structure for each array item
3397
- *
3398
- * By using applyEach, Angular Signal Forms will:
3399
- * 1. Create FieldTree instances for each array item
3400
- * 2. Apply the item schema (validation, logic) to each item
3401
- * 3. Allow array items to be accessed via arrayFieldTree[index]
3401
+ * Maps an array field to the form schema using applyEach.
3402
+ *
3403
+ * Array items use a template field definition. applyEach creates proper
3404
+ * FieldTree structure for each array item.
3402
3405
  */
3403
3406
  function mapArrayFieldToForm(arrayField, fieldPath) {
3404
- if (!isArrayField(arrayField) || !arrayField.fields || arrayField.fields.length === 0) {
3407
+ if (!isArrayField(arrayField) || !arrayField.fields?.length) {
3405
3408
  return;
3406
3409
  }
3407
- // Get the template field (first field in array.fields)
3408
3410
  const templateField = arrayField.fields[0];
3409
- // Create an item schema from the template field
3410
3411
  const itemSchema = schema((itemPath) => {
3411
- // Handle container fields (row, group) that have nested fields
3412
+ const pathRecord = itemPath;
3412
3413
  if (isRowField(templateField) || isPageField(templateField)) {
3413
- // Row/page templates flatten their children - apply child validations directly to item
3414
- if (templateField.fields && Array.isArray(templateField.fields)) {
3415
- for (const childField of templateField.fields) {
3416
- if (!childField.key)
3417
- continue;
3418
- const childPath = itemPath[childField.key];
3419
- if (childPath) {
3420
- mapFieldToForm(childField, childPath);
3421
- }
3422
- }
3423
- }
3414
+ // Row/page templates flatten their children
3415
+ mapContainerChildren(templateField.fields, itemPath);
3424
3416
  }
3425
3417
  else if (isGroupField(templateField)) {
3426
- // Group template - apply child validations to the item
3427
- if (templateField.fields && Array.isArray(templateField.fields)) {
3428
- for (const childField of templateField.fields) {
3429
- if (!childField.key)
3430
- continue;
3431
- const childPath = itemPath[childField.key];
3432
- if (childPath) {
3433
- mapFieldToForm(childField, childPath);
3434
- }
3418
+ // Group template - access group's path first
3419
+ const groupKey = templateField.key;
3420
+ if (groupKey) {
3421
+ const groupPath = pathRecord[groupKey];
3422
+ if (groupPath) {
3423
+ mapContainerChildren(templateField.fields, groupPath);
3435
3424
  }
3436
3425
  }
3426
+ else {
3427
+ // No group key - apply children directly (edge case)
3428
+ mapContainerChildren(templateField.fields, itemPath);
3429
+ }
3437
3430
  }
3438
3431
  else {
3439
- // Simple field template - apply validation directly
3432
+ // Simple field template
3440
3433
  mapFieldToForm(templateField, itemPath);
3441
3434
  }
3442
3435
  });
3443
- // Apply the item schema to each array element
3444
3436
  applyEach(fieldPath, itemSchema);
3445
- // TODO: Support array-level validation (min/max length, unique items, etc.)
3446
- // This would be applied to the array field itself, not individual items:
3447
- // if (arrayField.minLength) minLength(fieldPath, arrayField.minLength);
3448
- // if (arrayField.maxLength) maxLength(fieldPath, arrayField.maxLength);
3449
3437
  }
3450
3438
 
3451
3439
  /**
@@ -3531,10 +3519,6 @@ function getFieldDefaultValue(field, registry) {
3531
3519
  }
3532
3520
  return groupDefaults;
3533
3521
  }
3534
- // Check for defaultValue first (used for reset/clear operations)
3535
- if ('defaultValue' in field && field.defaultValue !== undefined && field.defaultValue !== null) {
3536
- return field.defaultValue;
3537
- }
3538
3522
  // Use explicit value if provided, with type-specific handling for null
3539
3523
  if ('value' in field) {
3540
3524
  // If value is explicitly set (even to null/undefined), respect it
@@ -3604,6 +3588,9 @@ function isDisplayOnlyField(field) {
3604
3588
  }
3605
3589
 
3606
3590
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3591
+ function getFieldTreeByKey(ctx, key) {
3592
+ return ctx.fieldTree[key];
3593
+ }
3607
3594
  /**
3608
3595
  * Creates an Angular signal forms schema from field definitions
3609
3596
  * This is the single entry point at dynamic form level that replaces createSchemaFromFields
@@ -3622,6 +3609,7 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3622
3609
  // Inject services for cross-field validation
3623
3610
  // These will be available because createSchemaFromFields is called within runInInjectionContext
3624
3611
  const functionRegistry = inject(FunctionRegistryService);
3612
+ const logger = inject(DynamicFormLogger);
3625
3613
  return schema((path) => {
3626
3614
  for (const fieldDef of fields) {
3627
3615
  const valueHandling = getFieldValueHandling(fieldDef.type, registry);
@@ -3631,9 +3619,7 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3631
3619
  continue;
3632
3620
  }
3633
3621
  if (valueHandling === 'flatten' && hasChildFields(fieldDef)) {
3634
- // Handle both array (page/row fields) and object (group fields)
3635
- const fieldsArray = Array.isArray(fieldDef.fields) ? fieldDef.fields : Object.values(fieldDef.fields);
3636
- for (const childField of fieldsArray) {
3622
+ for (const childField of normalizeFieldsArray(fieldDef.fields)) {
3637
3623
  if (!childField.key)
3638
3624
  continue;
3639
3625
  const childPath = path[childField.key];
@@ -3655,7 +3641,7 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3655
3641
  }
3656
3642
  // Apply cross-field validators using validateTree
3657
3643
  if (crossFieldValidators && crossFieldValidators.length > 0) {
3658
- applyCrossFieldTreeValidator(path, crossFieldValidators, functionRegistry);
3644
+ applyCrossFieldTreeValidator(path, crossFieldValidators, functionRegistry, logger);
3659
3645
  }
3660
3646
  });
3661
3647
  }
@@ -3665,7 +3651,7 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3665
3651
  * This is the key integration point that routes cross-field validation errors
3666
3652
  * to the appropriate target fields via Angular's form state system.
3667
3653
  *
3668
- * The validateTree function allows returning errors with a `field` property
3654
+ * The validateTree function allows returning errors with a `fieldTree` property
3669
3655
  * that targets specific fields, which Angular automatically routes to those
3670
3656
  * fields' errors() signal.
3671
3657
  *
@@ -3676,8 +3662,9 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3676
3662
  * @param rootPath The root schema path tree
3677
3663
  * @param validators Array of collected cross-field validators
3678
3664
  * @param functionRegistry Registry containing custom functions for expression evaluation
3665
+ * @param logger Logger for error reporting
3679
3666
  */
3680
- function applyCrossFieldTreeValidator(rootPath, validators, functionRegistry) {
3667
+ function applyCrossFieldTreeValidator(rootPath, validators, functionRegistry, logger) {
3681
3668
  // Get custom functions for expression evaluation
3682
3669
  const customFunctions = functionRegistry.getCustomFunctions();
3683
3670
  validateTree(rootPath, (ctx) => {
@@ -3690,21 +3677,20 @@ function applyCrossFieldTreeValidator(rootPath, validators, functionRegistry) {
3690
3677
  for (const entry of validators) {
3691
3678
  const { sourceFieldKey, config } = entry;
3692
3679
  try {
3693
- const error = evaluateCrossFieldValidator(entry, formValue, sourceFieldKey, ctx, customFunctions);
3680
+ const error = evaluateCrossFieldValidator(entry, formValue, sourceFieldKey, ctx, customFunctions, logger);
3694
3681
  if (error) {
3695
3682
  errors.push(error);
3696
3683
  }
3697
3684
  }
3698
3685
  catch (err) {
3699
- console.error(`[Dynamic Forms] Error evaluating cross-field validator for ${sourceFieldKey}:`, err);
3686
+ logger.error(`Error evaluating cross-field validator for ${sourceFieldKey}:`, err);
3700
3687
  // On error, add a validation error to indicate the failure
3701
- // Use ctx.field[key] to access the field tree for dynamic field names
3702
- const targetField = ctx.field[sourceFieldKey];
3688
+ const targetField = getFieldTreeByKey(ctx, sourceFieldKey);
3703
3689
  if (targetField) {
3704
3690
  const customConfig = config;
3705
3691
  errors.push({
3706
3692
  kind: customConfig.kind || config.type || 'custom',
3707
- field: targetField,
3693
+ fieldTree: targetField,
3708
3694
  });
3709
3695
  }
3710
3696
  }
@@ -3715,7 +3701,7 @@ function applyCrossFieldTreeValidator(rootPath, validators, functionRegistry) {
3715
3701
  /**
3716
3702
  * Evaluates a single cross-field validator entry and returns an error if validation fails.
3717
3703
  */
3718
- function evaluateCrossFieldValidator(entry, formValue, sourceFieldKey, ctx, customFunctions) {
3704
+ function evaluateCrossFieldValidator(entry, formValue, sourceFieldKey, ctx, customFunctions, logger) {
3719
3705
  const { config } = entry;
3720
3706
  const fieldValue = formValue[sourceFieldKey];
3721
3707
  // Create evaluation context for condition/expression evaluation
@@ -3724,6 +3710,7 @@ function evaluateCrossFieldValidator(entry, formValue, sourceFieldKey, ctx, cust
3724
3710
  formValue,
3725
3711
  fieldPath: sourceFieldKey,
3726
3712
  customFunctions,
3713
+ logger,
3727
3714
  };
3728
3715
  // Check if this is a custom validator (with expression) or a built-in validator (with when condition)
3729
3716
  if (config.type === 'custom') {
@@ -3741,7 +3728,7 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3741
3728
  if (!config.expression) {
3742
3729
  return null;
3743
3730
  }
3744
- const { fieldValue, formValue } = evaluationContext;
3731
+ const { fieldValue, formValue, logger, customFunctions } = evaluationContext;
3745
3732
  // First, evaluate the when condition if present
3746
3733
  // If the condition is false, the validator doesn't apply (validation passes)
3747
3734
  if (config.when) {
@@ -3749,7 +3736,8 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3749
3736
  fieldValue,
3750
3737
  formValue,
3751
3738
  fieldPath: sourceFieldKey,
3752
- customFunctions: evaluationContext.customFunctions || {},
3739
+ customFunctions: customFunctions || {},
3740
+ logger,
3753
3741
  });
3754
3742
  if (!conditionMet) {
3755
3743
  return null; // Condition not met, skip validation
@@ -3762,14 +3750,13 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3762
3750
  return null;
3763
3751
  }
3764
3752
  // Validation failed - create error targeting the source field
3765
- // Use ctx.field[key] to access the field tree for dynamic field names
3766
- const targetField = ctx.field[sourceFieldKey];
3753
+ const targetField = getFieldTreeByKey(ctx, sourceFieldKey);
3767
3754
  if (!targetField) {
3768
3755
  return null;
3769
3756
  }
3770
3757
  const errorObj = {
3771
3758
  kind: config.kind || 'custom',
3772
- field: targetField,
3759
+ fieldTree: targetField,
3773
3760
  };
3774
3761
  // Evaluate and include errorParams for message interpolation
3775
3762
  if (config.errorParams) {
@@ -3789,7 +3776,7 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3789
3776
  * First checks the when condition, then applies the built-in validation logic.
3790
3777
  */
3791
3778
  function evaluateBuiltInCrossFieldValidator(config, evaluationContext, sourceFieldKey, ctx) {
3792
- const { fieldValue, formValue } = evaluationContext;
3779
+ const { fieldValue, formValue, logger, customFunctions } = evaluationContext;
3793
3780
  // First, evaluate the when condition
3794
3781
  // If the condition is false, the validator doesn't apply (validation passes)
3795
3782
  if (config.when) {
@@ -3797,7 +3784,8 @@ function evaluateBuiltInCrossFieldValidator(config, evaluationContext, sourceFie
3797
3784
  fieldValue,
3798
3785
  formValue,
3799
3786
  fieldPath: sourceFieldKey,
3800
- customFunctions: evaluationContext.customFunctions || {},
3787
+ customFunctions: customFunctions || {},
3788
+ logger,
3801
3789
  });
3802
3790
  if (!conditionMet) {
3803
3791
  return null; // Condition not met, skip validation
@@ -3809,14 +3797,13 @@ function evaluateBuiltInCrossFieldValidator(config, evaluationContext, sourceFie
3809
3797
  return null; // Validation passed
3810
3798
  }
3811
3799
  // Validation failed - create error targeting the source field
3812
- // Use ctx.field[key] to access the field tree for dynamic field names
3813
- const targetField = ctx.field[sourceFieldKey];
3800
+ const targetField = getFieldTreeByKey(ctx, sourceFieldKey);
3814
3801
  if (!targetField) {
3815
3802
  return null;
3816
3803
  }
3817
3804
  return {
3818
3805
  kind: config.type,
3819
- field: targetField,
3806
+ fieldTree: targetField,
3820
3807
  };
3821
3808
  }
3822
3809
  /**
@@ -3964,6 +3951,7 @@ class EventBus {
3964
3951
  * eventBus.dispatch(CustomFormEvent);
3965
3952
  * ```
3966
3953
  */
3954
+ // TypeScript limitation: Must use ConstructorParameters which relies on `any` in FormEventConstructor
3967
3955
  dispatch(eventConstructor, ...args) {
3968
3956
  this.pipeline$.next(new eventConstructor(...args));
3969
3957
  }
@@ -4005,10 +3993,10 @@ class EventBus {
4005
3993
  }
4006
3994
  return this.pipeline$.pipe(filter((event) => event.type === eventType));
4007
3995
  }
4008
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4009
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: EventBus });
3996
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3997
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: EventBus });
4010
3998
  }
4011
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: EventBus, decorators: [{
3999
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: EventBus, decorators: [{
4012
4000
  type: Injectable
4013
4001
  }] });
4014
4002
 
@@ -4052,7 +4040,7 @@ class ComponentInitializedEvent {
4052
4040
  * ```
4053
4041
  */
4054
4042
  function createInitializationTracker(eventBus, expectedCount) {
4055
- return eventBus.on('component-initialized').pipe(scan((count, _event) => count + 1, 0), map$1((currentCount) => currentCount >= expectedCount), filter$1((isComplete) => isComplete));
4043
+ return eventBus.on('component-initialized').pipe(scan$1((count) => count + 1, 0), map$1((currentCount) => currentCount >= expectedCount), filter$1((isComplete) => isComplete));
4056
4044
  }
4057
4045
  /**
4058
4046
  * Creates an observable that tracks component initialization progress with detailed status.
@@ -4077,7 +4065,7 @@ function createInitializationTracker(eventBus, expectedCount) {
4077
4065
  * ```
4078
4066
  */
4079
4067
  function createDetailedInitializationTracker(eventBus, expectedCount) {
4080
- return eventBus.on('component-initialized').pipe(scan((count, _event) => count + 1, 0), map$1((currentCount) => ({
4068
+ return eventBus.on('component-initialized').pipe(scan$1((count) => count + 1, 0), map$1((currentCount) => ({
4081
4069
  currentCount,
4082
4070
  expectedCount,
4083
4071
  isComplete: currentCount >= expectedCount,
@@ -4325,10 +4313,8 @@ function flattenFields(fields, registry, options = {}) {
4325
4313
  // Row fields need to render their container element for grid layouts to work
4326
4314
  if (options.preserveRows && isRowField(field)) {
4327
4315
  if (field.fields) {
4328
- // Normalize field children to array format (handles both array and object syntax)
4329
- const fieldsArray = Array.isArray(field.fields) ? field.fields : Object.values(field.fields);
4330
4316
  // Recursively flatten children while preserving row structure
4331
- const flattenedChildren = flattenFields(fieldsArray, registry, options);
4317
+ const flattenedChildren = flattenFields(normalizeFieldsArray(field.fields), registry, options);
4332
4318
  // Keep the row field in the result with its flattened children
4333
4319
  // This allows the row component to render its container while children are flattened
4334
4320
  result.push({
@@ -4342,8 +4328,8 @@ function flattenFields(fields, registry, options = {}) {
4342
4328
  // Step 3: Handle fields with 'flatten' value handling (typically page/row fields)
4343
4329
  // These fields are pure containers - merge their children directly into the parent level
4344
4330
  if (field.fields) {
4345
- const fieldsArray = Array.isArray(field.fields) ? field.fields : Object.values(field.fields);
4346
- const flattenedChildren = flattenFields(fieldsArray, registry, options);
4331
+ const fields = field.fields;
4332
+ const flattenedChildren = flattenFields(normalizeFieldsArray(fields), registry, options);
4347
4333
  // Spread children directly into result - the container field itself is discarded
4348
4334
  // This is used for page fields (form structure) and row fields (form values)
4349
4335
  result.push(...flattenedChildren);
@@ -4405,8 +4391,7 @@ function traverseFields(fields, collection) {
4405
4391
  collectFromField(field, collection);
4406
4392
  // Recursively process container fields (page, row, group, array)
4407
4393
  if (hasChildFields(field)) {
4408
- const childFields = Array.isArray(field.fields) ? field.fields : Object.values(field.fields);
4409
- traverseFields(childFields, collection);
4394
+ traverseFields(normalizeFieldsArray(field.fields), collection);
4410
4395
  }
4411
4396
  }
4412
4397
  }
@@ -4513,6 +4498,9 @@ function tryCreateSchemaEntry(fieldKey, config) {
4513
4498
  /** Converts a built-in validator with cross-field expression to a custom validator. */
4514
4499
  function convertBuiltInToCustomValidator(config) {
4515
4500
  const expression = config.expression;
4501
+ if (!expression) {
4502
+ throw new Error(`Built-in validator ${config.type} missing required expression for cross-field conversion`);
4503
+ }
4516
4504
  let validationExpression;
4517
4505
  switch (config.type) {
4518
4506
  case 'min':
@@ -4588,11 +4576,8 @@ class FormModeValidator {
4588
4576
  static validateFormConfigurationOrThrow(fields) {
4589
4577
  const result = this.validateFormConfiguration(fields);
4590
4578
  if (!result.isValid) {
4591
- const errorMessage = [
4592
- `[Dynamic Forms] Invalid form configuration (${result.mode} mode):`,
4593
- ...result.errors.map((error) => ` - ${error}`),
4594
- ].join('\n');
4595
- throw new Error(errorMessage);
4579
+ const errorMessage = [`Invalid form configuration (${result.mode} mode):`, ...result.errors.map((error) => ` - ${error}`)].join('\n');
4580
+ throw new DynamicFormError(errorMessage);
4596
4581
  }
4597
4582
  }
4598
4583
  /**
@@ -4812,19 +4797,22 @@ class PageNavigationStateChangeEvent {
4812
4797
  */
4813
4798
  class PageOrchestratorComponent {
4814
4799
  eventBus = inject(EventBus);
4815
- rootFormRegistry = inject(RootFormRegistryService);
4800
+ fieldContextRegistry = inject(FieldContextRegistryService);
4816
4801
  functionRegistry = inject(FunctionRegistryService);
4802
+ logger = inject(DynamicFormLogger);
4817
4803
  /**
4818
4804
  * Array of page field definitions to render
4819
4805
  */
4820
4806
  pageFields = input.required(...(ngDevMode ? [{ debugName: "pageFields" }] : []));
4821
4807
  /**
4822
- * Root form instance from parent DynamicForm
4808
+ * Root form instance from parent DynamicForm.
4809
+ * Uses FieldTree<unknown> to accept any form type.
4823
4810
  */
4824
4811
  form = input.required(...(ngDevMode ? [{ debugName: "form" }] : []));
4825
4812
  /**
4826
4813
  * Field signal context for child fields
4827
4814
  */
4815
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- FieldSignalContext is contravariant in TModel, using any allows accepting any form type
4828
4816
  fieldSignalContext = input.required(...(ngDevMode ? [{ debugName: "fieldSignalContext" }] : []));
4829
4817
  /**
4830
4818
  * Computed signal that tracks which pages are hidden.
@@ -4833,8 +4821,7 @@ class PageOrchestratorComponent {
4833
4821
  */
4834
4822
  pageHiddenStates = computed(() => {
4835
4823
  const pages = this.pageFields();
4836
- const formValue = this.rootFormRegistry.getFormValue();
4837
- return pages.map((page) => this.evaluatePageHidden(page, formValue));
4824
+ return pages.map((page) => this.evaluatePageHidden(page));
4838
4825
  }, ...(ngDevMode ? [{ debugName: "pageHiddenStates" }] : []));
4839
4826
  /**
4840
4827
  * Computed signal that returns indices of visible (non-hidden) pages.
@@ -4906,7 +4893,7 @@ class PageOrchestratorComponent {
4906
4893
  const fieldKey = fieldDef.key;
4907
4894
  if (!fieldKey)
4908
4895
  continue;
4909
- // Access the field from the form using dynamic key access
4896
+ // Access the field from the form using bracket notation
4910
4897
  const field = form[fieldKey];
4911
4898
  if (field && typeof field === 'function') {
4912
4899
  const fieldState = field();
@@ -5056,10 +5043,9 @@ class PageOrchestratorComponent {
5056
5043
  * A page is hidden if ANY of its hidden logic conditions evaluate to true.
5057
5044
  *
5058
5045
  * @param page The page field to evaluate
5059
- * @param formValue The current form value to evaluate against
5060
5046
  * @returns true if the page should be hidden, false otherwise
5061
5047
  */
5062
- evaluatePageHidden(page, formValue) {
5048
+ evaluatePageHidden(page) {
5063
5049
  // If no logic defined, page is visible
5064
5050
  if (!page.logic || page.logic.length === 0) {
5065
5051
  return false;
@@ -5072,29 +5058,24 @@ class PageOrchestratorComponent {
5072
5058
  }
5073
5059
  // Check each hidden logic - if ANY condition is true, the page is hidden
5074
5060
  for (const logic of hiddenLogic) {
5075
- // Handle static boolean conditions
5061
+ // Handle static boolean conditions (fast path)
5076
5062
  if (typeof logic.condition === 'boolean') {
5077
5063
  if (logic.condition) {
5078
5064
  return true;
5079
5065
  }
5080
5066
  continue;
5081
5067
  }
5082
- // Evaluate conditional expression
5068
+ // Evaluate conditional expression using centralized context creation
5083
5069
  const condition = logic.condition;
5084
- const context = {
5085
- fieldValue: null, // Pages don't have field values
5086
- formValue: (formValue ?? {}),
5087
- fieldPath: page.key || '',
5088
- customFunctions: this.functionRegistry.getCustomFunctions(),
5089
- };
5070
+ const context = this.fieldContextRegistry.createDisplayOnlyContext(page.key || '', this.functionRegistry.getCustomFunctions());
5090
5071
  if (evaluateCondition(condition, context)) {
5091
5072
  return true;
5092
5073
  }
5093
5074
  }
5094
5075
  return false;
5095
5076
  }
5096
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: PageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5097
- 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: [
5077
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5078
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", 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: [
5098
5079
  {
5099
5080
  provide: FIELD_SIGNAL_CONTEXT,
5100
5081
  useFactory: (orchestrator) => orchestrator.extendedFieldSignalContext(),
@@ -5124,9 +5105,9 @@ class PageOrchestratorComponent {
5124
5105
  }
5125
5106
  }
5126
5107
  }
5127
- `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-ZVrJUMNb.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-ZVrJUMNb.mjs').then(m => m.default)]] });
5108
+ `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-BM32StxL.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-BM32StxL.mjs').then(m => m.default)]] });
5128
5109
  }
5129
- i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.0.5", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-ZVrJUMNb.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
5110
+ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.0.6", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-BM32StxL.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
5130
5111
  type: Component,
5131
5112
  args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
5132
5113
  @for (pageField of pageFields(); track pageField.key; let i = $index) {
@@ -5195,6 +5176,7 @@ class DynamicForm {
5195
5176
  rootFormRegistry = inject(RootFormRegistryService);
5196
5177
  functionRegistry = inject(FunctionRegistryService);
5197
5178
  schemaRegistry = inject(SchemaRegistryService);
5179
+ logger = inject(DynamicFormLogger);
5198
5180
  // ─────────────────────────────────────────────────────────────────────────────
5199
5181
  // Inputs
5200
5182
  // ─────────────────────────────────────────────────────────────────────────────
@@ -5207,17 +5189,26 @@ class DynamicForm {
5207
5189
  // ─────────────────────────────────────────────────────────────────────────────
5208
5190
  // Memoized Functions
5209
5191
  // ─────────────────────────────────────────────────────────────────────────────
5210
- memoizedFlattenFields = memoize((fields, registry) => flattenFields(fields, registry), (fields, registry) => {
5211
- const fieldKeys = fields.map((f) => `${f.key || ''}:${f.type}`).join('|');
5212
- const registryKeys = Array.from(registry.keys()).sort().join('|');
5213
- return `${fieldKeys}__${registryKeys}`;
5192
+ memoizedFlattenFields = memoize((fields, registry) => flattenFields(fields, registry), {
5193
+ resolver: (fields, registry) => {
5194
+ const fieldKeys = fields.map((f) => `${f.key || ''}:${f.type}`).join('|');
5195
+ const registryKeys = Array.from(registry.keys()).sort().join('|');
5196
+ return `${fieldKeys}__${registryKeys}`;
5197
+ },
5198
+ maxSize: 10,
5214
5199
  });
5215
- memoizedFlattenFieldsForRendering = memoize((fields, registry) => flattenFields(fields, registry, { preserveRows: true }), (fields, registry) => {
5216
- const fieldKeys = fields.map((f) => `${f.key || ''}:${f.type}`).join('|');
5217
- const registryKeys = Array.from(registry.keys()).sort().join('|');
5218
- return `render_${fieldKeys}__${registryKeys}`;
5200
+ memoizedFlattenFieldsForRendering = memoize((fields, registry) => flattenFields(fields, registry, { preserveRows: true }), {
5201
+ resolver: (fields, registry) => {
5202
+ const fieldKeys = fields.map((f) => `${f.key || ''}:${f.type}`).join('|');
5203
+ const registryKeys = Array.from(registry.keys()).sort().join('|');
5204
+ return `render_${fieldKeys}__${registryKeys}`;
5205
+ },
5206
+ maxSize: 10,
5207
+ });
5208
+ memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), {
5209
+ resolver: (fields) => fields.map((f) => f.key).join('|'),
5210
+ maxSize: 10,
5219
5211
  });
5220
- memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), (fields) => fields.map((f) => f.key).join('|'));
5221
5212
  memoizedDefaultValues = memoize((fieldsById, registry) => {
5222
5213
  const result = {};
5223
5214
  for (const [key, field] of Object.entries(fieldsById)) {
@@ -5227,10 +5218,13 @@ class DynamicForm {
5227
5218
  }
5228
5219
  }
5229
5220
  return result;
5230
- }, (fieldsById, registry) => {
5231
- const fieldKeys = Object.keys(fieldsById).sort().join('|');
5232
- const registryKeys = Array.from(registry.keys()).sort().join('|');
5233
- return `defaults_${fieldKeys}__${registryKeys}`;
5221
+ }, {
5222
+ resolver: (fieldsById, registry) => {
5223
+ const fieldKeys = Object.keys(fieldsById).sort().join('|');
5224
+ const registryKeys = Array.from(registry.keys()).sort().join('|');
5225
+ return `defaults_${fieldKeys}__${registryKeys}`;
5226
+ },
5227
+ maxSize: 10,
5234
5228
  });
5235
5229
  // ─────────────────────────────────────────────────────────────────────────────
5236
5230
  // Computed Signals
@@ -5242,10 +5236,10 @@ class DynamicForm {
5242
5236
  const detection = detectFormMode(fields);
5243
5237
  const validation = FormModeValidator.validateFormConfiguration(fields);
5244
5238
  if (!validation.isValid) {
5245
- console.error('[Dynamic Forms] Invalid form configuration:', validation.errors);
5239
+ this.logger.error('Invalid form configuration:', validation.errors);
5246
5240
  }
5247
5241
  if (validation.warnings.length > 0) {
5248
- console.warn('[Dynamic Forms] Form configuration warnings:', validation.warnings);
5242
+ this.logger.warn('Form configuration warnings:', validation.warnings);
5249
5243
  }
5250
5244
  return detection;
5251
5245
  }, ...(ngDevMode ? [{ debugName: "formModeDetection" }] : []));
@@ -5385,7 +5379,7 @@ class DynamicForm {
5385
5379
  submitted = outputFromObservable(this.eventBus.on('submit').pipe(switchMap(() => {
5386
5380
  const submissionConfig = this.config().submission;
5387
5381
  if (submissionConfig?.action) {
5388
- console.warn('[Dynamic Forms] Both `submission.action` and `(submitted)` output are configured. ' +
5382
+ this.logger.warn('Both `submission.action` and `(submitted)` output are configured. ' +
5389
5383
  'When using `submission.action`, the `(submitted)` output will not emit. ' +
5390
5384
  'Use either `submission.action` OR `(submitted)`, not both.');
5391
5385
  return EMPTY;
@@ -5429,12 +5423,12 @@ class DynamicForm {
5429
5423
  error: error instanceof Error ? error : new Error(String(error)),
5430
5424
  },
5431
5425
  ]);
5432
- console.error(`[Dynamic Forms] Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}). ` +
5426
+ this.logger.error(`Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}). ` +
5433
5427
  `Ensure the field type is registered in your field registry.`, error);
5434
5428
  },
5435
5429
  };
5436
5430
  return forkJoin(fields.map((f) => resolveField(f, context)));
5437
- }), map((fields) => fields.filter((f) => f !== undefined)), scan$1(reconcileFields, [])), { initialValue: [], injector: this.injector });
5431
+ }), map((fields) => fields.filter((f) => f !== undefined)), scan(reconcileFields, [])), { initialValue: [], injector: this.injector });
5438
5432
  // ─────────────────────────────────────────────────────────────────────────────
5439
5433
  // Constructor
5440
5434
  // ─────────────────────────────────────────────────────────────────────────────
@@ -5488,17 +5482,14 @@ class DynamicForm {
5488
5482
  });
5489
5483
  }
5490
5484
  setupEventHandlers() {
5491
- // Handle form reset events
5492
5485
  this.eventBus
5493
5486
  .on('form-reset')
5494
5487
  .pipe(takeUntilDestroyed())
5495
5488
  .subscribe(() => this.onFormReset());
5496
- // Handle form clear events
5497
5489
  this.eventBus
5498
5490
  .on('form-clear')
5499
5491
  .pipe(takeUntilDestroyed())
5500
5492
  .subscribe(() => this.onFormClear());
5501
- // Handle form submission
5502
5493
  createSubmissionHandler({
5503
5494
  eventBus: this.eventBus,
5504
5495
  configSignal: this.config,
@@ -5553,13 +5544,13 @@ class DynamicForm {
5553
5544
  }
5554
5545
  onFormReset() {
5555
5546
  const defaults = this.defaultValues();
5556
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5547
+ // Type assertion is necessary: Angular Signal Forms expects exact form value type
5557
5548
  this.form()().value.set(defaults);
5558
5549
  this.value.set(defaults);
5559
5550
  }
5560
5551
  onFormClear() {
5561
5552
  const emptyValue = {};
5562
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5553
+ // Type assertion is necessary: Angular Signal Forms expects exact form value type
5563
5554
  this.form()().value.set(emptyValue);
5564
5555
  this.value.set(emptyValue);
5565
5556
  }
@@ -5569,10 +5560,10 @@ class DynamicForm {
5569
5560
  ngOnDestroy() {
5570
5561
  this.rootFormRegistry.unregisterForm();
5571
5562
  }
5572
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
5573
- 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: `
5563
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
5564
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", 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: `
5574
5565
  @if (formModeDetection().mode === 'paged') {
5575
- <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="$any(form())" [fieldSignalContext]="fieldSignalContext()"></div>
5566
+ <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
5576
5567
  } @else {
5577
5568
  @for (field of resolvedFields(); track field.key) {
5578
5569
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
@@ -5580,11 +5571,11 @@ class DynamicForm {
5580
5571
  }
5581
5572
  `, 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 });
5582
5573
  }
5583
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: DynamicForm, decorators: [{
5574
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DynamicForm, decorators: [{
5584
5575
  type: Component,
5585
5576
  args: [{ selector: 'form[dynamic-form]', imports: [NgComponentOutlet, PageOrchestratorComponent], template: `
5586
5577
  @if (formModeDetection().mode === 'paged') {
5587
- <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="$any(form())" [fieldSignalContext]="fieldSignalContext()"></div>
5578
+ <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
5588
5579
  } @else {
5589
5580
  @for (field of resolvedFields(); track field.key) {
5590
5581
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
@@ -5681,7 +5672,7 @@ function pageFieldMapper(fieldDef) {
5681
5672
  * @returns Signal containing Record of input names to values for ngComponentOutlet
5682
5673
  */
5683
5674
  function textFieldMapper(fieldDef) {
5684
- const rootFormRegistry = inject(RootFormRegistryService);
5675
+ const fieldContextRegistry = inject(FieldContextRegistryService);
5685
5676
  const functionRegistry = inject(FunctionRegistryService);
5686
5677
  // Build base inputs (static, from field definition)
5687
5678
  const baseInputs = buildBaseInputs(fieldDef);
@@ -5692,16 +5683,8 @@ function textFieldMapper(fieldDef) {
5692
5683
  const inputs = { ...baseInputs };
5693
5684
  // Evaluate hidden logic if present
5694
5685
  if (hiddenLogic.length > 0) {
5695
- const formValue = rootFormRegistry.getFormValue();
5696
- const customFunctions = functionRegistry.getCustomFunctions();
5697
- // Create evaluation context without a specific field value
5698
- // (text fields don't have their own value in the form)
5699
- const evaluationContext = {
5700
- fieldValue: undefined,
5701
- formValue,
5702
- fieldPath: fieldDef.key,
5703
- customFunctions,
5704
- };
5686
+ // Use centralized context creation for display-only components
5687
+ const evaluationContext = fieldContextRegistry.createDisplayOnlyContext(fieldDef.key, functionRegistry.getCustomFunctions());
5705
5688
  // Evaluate all hidden logic conditions - if ANY is true, the field is hidden
5706
5689
  inputs['hidden'] = hiddenLogic.some((logic) => {
5707
5690
  if (typeof logic.condition === 'boolean') {
@@ -5745,6 +5728,13 @@ function textFieldMapper(fieldDef) {
5745
5728
  * { type: 'page', key: 'step1', fields: [...] }
5746
5729
  * ```
5747
5730
  */
5731
+ /**
5732
+ * Built-in field types provided by the dynamic form library.
5733
+ *
5734
+ * Each field type is validated at compile time using satisfies, ensuring
5735
+ * type safety of the mapper function while allowing the array to be typed
5736
+ * as FieldTypeDefinition[] for consumer flexibility.
5737
+ */
5748
5738
  const BUILT_IN_FIELDS = [
5749
5739
  {
5750
5740
  name: 'row',
@@ -5766,25 +5756,65 @@ const BUILT_IN_FIELDS = [
5766
5756
  },
5767
5757
  {
5768
5758
  name: 'page',
5769
- loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-ZVrJUMNb.mjs'),
5759
+ loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-BM32StxL.mjs'),
5770
5760
  mapper: pageFieldMapper,
5771
5761
  valueHandling: 'flatten',
5772
5762
  },
5773
5763
  {
5774
5764
  name: 'text',
5775
- loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-BKTbxAKn.mjs'),
5765
+ loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-C0I8jf9T.mjs'),
5776
5766
  mapper: textFieldMapper,
5777
5767
  valueHandling: 'exclude',
5778
5768
  },
5779
5769
  ];
5780
5770
 
5771
+ /**
5772
+ * Type guard to check if a value is a DynamicFormFeature
5773
+ */
5774
+ function isDynamicFormFeature(value) {
5775
+ return (typeof value === 'object' &&
5776
+ value !== null &&
5777
+ 'ɵkind' in value &&
5778
+ typeof value.ɵkind === 'string' &&
5779
+ 'ɵproviders' in value &&
5780
+ Array.isArray(value.ɵproviders));
5781
+ }
5782
+ /**
5783
+ * Helper to create a feature with proper typing
5784
+ */
5785
+ function createFeature(kind, providers) {
5786
+ return {
5787
+ ɵkind: kind,
5788
+ ɵproviders: providers,
5789
+ };
5790
+ }
5791
+
5792
+ /**
5793
+ * Console-based logger implementation.
5794
+ */
5795
+ class ConsoleLogger {
5796
+ prefix = '[Dynamic Forms]';
5797
+ debug(message, ...args) {
5798
+ console.debug(this.prefix, message, ...args);
5799
+ }
5800
+ info(message, ...args) {
5801
+ console.info(this.prefix, message, ...args);
5802
+ }
5803
+ warn(message, ...args) {
5804
+ console.warn(this.prefix, message, ...args);
5805
+ }
5806
+ error(message, ...args) {
5807
+ console.error(this.prefix, message, ...args);
5808
+ }
5809
+ }
5810
+
5781
5811
  /**
5782
5812
  * Provider function to configure the dynamic form system with field types and options.
5783
5813
  *
5784
5814
  * This function creates environment providers that can be used at application or route level
5785
5815
  * to register field types. It provides type-safe field registration with automatic type inference.
5786
5816
  *
5787
- * @param fieldTypes - Custom field type definitions to register alongside built-in types
5817
+ * @param items - Field type definitions and/or features (like withLoggerConfig)
5788
5818
  * @returns Environment providers for dependency injection with type inference
5789
5819
  *
5790
5820
  * @example
@@ -5799,14 +5829,15 @@ const BUILT_IN_FIELDS = [
5799
5829
  *
5800
5830
  * @example
5801
5831
  * ```typescript
5802
- * // Route-level setup
5803
- * const routes: Routes = [
5804
- * {
5805
- * path: 'form',
5806
- * component: FormComponent,
5807
- * providers: [provideDynamicForm(...withMaterialFields())]
5808
- * }
5809
- * ];
5832
+ * // Disable logging
5833
+ * bootstrapApplication(AppComponent, {
5834
+ * providers: [
5835
+ * provideDynamicForm(
5836
+ * ...withMaterialFields(),
5837
+ * withLoggerConfig(false)
5838
+ * )
5839
+ * ]
5840
+ * });
5810
5841
  * ```
5811
5842
  *
5812
5843
  * @example
@@ -5825,7 +5856,10 @@ const BUILT_IN_FIELDS = [
5825
5856
  *
5826
5857
  * @public
5827
5858
  */
5828
- function provideDynamicForm(...fieldTypes) {
5859
+ function provideDynamicForm(...items) {
5860
+ // Separate field types from features
5861
+ const fieldTypes = items.filter((item) => !isDynamicFormFeature(item));
5862
+ const features = items.filter(isDynamicFormFeature);
5829
5863
  const fields = [...BUILT_IN_FIELDS, ...fieldTypes];
5830
5864
  // Extract config providers from field type arrays
5831
5865
  const configProviders = [];
@@ -5835,15 +5869,25 @@ function provideDynamicForm(...fieldTypes) {
5835
5869
  configProviders.push(...fieldTypeWithConfig.__configProviders);
5836
5870
  }
5837
5871
  });
5872
+ // Extract providers from features
5873
+ const featureProviders = [];
5874
+ const hasLoggerFeature = features.some((feature) => feature.ɵkind === 'logger');
5875
+ features.forEach((feature) => {
5876
+ featureProviders.push(...feature.ɵproviders);
5877
+ });
5878
+ // Default logger provider (ConsoleLogger) if no logger feature was provided
5879
+ const defaultLoggerProvider = hasLoggerFeature ? [] : [{ provide: DynamicFormLogger, useValue: new ConsoleLogger() }];
5838
5880
  return makeEnvironmentProviders([
5881
+ ...defaultLoggerProvider,
5839
5882
  {
5840
5883
  provide: FIELD_REGISTRY,
5841
5884
  useFactory: () => {
5885
+ const logger = inject(DynamicFormLogger);
5842
5886
  const registry = new Map();
5843
5887
  // Add custom field types
5844
5888
  fields.forEach((fieldType) => {
5845
5889
  if (registry.has(fieldType.name)) {
5846
- console.warn(`[Dynamic Forms] Field type "${fieldType.name}" is already registered. Overwriting.`);
5890
+ logger.warn(`Field type "${fieldType.name}" is already registered. Overwriting.`);
5847
5891
  }
5848
5892
  registry.set(fieldType.name, fieldType);
5849
5893
  });
@@ -5851,9 +5895,73 @@ function provideDynamicForm(...fieldTypes) {
5851
5895
  },
5852
5896
  },
5853
5897
  ...configProviders,
5898
+ ...featureProviders,
5854
5899
  ]);
5855
5900
  }
5856
5901
 
5902
+ /**
5903
+ * No-operation logger implementation.
5904
+ * All methods are no-ops - used in production by default.
5905
+ */
5906
+ class NoopLogger {
5907
+ debug() {
5908
+ // Intentionally empty
5909
+ }
5910
+ info() {
5911
+ // Intentionally empty
5912
+ }
5913
+ warn() {
5914
+ // Intentionally empty
5915
+ }
5916
+ error() {
5917
+ // Intentionally empty
5918
+ }
5919
+ }
5920
+
5921
+ /**
5922
+ * Configure logging for dynamic forms.
5923
+ *
5924
+ * By default, logging is enabled (ConsoleLogger). Use this feature to disable logging
5925
+ * or configure it conditionally.
5926
+ *
5927
+ * @example
5928
+ * ```typescript
5929
+ * // Disable logging
5930
+ * provideDynamicForm(
5931
+ * ...withMaterialFields(),
5932
+ * withLoggerConfig(false)
5933
+ * )
5934
+ *
5935
+ * // Conditional logging (e.g., only in dev mode)
5936
+ * provideDynamicForm(
5937
+ * ...withMaterialFields(),
5938
+ * withLoggerConfig(() => isDevMode())
5939
+ * )
5940
+ * ```
5941
+ *
5942
+ * @param enabled - Whether logging is enabled. Can be a boolean or a function that returns a boolean. Defaults to true.
5943
+ * @returns A DynamicFormFeature that configures logging
5944
+ */
5945
+ function withLoggerConfig(enabled = true) {
5946
+ const isEnabled = typeof enabled === 'function' ? enabled() : enabled;
5947
+ const logger = isEnabled ? new ConsoleLogger() : new NoopLogger();
5948
+ return createFeature('logger', [{ provide: DynamicFormLogger, useValue: logger }]);
5949
+ }
5950
+
5951
+ /**
5952
+ * No-op logger for when no logger is provided.
5953
+ * Used as fallback in button logic evaluation to avoid breaking downstream packages.
5954
+ */
5955
+ const noOpLogger = {
5956
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
5957
+ debug: () => { },
5958
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
5959
+ info: () => { },
5960
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
5961
+ warn: () => { },
5962
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
5963
+ error: () => { },
5964
+ };
5857
5965
  /**
5858
5966
  * Default options for submit button disabled behavior.
5859
5967
  */
@@ -5910,6 +6018,7 @@ function evaluateLogicCondition(condition, ctx) {
5910
6018
  fieldValue: undefined,
5911
6019
  formValue,
5912
6020
  fieldPath: '',
6021
+ logger: ctx.logger ?? noOpLogger,
5913
6022
  };
5914
6023
  return evaluateCondition(condition, evaluationContext);
5915
6024
  }
@@ -6094,244 +6203,6 @@ function resolveTokens(args, context) {
6094
6203
  });
6095
6204
  }
6096
6205
 
6097
- /**
6098
- * Type guard to check if a value has the FormInternals structure.
6099
- */
6100
- function isFormInternals(value) {
6101
- return (typeof value === 'object' &&
6102
- value !== null &&
6103
- 'structure' in value &&
6104
- typeof value.structure?.childrenMap === 'function');
6105
- }
6106
- /**
6107
- * Type guard that unwraps signals and checks for FormInternals structure.
6108
- * Use this when the input might be a signal wrapping FormInternals.
6109
- */
6110
- function hasFormStructure(form) {
6111
- const unwrapped = isSignal(form) ? form() : form;
6112
- return isFormInternals(unwrapped);
6113
- }
6114
- /**
6115
- * Unwraps a signal if needed and returns the FormInternals or null.
6116
- */
6117
- function unwrapFormInput(form) {
6118
- const unwrapped = isSignal(form) ? form() : form;
6119
- return isFormInternals(unwrapped) ? unwrapped : null;
6120
- }
6121
- function getChildrenMap(form) {
6122
- const target = unwrapFormInput(form);
6123
- if (!target) {
6124
- return null;
6125
- }
6126
- const result = target.structure.childrenMap();
6127
- const normalizedMap = new Map();
6128
- for (const [key, childData] of result.byPropertyKey) {
6129
- normalizedMap.set(key, childData.reader);
6130
- }
6131
- return normalizedMap;
6132
- }
6133
- function getFieldProxy(node) {
6134
- if (!isFormInternals(node)) {
6135
- return null;
6136
- }
6137
- return node.fieldProxy ?? null;
6138
- }
6139
- function getChildFieldTree(form, key) {
6140
- const childrenMap = getChildrenMap(form);
6141
- return childrenMap?.get(key) ?? null;
6142
- }
6143
-
6144
- /**
6145
- * Resolves the field proxy and default validation messages from the form context.
6146
- * Must be called within an injection context.
6147
- *
6148
- * @param fieldKey The key of the field to resolve
6149
- * @returns The resolved context with field proxy and validation messages
6150
- */
6151
- function resolveValueFieldContext(fieldKey) {
6152
- const context = inject(FIELD_SIGNAL_CONTEXT);
6153
- // Get form-level validation messages
6154
- const defaultValidationMessages = context.defaultValidationMessages;
6155
- // Get the form root to access field proxy
6156
- const formRoot = context.form();
6157
- const childrenMap = getChildrenMap(formRoot);
6158
- // Resolve field proxy
6159
- let fieldProxy = undefined;
6160
- if (!childrenMap) {
6161
- // No childrenMap - the form might be a FormRecord where fields are direct properties
6162
- // First check if formRoot itself has fieldProxy (for FieldTree items)
6163
- const rootFieldProxy = getFieldProxy(formRoot);
6164
- if (rootFieldProxy) {
6165
- fieldProxy = rootFieldProxy;
6166
- }
6167
- else {
6168
- // Try accessing the field as a direct property on formRoot (FormRecord case)
6169
- // This handles forms created with form(entity, schema) where fields are accessible as formRoot[key]
6170
- const formField = formRoot[fieldKey];
6171
- const resolvedProxy = getFieldProxy(formField);
6172
- if (resolvedProxy) {
6173
- fieldProxy = resolvedProxy;
6174
- }
6175
- else if (isSignal(formField) || formField) {
6176
- // The field itself might be the proxy (signal), or might have a different structure
6177
- // Try using the field directly as the proxy
6178
- fieldProxy = formField;
6179
- }
6180
- }
6181
- }
6182
- else {
6183
- // Standard field access for non-array keys via childrenMap lookup
6184
- const formField = childrenMap.get(fieldKey);
6185
- if (formField) {
6186
- fieldProxy = formField;
6187
- }
6188
- }
6189
- return { fieldProxy, defaultValidationMessages };
6190
- }
6191
- /**
6192
- * Builds the base inputs for a value field.
6193
- * This is a helper function used by valueFieldMapper and specialized mappers.
6194
- *
6195
- * @param fieldDef The value field definition
6196
- * @param ctx The resolved value field context
6197
- * @returns Record of input names to values
6198
- */
6199
- function buildValueFieldInputs(fieldDef, ctx) {
6200
- const omittedFields = omit(fieldDef, ['value']);
6201
- const baseInputs = buildBaseInputs(omittedFields);
6202
- const inputs = {
6203
- ...baseInputs,
6204
- // Always pass validationMessages (or empty object) - required for error display signals
6205
- validationMessages: fieldDef.validationMessages ?? {},
6206
- };
6207
- // Value field specific properties
6208
- if (fieldDef.placeholder !== undefined) {
6209
- inputs['placeholder'] = fieldDef.placeholder;
6210
- }
6211
- if (ctx.defaultValidationMessages !== undefined) {
6212
- inputs['defaultValidationMessages'] = ctx.defaultValidationMessages;
6213
- }
6214
- if (ctx.fieldProxy !== undefined) {
6215
- inputs['field'] = ctx.fieldProxy;
6216
- }
6217
- return inputs;
6218
- }
6219
- /**
6220
- * Maps a value field definition to component inputs.
6221
- *
6222
- * Value fields are input fields that contribute to the form's value (text, number, etc.).
6223
- * This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field proxy.
6224
- *
6225
- * For fields with specific properties (select, datepicker, textarea, slider), use the specialized mappers:
6226
- * - selectFieldMapper: for fields with options (select, radio, multi-checkbox)
6227
- * - datepickerFieldMapper: for fields with minDate, maxDate, startAt
6228
- * - textareaFieldMapper: for fields with rows, cols
6229
- * - sliderFieldMapper: for fields with minValue, maxValue, step
6230
- *
6231
- * @param fieldDef The value field definition
6232
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6233
- */
6234
- function valueFieldMapper(fieldDef) {
6235
- const ctx = resolveValueFieldContext(fieldDef.key);
6236
- return computed(() => buildValueFieldInputs(fieldDef, ctx));
6237
- }
6238
-
6239
- /**
6240
- * Maps a checkbox/toggle field definition to component inputs.
6241
- *
6242
- * Checkbox fields are checked fields that contribute to the form's value as boolean.
6243
- * This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field proxy.
6244
- *
6245
- * @param fieldDef The checkbox field definition
6246
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6247
- */
6248
- function checkboxFieldMapper(fieldDef) {
6249
- const context = inject(FIELD_SIGNAL_CONTEXT);
6250
- const omittedFields = omit(fieldDef, ['value']);
6251
- // Build base inputs (static, from field definition)
6252
- const baseInputs = buildBaseInputs(omittedFields);
6253
- // Get form-level validation messages (static)
6254
- const defaultValidationMessages = context.defaultValidationMessages;
6255
- // Get the form root to access field proxy
6256
- const formRoot = context.form();
6257
- const childrenMap = getChildrenMap(formRoot);
6258
- // Resolve field proxy (static - determined once during mapping)
6259
- let fieldProxy = undefined;
6260
- if (childrenMap) {
6261
- const formField = childrenMap.get(fieldDef.key);
6262
- if (formField) {
6263
- fieldProxy = formField;
6264
- }
6265
- }
6266
- // Return computed signal for reactive updates
6267
- return computed(() => {
6268
- const inputs = {
6269
- ...baseInputs,
6270
- // Always pass validationMessages (or empty object) - required for error display signals
6271
- validationMessages: fieldDef.validationMessages ?? {},
6272
- };
6273
- // Checked field specific property: placeholder
6274
- if (fieldDef.placeholder !== undefined) {
6275
- inputs['placeholder'] = fieldDef.placeholder;
6276
- }
6277
- if (defaultValidationMessages !== undefined) {
6278
- inputs['defaultValidationMessages'] = defaultValidationMessages;
6279
- }
6280
- if (fieldProxy !== undefined) {
6281
- inputs['field'] = fieldProxy;
6282
- }
6283
- return inputs;
6284
- });
6285
- }
6286
-
6287
- /**
6288
- * Maps a field with options (select, radio, multi-checkbox) to component inputs.
6289
- *
6290
- * Extends the base value field mapper by adding the options property.
6291
- *
6292
- * @param fieldDef The field definition with options
6293
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6294
- */
6295
- function optionsFieldMapper(fieldDef) {
6296
- const ctx = resolveValueFieldContext(fieldDef.key);
6297
- return computed(() => {
6298
- // Cast to BaseValueField - safe because all FieldWithOptions types extend BaseValueField
6299
- const inputs = buildValueFieldInputs(fieldDef, ctx);
6300
- // Add options property
6301
- inputs['options'] = fieldDef.options;
6302
- return inputs;
6303
- });
6304
- }
6305
-
6306
- /**
6307
- * Maps a datepicker field to component inputs.
6308
- *
6309
- * Extends the base value field mapper by adding datepicker-specific properties:
6310
- * - minDate: Minimum selectable date
6311
- * - maxDate: Maximum selectable date
6312
- * - startAt: Initial date to display when opening the picker
6313
- *
6314
- * @param fieldDef The datepicker field definition
6315
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6316
- */
6317
- function datepickerFieldMapper(fieldDef) {
6318
- const ctx = resolveValueFieldContext(fieldDef.key);
6319
- return computed(() => {
6320
- const inputs = buildValueFieldInputs(fieldDef, ctx);
6321
- // Add datepicker-specific properties
6322
- if (fieldDef.minDate !== undefined) {
6323
- inputs['minDate'] = fieldDef.minDate;
6324
- }
6325
- if (fieldDef.maxDate !== undefined) {
6326
- inputs['maxDate'] = fieldDef.maxDate;
6327
- }
6328
- if (fieldDef.startAt !== undefined) {
6329
- inputs['startAt'] = fieldDef.startAt;
6330
- }
6331
- return inputs;
6332
- });
6333
- }
6334
-
6335
6206
  /**
6336
6207
  * Converts DynamicText (string | Observable | Signal) to Observable<string>
6337
6208
  * Unifies all three types into a consistent Observable stream
@@ -6353,187 +6224,6 @@ function dynamicTextToObservable(value, injector) {
6353
6224
  return of(String(value));
6354
6225
  }
6355
6226
 
6356
- /**
6357
- * Computed signal that determines if errors should be displayed
6358
- * Based on field's invalid, touched, and error count
6359
- *
6360
- * @param field - Signal containing FieldTree
6361
- * @returns Signal<boolean> - True if errors should be displayed
6362
- */
6363
- function shouldShowErrors(field) {
6364
- return computed(() => {
6365
- const fieldTree = field();
6366
- const control = fieldTree();
6367
- return control.invalid() && control.touched() && control.errors().length > 0;
6368
- });
6369
- }
6370
-
6371
- /**
6372
- * Interpolates {{param}} placeholders in message with error values
6373
- *
6374
- * @param message - Message template with {{param}} placeholders
6375
- * @param error - Validation error containing parameter values
6376
- * @returns Message with interpolated parameters
6377
- *
6378
- * @example
6379
- * interpolateParams("Min value is {{min}}", { kind: 'min', min: 5 })
6380
- * // Returns: "Min value is 5"
6381
- */
6382
- function interpolateParams(message, error) {
6383
- let result = message;
6384
- const params = extractErrorParams(error);
6385
- Object.entries(params).forEach(([key, value]) => {
6386
- const placeholder = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
6387
- result = result.replace(placeholder, safeToString(value));
6388
- });
6389
- return result;
6390
- }
6391
- /**
6392
- * Safely converts a value to a string, handling complex objects
6393
- * @internal
6394
- */
6395
- function safeToString(value) {
6396
- if (value === null || value === undefined) {
6397
- return '';
6398
- }
6399
- // Handle primitives directly
6400
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
6401
- return String(value);
6402
- }
6403
- // Handle RegExp (has its own toString)
6404
- if (value instanceof RegExp) {
6405
- return value.toString();
6406
- }
6407
- // Handle Date
6408
- if (value instanceof Date) {
6409
- return value.toISOString();
6410
- }
6411
- // Handle arrays
6412
- if (Array.isArray(value)) {
6413
- return value.join(', ');
6414
- }
6415
- // For objects, try JSON.stringify first, fall back to empty string
6416
- try {
6417
- // Check if object has a custom toString that's not Object.prototype.toString
6418
- if (typeof value === 'object' && value.toString !== Object.prototype.toString) {
6419
- return value.toString();
6420
- }
6421
- // Otherwise use JSON representation
6422
- return JSON.stringify(value);
6423
- }
6424
- catch {
6425
- // If JSON.stringify fails (circular reference, etc.), return empty string
6426
- return '[object]';
6427
- }
6428
- }
6429
- /**
6430
- * Extracts parameter values from validation error
6431
- * @internal
6432
- */
6433
- function extractErrorParams(error) {
6434
- const params = {};
6435
- // Common validation error parameters
6436
- if ('min' in error)
6437
- params.min = error.min;
6438
- if ('max' in error)
6439
- params.max = error.max;
6440
- if ('requiredLength' in error)
6441
- params.requiredLength = error.requiredLength;
6442
- if ('actualLength' in error)
6443
- params.actualLength = error.actualLength;
6444
- if ('pattern' in error)
6445
- params.pattern = error.pattern;
6446
- if ('actual' in error)
6447
- params.actual = error.actual;
6448
- if ('expected' in error)
6449
- params.expected = error.expected;
6450
- // Include all custom properties from error object (for custom validators)
6451
- // Skip 'kind' as it's the error type identifier, not a parameter
6452
- Object.keys(error).forEach((key) => {
6453
- if (key !== 'kind' && !(key in params)) {
6454
- params[key] = error[key];
6455
- }
6456
- });
6457
- return params;
6458
- }
6459
-
6460
- /**
6461
- * Factory function that creates a signal of resolved validation errors
6462
- * Handles async resolution of DynamicText validation messages
6463
- *
6464
- * @param field - Signal containing FieldTree
6465
- * @param validationMessages - Signal containing custom field-level validation messages
6466
- * @param defaultValidationMessages - Signal containing default validation messages (fallback)
6467
- * @param injector - Optional injector for DynamicText resolution
6468
- * @returns Signal<ResolvedError[]> - Reactively resolved error messages
6469
- *
6470
- * @example
6471
- * ```typescript
6472
- * readonly resolvedErrors = createResolvedErrorsSignal(
6473
- * this.field,
6474
- * this.validationMessages,
6475
- * this.defaultValidationMessages
6476
- * );
6477
- * ```
6478
- */
6479
- function createResolvedErrorsSignal(field, validationMessages, defaultValidationMessages = signal(undefined), injector = inject(Injector)) {
6480
- // Ensure validationMessages is never undefined (mappers pass {} if not defined)
6481
- const messages = computed(() => validationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "messages" }] : []));
6482
- const defaultMessages = computed(() => defaultValidationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "defaultMessages" }] : []));
6483
- // Create a computed signal that reads the actual errors from the field
6484
- // This ensures the signal tracks changes to field().errors(), not just the field reference
6485
- const errors = computed(() => {
6486
- const control = field()();
6487
- return control.errors();
6488
- }, ...(ngDevMode ? [{ debugName: "errors" }] : []));
6489
- // Convert signals to observables using toObservable with injector
6490
- const errors$ = toObservable(errors, { injector });
6491
- const messages$ = toObservable(messages, { injector });
6492
- const defaultMessages$ = toObservable(defaultMessages, { injector });
6493
- // Combine observables and process errors
6494
- const resolvedErrors$ = combineLatest([errors$, messages$, defaultMessages$]).pipe(switchMap$1(([currentErrors, msgs, defaultMsgs]) => {
6495
- // No errors - return empty array
6496
- if (!currentErrors || currentErrors.length === 0) {
6497
- return of([]);
6498
- }
6499
- // Create observable for each error's resolved message
6500
- const errorResolvers = currentErrors.map((error) => resolveErrorMessage(error, msgs, defaultMsgs, injector));
6501
- // Combine all error message observables into single array emission, filtering out nulls
6502
- return errorResolvers.length > 0
6503
- ? combineLatest(errorResolvers).pipe(map$1((errors) => errors.filter((e) => e !== null)))
6504
- : of([]);
6505
- }));
6506
- // Convert observable back to signal using toSignal with injector
6507
- // toSignal properly handles the injection context and manages subscriptions
6508
- return toSignal(resolvedErrors$, { initialValue: [], injector });
6509
- }
6510
- /**
6511
- * Resolves a single error message from DynamicText sources with fallback logic
6512
- * Priority: field-level message → default message → no message (logs warning)
6513
- * @internal
6514
- */
6515
- function resolveErrorMessage(error, fieldMessages, defaultMessages, injector) {
6516
- // Check for field-level custom message first
6517
- const fieldMessage = fieldMessages[error.kind];
6518
- // Fall back to default message if no field-level message exists
6519
- const defaultMessage = defaultMessages[error.kind];
6520
- // Determine which message to use: field-level → default
6521
- const messageToUse = fieldMessage ?? defaultMessage;
6522
- // If no message found, log warning and return null (will be filtered out)
6523
- if (!messageToUse) {
6524
- console.warn(`[Dynamic Forms] No validation message found for error kind "${error.kind}". ` +
6525
- `Please provide a message via field-level validationMessages or form-level defaultValidationMessages.`);
6526
- return of(null);
6527
- }
6528
- // Convert DynamicText to Observable
6529
- const messageObservable = dynamicTextToObservable(messageToUse, injector);
6530
- // Apply parameter interpolation to support {{param}} syntax
6531
- return messageObservable.pipe(map$1((msg) => ({
6532
- kind: error.kind,
6533
- message: interpolateParams(msg, error),
6534
- })));
6535
- }
6536
-
6537
6227
  /**
6538
6228
  * Pipe that handles dynamic text resolution with support for static strings,
6539
6229
  * Observables, and Signals.
@@ -6574,10 +6264,10 @@ class DynamicTextPipe {
6574
6264
  }
6575
6265
  return of(value || '');
6576
6266
  }
6577
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
6578
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.5", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
6267
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
6268
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.6", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
6579
6269
  }
6580
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: DynamicTextPipe, decorators: [{
6270
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DynamicTextPipe, decorators: [{
6581
6271
  type: Pipe,
6582
6272
  args: [{
6583
6273
  name: 'dynamicText',
@@ -6623,6 +6313,7 @@ class RowFieldComponent {
6623
6313
  fieldRegistry = injectFieldRegistry();
6624
6314
  injector = inject(Injector);
6625
6315
  eventBus = inject(EventBus);
6316
+ logger = inject(DynamicFormLogger);
6626
6317
  // ─────────────────────────────────────────────────────────────────────────────
6627
6318
  // Inputs
6628
6319
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6656,12 +6347,12 @@ class RowFieldComponent {
6656
6347
  destroyRef: this.destroyRef,
6657
6348
  onError: (fieldDef, error) => {
6658
6349
  const fieldKey = fieldDef.key || '<no key>';
6659
- console.error(`[Dynamic Forms] Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +
6350
+ this.logger.error(`Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +
6660
6351
  `within row '${rowKey}'. Ensure the field type is registered in your field registry.`, error);
6661
6352
  },
6662
6353
  };
6663
6354
  return forkJoin(fields.map((f) => resolveField(f, context)));
6664
- }), map((fields) => fields.filter((f) => f !== undefined)), scan$1(reconcileFields, [])), { initialValue: [], injector: this.injector });
6355
+ }), map((fields) => fields.filter((f) => f !== undefined)), scan(reconcileFields, [])), { initialValue: [], injector: this.injector });
6665
6356
  // ─────────────────────────────────────────────────────────────────────────────
6666
6357
  // Constructor
6667
6358
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6679,14 +6370,14 @@ class RowFieldComponent {
6679
6370
  }
6680
6371
  });
6681
6372
  }
6682
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: RowFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6683
- 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: `
6373
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RowFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6374
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", 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: `
6684
6375
  @for (field of resolvedFields(); track field.key) {
6685
6376
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
6686
6377
  }
6687
6378
  `, 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 });
6688
6379
  }
6689
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: RowFieldComponent, decorators: [{
6380
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RowFieldComponent, decorators: [{
6690
6381
  type: Component,
6691
6382
  args: [{ selector: 'div[row-field]', imports: [NgComponentOutlet], template: `
6692
6383
  @for (field of resolvedFields(); track field.key) {
@@ -6722,12 +6413,22 @@ class GroupFieldComponent {
6722
6413
  parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
6723
6414
  injector = inject(Injector);
6724
6415
  eventBus = inject(EventBus);
6416
+ logger = inject(DynamicFormLogger);
6725
6417
  // ─────────────────────────────────────────────────────────────────────────────
6726
6418
  // Memoized Functions
6727
6419
  // ─────────────────────────────────────────────────────────────────────────────
6728
- memoizedFlattenFields = memoize((fields, registry) => flattenFields([...fields], registry), (fields, registry) => JSON.stringify(fields.map((f) => ({ key: f.key, type: f.type }))) + '_' + Array.from(registry.keys()).sort().join(','));
6729
- memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), (fields) => fields.map((f) => f.key).join(','));
6730
- memoizedDefaultValues = memoize((fieldsById, registry) => mapValues(fieldsById, (field) => getFieldDefaultValue(field, registry)), (fieldsById, registry) => Object.keys(fieldsById).sort().join(',') + '_' + Array.from(registry.keys()).sort().join(','));
6420
+ memoizedFlattenFields = memoize((fields, registry) => flattenFields([...fields], registry), {
6421
+ resolver: (fields, registry) => JSON.stringify(fields.map((f) => ({ key: f.key, type: f.type }))) + '_' + Array.from(registry.keys()).sort().join(','),
6422
+ maxSize: 10,
6423
+ });
6424
+ memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), {
6425
+ resolver: (fields) => fields.map((f) => f.key).join(','),
6426
+ maxSize: 10,
6427
+ });
6428
+ memoizedDefaultValues = memoize((fieldsById, registry) => mapValues(fieldsById, (field) => getFieldDefaultValue(field, registry)), {
6429
+ resolver: (fieldsById, registry) => Object.keys(fieldsById).sort().join(',') + '_' + Array.from(registry.keys()).sort().join(','),
6430
+ maxSize: 10,
6431
+ });
6731
6432
  // ─────────────────────────────────────────────────────────────────────────────
6732
6433
  // Inputs
6733
6434
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6738,35 +6439,25 @@ class GroupFieldComponent {
6738
6439
  // ─────────────────────────────────────────────────────────────────────────────
6739
6440
  rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : []));
6740
6441
  formSetup = computed(() => {
6741
- try {
6742
- const groupField = this.field();
6743
- const registry = this.rawFieldRegistry();
6744
- if (groupField.fields && groupField.fields.length > 0) {
6745
- const flattenedFields = this.memoizedFlattenFields(groupField.fields, registry);
6746
- const fieldsById = this.memoizedKeyBy(flattenedFields);
6747
- const defaultValues = this.memoizedDefaultValues(fieldsById, registry);
6748
- return {
6749
- fields: flattenedFields,
6750
- originalFields: groupField.fields,
6751
- defaultValues,
6752
- registry,
6753
- };
6754
- }
6442
+ const groupField = this.field();
6443
+ const registry = this.rawFieldRegistry();
6444
+ if (groupField.fields && groupField.fields.length > 0) {
6445
+ const flattenedFields = this.memoizedFlattenFields(groupField.fields, registry);
6446
+ const fieldsById = this.memoizedKeyBy(flattenedFields);
6447
+ const defaultValues = this.memoizedDefaultValues(fieldsById, registry);
6755
6448
  return {
6756
- fields: [],
6757
- originalFields: [],
6758
- defaultValues: {},
6449
+ fields: flattenedFields,
6450
+ originalFields: groupField.fields,
6451
+ defaultValues,
6759
6452
  registry,
6760
6453
  };
6761
6454
  }
6762
- catch {
6763
- return {
6764
- fields: [],
6765
- originalFields: [],
6766
- defaultValues: {},
6767
- registry: this.rawFieldRegistry(),
6768
- };
6769
- }
6455
+ return {
6456
+ fields: [],
6457
+ originalFields: [],
6458
+ defaultValues: {},
6459
+ registry,
6460
+ };
6770
6461
  }, ...(ngDevMode ? [{ debugName: "formSetup" }] : []));
6771
6462
  defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : []));
6772
6463
  entity = linkedSignal(() => {
@@ -6796,36 +6487,26 @@ class GroupFieldComponent {
6796
6487
  touched = computed(() => this.form()().touched(), ...(ngDevMode ? [{ debugName: "touched" }] : []));
6797
6488
  errors = computed(() => this.form()().errors(), ...(ngDevMode ? [{ debugName: "errors" }] : []));
6798
6489
  disabled = computed(() => this.form()().disabled(), ...(ngDevMode ? [{ debugName: "disabled" }] : []));
6799
- /**
6800
- * Get the nested FieldTree from the parent form for this group.
6801
- * This allows child fields to update the parent form directly,
6802
- * avoiding the need for separate form synchronization.
6803
- */
6804
6490
  nestedFieldTree = computed(() => {
6805
- const parentForm = this.parentFieldSignalContext.form();
6491
+ const parentForm = this.parentFieldSignalContext.form;
6806
6492
  const groupKey = this.field().key;
6807
- return getChildFieldTree(parentForm, groupKey);
6493
+ const child = parentForm[groupKey];
6494
+ if (!child) {
6495
+ throw new DynamicFormError(`Group field "${groupKey}" not found in parent form. ` + `Ensure the parent form schema includes this group field.`);
6496
+ }
6497
+ return child;
6808
6498
  }, ...(ngDevMode ? [{ debugName: "nestedFieldTree" }] : []));
6809
6499
  groupInjector = computed(() => {
6810
- const nestedTree = this.nestedFieldTree();
6811
- // Use the nested FieldTree from parent if available, otherwise fall back to our own form
6812
- // The nested FieldTree ensures changes propagate directly to the parent form
6813
- const formToProvide = nestedTree ?? this.form();
6814
6500
  const groupFieldSignalContext = {
6815
6501
  injector: this.injector,
6816
6502
  value: this.parentFieldSignalContext.value,
6817
6503
  defaultValues: this.defaultValues,
6818
- form: (() => formToProvide),
6504
+ form: this.nestedFieldTree(),
6819
6505
  defaultValidationMessages: this.parentFieldSignalContext.defaultValidationMessages,
6820
6506
  };
6821
6507
  return Injector.create({
6822
6508
  parent: this.injector,
6823
- providers: [
6824
- {
6825
- provide: FIELD_SIGNAL_CONTEXT,
6826
- useValue: groupFieldSignalContext,
6827
- },
6828
- ],
6509
+ providers: [{ provide: FIELD_SIGNAL_CONTEXT, useValue: groupFieldSignalContext }],
6829
6510
  });
6830
6511
  }, ...(ngDevMode ? [{ debugName: "groupInjector" }] : []));
6831
6512
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6838,24 +6519,17 @@ class GroupFieldComponent {
6838
6519
  // Field Resolution
6839
6520
  // ─────────────────────────────────────────────────────────────────────────────
6840
6521
  fieldsSource = computed(() => this.formSetup().fields, ...(ngDevMode ? [{ debugName: "fieldsSource" }] : []));
6841
- resolvedFields = derivedFromDeferred(this.fieldsSource, pipe(switchMap((fields) => {
6842
- if (!fields || fields.length === 0) {
6843
- return of([]);
6844
- }
6845
- const groupKey = this.field().key;
6846
- const context = {
6847
- loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
6848
- registry: this.rawFieldRegistry(),
6849
- injector: this.groupInjector(),
6850
- destroyRef: this.destroyRef,
6851
- onError: (fieldDef, error) => {
6852
- const fieldKey = fieldDef.key || '<no key>';
6853
- console.error(`[Dynamic Forms] Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +
6854
- `within group '${groupKey}'. Ensure the field type is registered in your field registry.`, error);
6855
- },
6856
- };
6857
- return forkJoin(fields.map((f) => resolveField(f, context)));
6858
- }), map((fields) => fields.filter((f) => f !== undefined)), scan$1(reconcileFields, [])), { initialValue: [], injector: this.injector });
6522
+ resolvedFields = derivedFromDeferred(this.fieldsSource, createFieldResolutionPipe(() => ({
6523
+ loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
6524
+ registry: this.rawFieldRegistry(),
6525
+ injector: this.groupInjector(),
6526
+ destroyRef: this.destroyRef,
6527
+ onError: (fieldDef, error) => {
6528
+ const fieldKey = fieldDef.key || '<no key>';
6529
+ this.logger.error(`Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +
6530
+ `within group '${this.field().key}'. Ensure the field type is registered in your field registry.`, error);
6531
+ },
6532
+ })), { initialValue: [], injector: this.injector });
6859
6533
  // ─────────────────────────────────────────────────────────────────────────────
6860
6534
  // Constructor
6861
6535
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6873,14 +6547,14 @@ class GroupFieldComponent {
6873
6547
  }
6874
6548
  });
6875
6549
  }
6876
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6877
- 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: `
6550
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6551
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", 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: `
6878
6552
  @for (field of resolvedFields(); track field.key) {
6879
6553
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
6880
6554
  }
6881
6555
  `, 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 });
6882
6556
  }
6883
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: GroupFieldComponent, decorators: [{
6557
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: GroupFieldComponent, decorators: [{
6884
6558
  type: Component,
6885
6559
  args: [{ selector: 'fieldset[group-field]', imports: [NgComponentOutlet], template: `
6886
6560
  @for (field of resolvedFields(); track field.key) {
@@ -6961,8 +6635,8 @@ function syncItemToParent(itemFormInstance, parentFieldSignalContext, arrayKey,
6961
6635
  explicitEffect([() => itemFormInstance().value()], ([itemValue]) => {
6962
6636
  if (isSyncing)
6963
6637
  return;
6964
- const parentForm = parentFieldSignalContext.form();
6965
- const parentValue = untracked(() => parentForm.value());
6638
+ const parentForm = parentFieldSignalContext.form;
6639
+ const parentValue = untracked(() => parentForm().value());
6966
6640
  const currentArray = getArrayValue(parentValue, arrayKey);
6967
6641
  const idx = untracked(() => indexSignal());
6968
6642
  // Only sync if value actually changed
@@ -6970,8 +6644,10 @@ function syncItemToParent(itemFormInstance, parentFieldSignalContext, arrayKey,
6970
6644
  isSyncing = true;
6971
6645
  const newArray = [...currentArray];
6972
6646
  newArray[idx] = itemValue;
6647
+ // Update the parent form with the new array value
6648
+ // The `as any` is required due to Angular Signal Forms' complex conditional types
6973
6649
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6974
- parentForm.value.set({ ...parentValue, [arrayKey]: newArray });
6650
+ parentForm().value.set({ ...parentValue, [arrayKey]: newArray });
6975
6651
  isSyncing = false;
6976
6652
  }
6977
6653
  });
@@ -7018,7 +6694,7 @@ function createArrayItemInjectorAndInputs(options) {
7018
6694
  * The linkedSignal derives from the parent array at the current index.
7019
6695
  *
7020
6696
  * For array items, we always create a form with a schema to ensure proper field structure.
7021
- * This is needed for valueFieldMapper to find fields via childrenMap.
6697
+ * This is needed for valueFieldMapper to find fields via bracket notation.
7022
6698
  */
7023
6699
  function createObjectItemForm(options) {
7024
6700
  const { template, indexSignal, parentFieldSignalContext, parentInjector, registry, arrayKey } = options;
@@ -7054,7 +6730,6 @@ function createItemInjector(options) {
7054
6730
  injector: undefined,
7055
6731
  value: parentFieldSignalContext.value,
7056
6732
  defaultValues: () => ({}),
7057
- // formRef is already the form instance (result of form()), no need to wrap in another function
7058
6733
  form: formRef,
7059
6734
  defaultValidationMessages: parentFieldSignalContext.defaultValidationMessages,
7060
6735
  };
@@ -7098,14 +6773,14 @@ function resolveArrayItem(options) {
7098
6773
  });
7099
6774
  return {
7100
6775
  id: itemId,
7101
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7102
6776
  component: component,
7103
6777
  injector,
7104
6778
  inputs,
7105
6779
  };
7106
6780
  }), catchError((error) => {
7107
6781
  if (!destroyRef.destroyed) {
7108
- console.error(`[Dynamic Forms] Failed to load component for field type '${template.type}' at index ${index} ` +
6782
+ const logger = parentInjector.get(DynamicFormLogger);
6783
+ logger.error(`Failed to load component for field type '${template.type}' at index ${index} ` +
7109
6784
  `within array '${arrayField.key}'. Ensure the field type is registered in your field registry.`, error);
7110
6785
  }
7111
6786
  return of(undefined);
@@ -7128,6 +6803,7 @@ class ArrayFieldComponent {
7128
6803
  parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
7129
6804
  parentInjector = inject(Injector);
7130
6805
  eventBus = inject(EventBus);
6806
+ logger = inject(DynamicFormLogger);
7131
6807
  // ─────────────────────────────────────────────────────────────────────────────
7132
6808
  // Inputs
7133
6809
  // ─────────────────────────────────────────────────────────────────────────────
@@ -7143,21 +6819,18 @@ class ArrayFieldComponent {
7143
6819
  }, ...(ngDevMode ? [{ debugName: "fieldTemplate" }] : []));
7144
6820
  arrayFieldTrees = computed(() => {
7145
6821
  const arrayKey = this.field().key;
7146
- const parentForm = this.parentFieldSignalContext.form();
6822
+ const parentForm = this.parentFieldSignalContext.form;
7147
6823
  const arrayValue = getArrayValue(this.parentFieldSignalContext.value(), arrayKey);
7148
6824
  if (arrayValue.length === 0)
7149
6825
  return [];
7150
- // Get the array FieldTree from the parent form
7151
- const arrayFieldTree = getChildFieldTree(parentForm, arrayKey);
6826
+ const arrayFieldTree = parentForm[arrayKey];
7152
6827
  if (!arrayFieldTree)
7153
6828
  return arrayValue.map(() => null);
7154
6829
  // Access array items via bracket notation - Angular Signal Forms arrays support this
7155
- // The type is ReadonlyArrayLike<MaybeFieldTree<U, number>>
7156
- const arrayWithIndex = arrayFieldTree;
7157
6830
  const items = [];
7158
6831
  for (let i = 0; i < arrayValue.length; i++) {
7159
- // Access item FieldTree directly via bracket notation
7160
- const itemFieldTree = arrayWithIndex[i];
6832
+ // Access item FieldTree directly via numeric indexing (ArrayFieldTree supports this)
6833
+ const itemFieldTree = arrayFieldTree[i];
7161
6834
  items.push(itemFieldTree ?? null);
7162
6835
  }
7163
6836
  return items;
@@ -7270,13 +6943,13 @@ class ArrayFieldComponent {
7270
6943
  }
7271
6944
  addItem(fieldTemplate, index) {
7272
6945
  if (!fieldTemplate) {
7273
- console.error(`[Dynamic Forms] Cannot add item to array '${this.field().key}': no field template provided. ` +
6946
+ this.logger.error(`Cannot add item to array '${this.field().key}': no field template provided. ` +
7274
6947
  'Ensure the array field has a fields property with at least one field definition.');
7275
6948
  return;
7276
6949
  }
7277
6950
  const arrayKey = this.field().key;
7278
- const formObject = this.parentFieldSignalContext.form();
7279
- const currentValue = formObject.value();
6951
+ const parentForm = this.parentFieldSignalContext.form;
6952
+ const currentValue = parentForm().value();
7280
6953
  const currentArray = getArrayValue(currentValue, arrayKey);
7281
6954
  const insertIndex = index !== undefined ? Math.min(index, currentArray.length) : currentArray.length;
7282
6955
  const rawValue = getFieldDefaultValue(fieldTemplate, this.rawFieldRegistry());
@@ -7286,30 +6959,34 @@ class ArrayFieldComponent {
7286
6959
  const value = valueHandling === 'include' && fieldTemplate.key ? { [fieldTemplate.key]: rawValue } : rawValue;
7287
6960
  const newArray = [...currentArray];
7288
6961
  newArray.splice(insertIndex, 0, value);
6962
+ // Update the parent form with the new array value
6963
+ // The `as any` is required due to Angular Signal Forms' complex conditional types
7289
6964
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7290
- formObject.value.set({ ...currentValue, [arrayKey]: newArray });
6965
+ parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
7291
6966
  }
7292
6967
  removeItem(index) {
7293
6968
  const arrayKey = this.field().key;
7294
- const formObject = this.parentFieldSignalContext.form();
7295
- const currentValue = formObject.value();
6969
+ const parentForm = this.parentFieldSignalContext.form;
6970
+ const currentValue = parentForm().value();
7296
6971
  const currentArray = getArrayValue(currentValue, arrayKey);
7297
6972
  if (currentArray.length === 0)
7298
6973
  return;
7299
6974
  const removeIndex = index !== undefined ? Math.min(index, currentArray.length - 1) : currentArray.length - 1;
7300
6975
  const newArray = [...currentArray];
7301
6976
  newArray.splice(removeIndex, 1);
6977
+ // Update the parent form with the new array value
6978
+ // The `as any` is required due to Angular Signal Forms' complex conditional types
7302
6979
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7303
- formObject.value.set({ ...currentValue, [arrayKey]: newArray });
6980
+ parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
7304
6981
  }
7305
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7306
- 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: `
6982
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6983
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", 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: `
7307
6984
  @for (item of resolvedItems(); track item.id) {
7308
6985
  <ng-container *ngComponentOutlet="item.component; injector: item.injector; inputs: item.inputs()" />
7309
6986
  }
7310
6987
  `, 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 });
7311
6988
  }
7312
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: ArrayFieldComponent, decorators: [{
6989
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ArrayFieldComponent, decorators: [{
7313
6990
  type: Component,
7314
6991
  args: [{ selector: 'array-field', imports: [NgComponentOutlet], template: `
7315
6992
  @for (item of resolvedItems(); track item.id) {
@@ -7328,6 +7005,103 @@ var arrayField_component = /*#__PURE__*/Object.freeze({
7328
7005
  default: ArrayFieldComponent
7329
7006
  });
7330
7007
 
7008
+ /**
7009
+ * Get the length of an array FieldTree.
7010
+ */
7011
+ function getArrayLength(arrayFieldTree) {
7012
+ const value = arrayFieldTree().value();
7013
+ return Array.isArray(value) ? value.length : 0;
7014
+ }
7015
+
7016
+ /**
7017
+ * Interpolates {{param}} placeholders in message with error values
7018
+ *
7019
+ * @param message - Message template with {{param}} placeholders
7020
+ * @param error - Validation error containing parameter values
7021
+ * @returns Message with interpolated parameters
7022
+ *
7023
+ * @example
7024
+ * interpolateParams("Min value is {{min}}", { kind: 'min', min: 5 })
7025
+ * // Returns: "Min value is 5"
7026
+ */
7027
+ function interpolateParams(message, error) {
7028
+ let result = message;
7029
+ const params = extractErrorParams(error);
7030
+ Object.entries(params).forEach(([key, value]) => {
7031
+ const placeholder = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
7032
+ result = result.replace(placeholder, safeToString(value));
7033
+ });
7034
+ return result;
7035
+ }
7036
+ /**
7037
+ * Safely converts a value to a string, handling complex objects
7038
+ * @internal
7039
+ */
7040
+ function safeToString(value) {
7041
+ if (value === null || value === undefined) {
7042
+ return '';
7043
+ }
7044
+ // Handle primitives directly
7045
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
7046
+ return String(value);
7047
+ }
7048
+ // Handle RegExp (has its own toString)
7049
+ if (value instanceof RegExp) {
7050
+ return value.toString();
7051
+ }
7052
+ // Handle Date
7053
+ if (value instanceof Date) {
7054
+ return value.toISOString();
7055
+ }
7056
+ // Handle arrays
7057
+ if (Array.isArray(value)) {
7058
+ return value.join(', ');
7059
+ }
7060
+ // For objects, try JSON.stringify first, fall back to empty string
7061
+ try {
7062
+ // Check if object has a custom toString that's not Object.prototype.toString
7063
+ if (typeof value === 'object' && value.toString !== Object.prototype.toString) {
7064
+ return value.toString();
7065
+ }
7066
+ // Otherwise use JSON representation
7067
+ return JSON.stringify(value);
7068
+ }
7069
+ catch {
7070
+ // If JSON.stringify fails (circular reference, etc.), return empty string
7071
+ return '[object]';
7072
+ }
7073
+ }
7074
+ /**
7075
+ * Extracts parameter values from validation error
7076
+ * @internal
7077
+ */
7078
+ function extractErrorParams(error) {
7079
+ const params = {};
7080
+ // Common validation error parameters
7081
+ if ('min' in error)
7082
+ params.min = error.min;
7083
+ if ('max' in error)
7084
+ params.max = error.max;
7085
+ if ('requiredLength' in error)
7086
+ params.requiredLength = error.requiredLength;
7087
+ if ('actualLength' in error)
7088
+ params.actualLength = error.actualLength;
7089
+ if ('pattern' in error)
7090
+ params.pattern = error.pattern;
7091
+ if ('actual' in error)
7092
+ params.actual = error.actual;
7093
+ if ('expected' in error)
7094
+ params.expected = error.expected;
7095
+ // Include all custom properties from error object (for custom validators)
7096
+ // Skip 'kind' as it's the error type identifier, not a parameter
7097
+ Object.keys(error).forEach((key) => {
7098
+ if (key !== 'kind' && !(key in params)) {
7099
+ params[key] = error[key];
7100
+ }
7101
+ });
7102
+ return params;
7103
+ }
7104
+
7331
7105
  /**
7332
7106
  * @ng-forge/dynamic-forms
7333
7107
  *
@@ -7339,11 +7113,16 @@ var arrayField_component = /*#__PURE__*/Object.freeze({
7339
7113
  * - FormConfig and field definition types
7340
7114
  * - Event classes (SubmitEvent, PageChangeEvent, etc.)
7341
7115
  *
7342
- * ## Integration API (for UI library authors building Material, Bootstrap, etc.)
7343
- * - Mappers (baseFieldMapper, valueFieldMapper, etc.)
7344
- * - FieldTypeDefinition for registering field types
7345
- * - DynamicTextPipe, shouldShowErrors, createResolvedErrorsSignal
7346
- * - EventBus for custom event handling
7116
+ * ## Integration API (for UI library authors)
7117
+ * Import from '@ng-forge/dynamic-forms/integration' for:
7118
+ * - Specific field types (InputField, SelectField, etc.)
7119
+ * - Field mappers (valueFieldMapper, checkboxFieldMapper, etc.)
7120
+ * - Error utilities (createResolvedErrorsSignal, shouldShowErrors)
7121
+ *
7122
+ * ## Testing API
7123
+ * Import from '@ng-forge/dynamic-forms/testing' for:
7124
+ * - Test utilities and harnesses
7125
+ * - Form config builders for testing
7347
7126
  */
7348
7127
  // ============================================================
7349
7128
  // PUBLIC API - For end users
@@ -7354,5 +7133,5 @@ var arrayField_component = /*#__PURE__*/Object.freeze({
7354
7133
  * Generated bundle index. Do not edit.
7355
7134
  */
7356
7135
 
7357
- export { resolveTokens as $, AddArrayItemEvent as A, checkboxFieldMapper as B, datepickerFieldMapper as C, DynamicTextPipe as D, EventBus as E, FormClearEvent as F, groupFieldMapper as G, pageFieldMapper as H, optionsFieldMapper as I, rowFieldMapper as J, valueFieldMapper as K, FIELD_REGISTRY as L, getFieldValueHandling as M, NextPageEvent as N, ARRAY_CONTEXT as O, PageChangeEvent as P, FIELD_SIGNAL_CONTEXT as Q, RemoveArrayItemEvent as R, SubmitEvent as S, createResolvedErrorsSignal as T, shouldShowErrors as U, dynamicTextToObservable as V, ArrayFieldComponent as W, GroupFieldComponent as X, RowFieldComponent as Y, applyValidator as Z, applyValidators as _, reconcileFields as a, BUILT_IN_FIELDS as a0, getChildFieldTree as a1, getChildrenMap as a2, getFieldProxy as a3, hasFormStructure as a4, DynamicForm as b, isFormStateCondition as c, derivedFromDeferred as d, emitComponentInitialized as e, resolveSubmitButtonDisabled as f, resolveNextButtonDisabled as g, isCheckedField as h, injectFieldRegistry as i, isValueField as j, isArrayField as k, isContainerField as l, isDisplayOnlyField as m, isGroupField as n, isLeafField as o, provideDynamicForm as p, isPageField as q, resolveField as r, isRowField as s, isValueBearingField as t, FormResetEvent as u, validatePageNesting as v, PreviousPageEvent as w, arrayFieldMapper as x, baseFieldMapper as y, buildBaseInputs as z };
7358
- //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-BWj75LVN.mjs.map
7136
+ 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, 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 };
7137
+ //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-CZBdtTAU.mjs.map