@ng-forge/dynamic-forms 0.1.1 → 0.1.3

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 (20) hide show
  1. package/README.md +33 -0
  2. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +269 -0
  3. package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -0
  4. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-Dcd04rfS.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-B_676NHl.mjs} +989 -1211
  5. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-B_676NHl.mjs.map +1 -0
  6. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-DjrahUgb.mjs → ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs} +31 -29
  7. package/fesm2022/ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs.map +1 -0
  8. package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-BenmCFcM.mjs +31 -0
  9. package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-BenmCFcM.mjs.map +1 -0
  10. package/fesm2022/ng-forge-dynamic-forms-testing.mjs +968 -0
  11. package/fesm2022/ng-forge-dynamic-forms-testing.mjs.map +1 -0
  12. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-7QVNm95W.mjs → ng-forge-dynamic-forms-text-field.component-CiYq73aF.mjs} +2 -2
  13. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-7QVNm95W.mjs.map → ng-forge-dynamic-forms-text-field.component-CiYq73aF.mjs.map} +1 -1
  14. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  15. package/package.json +17 -3
  16. package/types/ng-forge-dynamic-forms-integration.d.ts +315 -0
  17. package/types/ng-forge-dynamic-forms-testing.d.ts +565 -0
  18. package/types/ng-forge-dynamic-forms.d.ts +353 -549
  19. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-Dcd04rfS.mjs.map +0 -1
  20. package/fesm2022/ng-forge-dynamic-forms-page-field.component-DjrahUgb.mjs.map +0 -1
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { isSignal, computed, runInInjectionContext, InjectionToken, inject, signal, Injectable, untracked, input, linkedSignal, ChangeDetectionStrategy, Component, DestroyRef, Injector, model, afterNextRender, makeEnvironmentProviders, Pipe } from '@angular/core';
2
+ import { isSignal, computed, runInInjectionContext, InjectionToken, inject, isDevMode, 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
  /**
@@ -84,7 +84,7 @@ function derivedFromDeferred(source, operator, options) {
84
84
  * `
85
85
  * })
86
86
  * export class FieldComponent {
87
- * field: FieldDef<any>;
87
+ * field: FieldDef<unknown>;
88
88
  * getGridClassString = getGridClassString;
89
89
  * }
90
90
  * ```
@@ -92,11 +92,10 @@ function derivedFromDeferred(source, operator, options) {
92
92
  * @public
93
93
  */
94
94
  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}`);
95
+ if (typeof fieldDef.col === 'number' && Number.isInteger(fieldDef.col) && fieldDef.col > 0 && fieldDef.col <= 12) {
96
+ return `df-col-${fieldDef.col}`;
98
97
  }
99
- return classes.join(' ');
98
+ return '';
100
99
  }
101
100
 
102
101
  /**
@@ -228,6 +227,19 @@ function reconcileFields(prev, curr) {
228
227
  return field;
229
228
  });
230
229
  }
230
+ /**
231
+ * Creates an RxJS pipe for resolving field definitions to rendered components.
232
+ * Used by container components (page, group) to resolve their child fields.
233
+ */
234
+ function createFieldResolutionPipe(getContext) {
235
+ return pipe(switchMap((fields) => {
236
+ if (!fields || fields.length === 0) {
237
+ return of([]);
238
+ }
239
+ const context = getContext();
240
+ return forkJoin(fields.map((f) => resolveField(f, context)));
241
+ }), map((fields) => fields.filter((f) => f !== undefined)), scan(reconcileFields, []));
242
+ }
231
243
 
232
244
  /**
233
245
  * Wraps a submission action to handle both Promise and Observable returns.
@@ -332,7 +344,7 @@ function omit(obj, keys) {
332
344
  */
333
345
  function keyBy(array, key) {
334
346
  return array.reduce((acc, item) => {
335
- acc[item[key]] = item;
347
+ acc[String(item[key])] = item;
336
348
  return acc;
337
349
  }, {});
338
350
  }
@@ -357,29 +369,49 @@ function mapValues(obj, fn) {
357
369
  }, {});
358
370
  }
359
371
  /**
360
- * Memoizes a function with optional custom cache key resolver
361
- * Native replacement for lodash memoize()
372
+ * Options for memoize function
373
+ */
374
+ /** Default maximum cache size for memoized functions */
375
+ const DEFAULT_MEMOIZE_MAX_SIZE = 100;
376
+ /**
377
+ * Memoizes a function with LRU cache eviction and optional custom cache key resolver.
362
378
  *
363
379
  * @param fn - Function to memoize
364
- * @param resolver - Optional function to generate cache key from arguments
380
+ * @param resolverOrOptions - Optional key resolver function or options object
365
381
  * @returns Memoized function
366
382
  *
367
383
  * @example
368
384
  * ```typescript
369
385
  * 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
386
+ * const memoized = memoize(expensive); // Uses default maxSize of 100
387
+ *
388
+ * // With custom resolver
389
+ * const withResolver = memoize(expensive, (a, b) => `${a}-${b}`);
390
+ *
391
+ * // With custom maxSize
392
+ * const small = memoize(expensive, { maxSize: 10 });
373
393
  * ```
374
394
  */
375
- function memoize(fn, resolver) {
395
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
396
+ function memoize(fn, resolverOrOptions) {
397
+ const options = typeof resolverOrOptions === 'function' ? { resolver: resolverOrOptions } : (resolverOrOptions ?? {});
398
+ const { resolver, maxSize = DEFAULT_MEMOIZE_MAX_SIZE } = options;
376
399
  const cache = new Map();
377
400
  return ((...args) => {
378
401
  const key = resolver ? resolver(...args) : JSON.stringify(args);
379
402
  if (cache.has(key)) {
380
- return cache.get(key);
403
+ const value = cache.get(key);
404
+ cache.delete(key);
405
+ cache.set(key, value);
406
+ return value;
381
407
  }
382
408
  const result = fn(...args);
409
+ if (cache.size >= maxSize) {
410
+ const oldestKey = cache.keys().next().value;
411
+ if (oldestKey !== undefined) {
412
+ cache.delete(oldestKey);
413
+ }
414
+ }
383
415
  cache.set(key, result);
384
416
  return result;
385
417
  });
@@ -421,15 +453,27 @@ function isEqual(a, b) {
421
453
  }
422
454
  // Handle objects
423
455
  if (typeof a === 'object' && typeof b === 'object') {
424
- const keysA = Object.keys(a);
425
- const keysB = Object.keys(b);
456
+ const objA = a;
457
+ const objB = b;
458
+ const keysA = Object.keys(objA);
459
+ const keysB = Object.keys(objB);
426
460
  if (keysA.length !== keysB.length)
427
461
  return false;
428
- return keysA.every((key) => keysB.includes(key) && isEqual(a[key], b[key]));
462
+ return keysA.every((key) => keysB.includes(key) && isEqual(objA[key], objB[key]));
429
463
  }
430
464
  // Primitives - use Object.is to handle NaN correctly (NaN === NaN is false, but Object.is(NaN, NaN) is true)
431
465
  return Object.is(a, b);
432
466
  }
467
+ /**
468
+ * Normalizes a fields collection to an array.
469
+ * Handles both array format and object format (keyed by field key).
470
+ *
471
+ * @param fields - Fields as array or object
472
+ * @returns Fields as array
473
+ */
474
+ function normalizeFieldsArray(fields) {
475
+ return Array.isArray(fields) ? [...fields] : Object.values(fields);
476
+ }
433
477
 
434
478
  /**
435
479
  * Injection token for providing field signal context to mappers and components.
@@ -691,6 +735,101 @@ function injectFieldRegistry() {
691
735
  };
692
736
  }
693
737
 
738
+ /**
739
+ * Log levels for the dynamic forms logger.
740
+ * Each level includes all levels above it (more severe).
741
+ *
742
+ * - Off: No logging
743
+ * - Error: Only errors
744
+ * - Warn: Errors + warnings
745
+ * - Info: Errors + warnings + info
746
+ * - Debug: All messages including debug
747
+ */
748
+ var LogLevel;
749
+ (function (LogLevel) {
750
+ /** Disable all logging */
751
+ LogLevel[LogLevel["Off"] = 0] = "Off";
752
+ /** Only log errors */
753
+ LogLevel[LogLevel["Error"] = 1] = "Error";
754
+ /** Log errors and warnings */
755
+ LogLevel[LogLevel["Warn"] = 2] = "Warn";
756
+ /** Log errors, warnings, and info messages */
757
+ LogLevel[LogLevel["Info"] = 3] = "Info";
758
+ /** Log all messages including debug */
759
+ LogLevel[LogLevel["Debug"] = 4] = "Debug";
760
+ })(LogLevel || (LogLevel = {}));
761
+
762
+ /**
763
+ * Console-based logger implementation.
764
+ * Respects log level configuration to filter messages.
765
+ */
766
+ class ConsoleLogger {
767
+ level;
768
+ prefix = '[Dynamic Forms]';
769
+ constructor(level) {
770
+ this.level = level;
771
+ }
772
+ debug(message, ...args) {
773
+ if (this.level >= LogLevel.Debug) {
774
+ console.debug(this.prefix, message, ...args);
775
+ }
776
+ }
777
+ info(message, ...args) {
778
+ if (this.level >= LogLevel.Info) {
779
+ console.info(this.prefix, message, ...args);
780
+ }
781
+ }
782
+ warn(message, ...args) {
783
+ if (this.level >= LogLevel.Warn) {
784
+ console.warn(this.prefix, message, ...args);
785
+ }
786
+ }
787
+ error(message, ...args) {
788
+ if (this.level >= LogLevel.Error) {
789
+ console.error(this.prefix, message, ...args);
790
+ }
791
+ }
792
+ }
793
+
794
+ /**
795
+ * No-operation logger implementation.
796
+ * All methods are no-ops - used in production by default.
797
+ */
798
+ class NoopLogger {
799
+ debug() {
800
+ // Intentionally empty
801
+ }
802
+ info() {
803
+ // Intentionally empty
804
+ }
805
+ warn() {
806
+ // Intentionally empty
807
+ }
808
+ error() {
809
+ // Intentionally empty
810
+ }
811
+ }
812
+
813
+ /**
814
+ * Injection token for the dynamic forms logger.
815
+ *
816
+ * Default behavior:
817
+ * - Development mode: ConsoleLogger with LogLevel.Warn
818
+ * - Production mode: NoopLogger (silent)
819
+ *
820
+ * Override by providing a custom logger via withLogger() feature
821
+ * or by providing directly to this token.
822
+ */
823
+ const DYNAMIC_FORM_LOGGER = new InjectionToken('DYNAMIC_FORM_LOGGER', {
824
+ providedIn: 'root',
825
+ factory: () => {
826
+ if (isDevMode()) {
827
+ return new ConsoleLogger(LogLevel.Warn);
828
+ }
829
+ return new NoopLogger();
830
+ },
831
+ });
832
+
694
833
  /**
695
834
  * Registry service that maintains references to root form fields.
696
835
  * This provides a type-safe way to access root form values from child fields
@@ -867,6 +1006,7 @@ function isChildFieldContext(context) {
867
1006
  */
868
1007
  class FieldContextRegistryService {
869
1008
  rootFormRegistry = inject(RootFormRegistryService);
1009
+ logger = inject(DYNAMIC_FORM_LOGGER);
870
1010
  /**
871
1011
  * Creates an evaluation context for a field by combining:
872
1012
  * - The field's current value
@@ -895,43 +1035,25 @@ class FieldContextRegistryService {
895
1035
  // - Validators still run when their own field value changes (via fieldValue)
896
1036
  // - Cross-field validation still works, just without cascading re-evaluation
897
1037
  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);
1038
+ const fieldPath = this.extractFieldPath(fieldContext);
901
1039
  return {
902
1040
  fieldValue,
903
1041
  formValue,
904
1042
  fieldPath,
905
1043
  customFunctions: customFunctions || {},
1044
+ logger: this.logger,
906
1045
  };
907
1046
  }
908
1047
  /**
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.
1048
+ * Extracts the field path (key) for a given field context.
923
1049
  */
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)) {
1050
+ extractFieldPath(fieldContext) {
1051
+ if (isChildFieldContext(fieldContext)) {
928
1052
  try {
929
- const key = extendedContext.key();
930
- const fieldPaths = formContext.fieldPaths || {};
931
- return fieldPaths[String(key)] || String(key);
1053
+ return String(fieldContext.key());
932
1054
  }
933
1055
  catch (error) {
934
- console.warn('[Dynamic Forms] Unable to extract field key:', error);
1056
+ this.logger.warn('Unable to extract field key:', error);
935
1057
  }
936
1058
  }
937
1059
  // For root fields or when key is not available
@@ -957,19 +1079,39 @@ class FieldContextRegistryService {
957
1079
  * hoisted to form-level using validateTree.
958
1080
  */
959
1081
  createReactiveEvaluationContext(fieldContext, customFunctions, formId = 'default') {
960
- // Read field value reactively (creates a dependency)
961
1082
  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
1083
  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);
1084
+ const fieldPath = this.extractFieldPath(fieldContext);
968
1085
  return {
969
1086
  fieldValue,
970
1087
  formValue,
971
1088
  fieldPath,
972
1089
  customFunctions: customFunctions || {},
1090
+ logger: this.logger,
1091
+ };
1092
+ }
1093
+ /**
1094
+ * Creates an evaluation context for display-only components (text fields, pages)
1095
+ * that don't have their own FieldContext.
1096
+ *
1097
+ * This is useful for:
1098
+ * - Text fields (display-only, not part of form schema)
1099
+ * - Pages (containers that need to evaluate visibility logic)
1100
+ *
1101
+ * Uses reactive form value access to allow logic re-evaluation when form values change.
1102
+ *
1103
+ * @param fieldPath - The key/path of the display-only component
1104
+ * @param customFunctions - Optional custom functions for expression evaluation
1105
+ * @param formId - Optional form identifier (defaults to 'default')
1106
+ */
1107
+ createDisplayOnlyContext(fieldPath, customFunctions, formId = 'default') {
1108
+ const formValue = this.rootFormRegistry.getFormValue(formId);
1109
+ return {
1110
+ fieldValue: undefined,
1111
+ formValue,
1112
+ fieldPath,
1113
+ customFunctions: customFunctions || {},
1114
+ logger: this.logger,
973
1115
  };
974
1116
  }
975
1117
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
@@ -1461,7 +1603,6 @@ var TokenType;
1461
1603
  TokenType["RBRACKET"] = "RBRACKET";
1462
1604
  // Special
1463
1605
  TokenType["EOF"] = "EOF";
1464
- TokenType["TYPEOF"] = "TYPEOF";
1465
1606
  })(TokenType || (TokenType = {}));
1466
1607
  /**
1467
1608
  * Parser error with position information
@@ -1596,8 +1737,6 @@ class Tokenizer {
1596
1737
  return TokenType.NULL;
1597
1738
  case 'undefined':
1598
1739
  return TokenType.UNDEFINED;
1599
- case 'typeof':
1600
- return TokenType.TYPEOF;
1601
1740
  default:
1602
1741
  return TokenType.IDENTIFIER;
1603
1742
  }
@@ -1757,7 +1896,7 @@ class Parser {
1757
1896
  return node;
1758
1897
  }
1759
1898
  parseUnary() {
1760
- if (this.match(TokenType.NOT, TokenType.MINUS, TokenType.PLUS, TokenType.TYPEOF)) {
1899
+ if (this.match(TokenType.NOT, TokenType.MINUS, TokenType.PLUS)) {
1761
1900
  const operator = this.previous().value;
1762
1901
  const operand = this.parseUnary();
1763
1902
  return { type: 'UnaryOp', operator, operand };
@@ -2030,10 +2169,10 @@ class Evaluator {
2030
2169
  }
2031
2170
  }
2032
2171
  evaluateIdentifier(name) {
2033
- if (!(name in this.scope)) {
2034
- return undefined;
2172
+ if (name in this.scope) {
2173
+ return this.scope[name];
2035
2174
  }
2036
- return this.scope[name];
2175
+ return undefined;
2037
2176
  }
2038
2177
  evaluateMemberAccess(node) {
2039
2178
  const obj = this.evaluate(node.object);
@@ -2051,6 +2190,16 @@ class Evaluator {
2051
2190
  return undefined;
2052
2191
  }
2053
2192
  evaluateBinaryOp(node) {
2193
+ // Handle short-circuit evaluation for logical operators
2194
+ if (node.operator === '&&') {
2195
+ const left = this.evaluate(node.left);
2196
+ return left ? this.evaluate(node.right) : left;
2197
+ }
2198
+ if (node.operator === '||') {
2199
+ const left = this.evaluate(node.left);
2200
+ return left ? left : this.evaluate(node.right);
2201
+ }
2202
+ // For all other operators, evaluate both sides
2054
2203
  const left = this.evaluate(node.left);
2055
2204
  const right = this.evaluate(node.right);
2056
2205
  switch (node.operator) {
@@ -2062,8 +2211,12 @@ class Evaluator {
2062
2211
  case '*':
2063
2212
  return left * right;
2064
2213
  case '/':
2214
+ if (right === 0)
2215
+ return null;
2065
2216
  return left / right;
2066
2217
  case '%':
2218
+ if (right === 0)
2219
+ return null;
2067
2220
  return left % right;
2068
2221
  // Comparison - using == and != intentionally for loose equality
2069
2222
  case '==':
@@ -2082,11 +2235,6 @@ class Evaluator {
2082
2235
  return left >= right;
2083
2236
  case '<=':
2084
2237
  return left <= right;
2085
- // Logical
2086
- case '&&':
2087
- return left && right;
2088
- case '||':
2089
- return left || right;
2090
2238
  default:
2091
2239
  throw new ExpressionParserError(`[Dynamic Forms] Unknown binary operator: ${node.operator}`, 0, this.expression);
2092
2240
  }
@@ -2100,8 +2248,6 @@ class Evaluator {
2100
2248
  return -operand;
2101
2249
  case '+':
2102
2250
  return +operand;
2103
- case 'typeof':
2104
- return typeof operand;
2105
2251
  default:
2106
2252
  throw new ExpressionParserError(`[Dynamic Forms] Unknown unary operator: ${node.operator}`, 0, this.expression);
2107
2253
  }
@@ -2117,7 +2263,7 @@ class Evaluator {
2117
2263
  if (BLOCKED_PROPERTIES.has(methodName)) {
2118
2264
  throw new ExpressionParserError(`[Dynamic Forms] Property "${methodName}" is not accessible for security reasons`, 0, this.expression);
2119
2265
  }
2120
- // Check if the method is in the whitelist
2266
+ // Check if the method is in the instance method whitelist
2121
2267
  if (!this.isMethodSafe(obj, methodName)) {
2122
2268
  throw new ExpressionParserError(`[Dynamic Forms] Method "${methodName}" is not allowed for security reasons`, 0, this.expression);
2123
2269
  }
@@ -2299,7 +2445,7 @@ function evaluateJavaScriptExpression(expression, context) {
2299
2445
  return !!result;
2300
2446
  }
2301
2447
  catch (error) {
2302
- console.error('[Dynamic Forms] Error evaluating JavaScript expression:', expression.expression, error);
2448
+ context.logger.error('Error evaluating JavaScript expression:', expression.expression, error);
2303
2449
  return false;
2304
2450
  }
2305
2451
  }
@@ -2308,14 +2454,14 @@ function evaluateCustomFunction(expression, context) {
2308
2454
  return false;
2309
2455
  const customFn = context.customFunctions?.[expression.expression];
2310
2456
  if (!customFn) {
2311
- console.error('[Dynamic Forms] Custom function not found:', expression.expression);
2457
+ context.logger.error('Custom function not found:', expression.expression);
2312
2458
  return false;
2313
2459
  }
2314
2460
  try {
2315
2461
  return !!customFn(context);
2316
2462
  }
2317
2463
  catch (error) {
2318
- console.error('[Dynamic Forms] Error executing custom function:', expression.expression, error);
2464
+ context.logger.error('Error executing custom function:', expression.expression, error);
2319
2465
  return false;
2320
2466
  }
2321
2467
  }
@@ -2357,23 +2503,54 @@ function createLogicFunction(expression) {
2357
2503
  }
2358
2504
 
2359
2505
  /**
2360
- * Create a dynamic value function from an expression string
2361
- * Uses secure AST-based parsing instead of eval() or new Function()
2506
+ * Cache for memoized dynamic value functions, keyed by service instance.
2507
+ * Uses WeakMap to automatically clean up when service is garbage collected.
2508
+ * Inner Map is keyed by expression string.
2509
+ */
2510
+ const dynamicValueFunctionCache = new WeakMap();
2511
+ /**
2512
+ * Create a dynamic value function from an expression string.
2513
+ * Uses secure AST-based parsing instead of eval() or new Function().
2514
+ *
2515
+ * Optimizations:
2516
+ * - Injects FieldContextRegistryService at creation time, not execution time
2517
+ * - Caches created functions by expression string per injection context
2518
+ * - Expression parsing is already cached by ExpressionParser's LRU cache
2519
+ *
2520
+ * @param expression The expression string to evaluate
2521
+ * @returns A LogicFn that evaluates the expression in the context of a field
2362
2522
  */
2363
2523
  function createDynamicValueFunction(expression) {
2364
- return (ctx) => {
2524
+ // Inject services during factory creation, not during execution
2525
+ // This captures the service instance in the closure
2526
+ const fieldContextRegistry = inject(FieldContextRegistryService);
2527
+ const logger = inject(DYNAMIC_FORM_LOGGER);
2528
+ // Get or create cache for this injection context
2529
+ let contextCache = dynamicValueFunctionCache.get(fieldContextRegistry);
2530
+ if (!contextCache) {
2531
+ contextCache = new Map();
2532
+ dynamicValueFunctionCache.set(fieldContextRegistry, contextCache);
2533
+ }
2534
+ // Check cache first
2535
+ const cached = contextCache.get(expression);
2536
+ if (cached) {
2537
+ return cached;
2538
+ }
2539
+ const fn = (ctx) => {
2365
2540
  // Create evaluation context using the registry-based approach
2366
- const fieldContextRegistry = inject(FieldContextRegistryService);
2367
2541
  const evaluationContext = fieldContextRegistry.createEvaluationContext(ctx);
2368
2542
  try {
2369
- // Use secure AST-based expression parser
2543
+ // Use secure AST-based expression parser (already has LRU cache)
2370
2544
  return ExpressionParser.evaluate(expression, evaluationContext);
2371
2545
  }
2372
2546
  catch (error) {
2373
- console.error('[Dynamic Forms] Error evaluating dynamic expression:', expression, error);
2547
+ logger.error('Error evaluating dynamic expression:', expression, error);
2374
2548
  return undefined;
2375
2549
  }
2376
2550
  };
2551
+ // Cache the function
2552
+ contextCache.set(expression, fn);
2553
+ return fn;
2377
2554
  }
2378
2555
 
2379
2556
  /**
@@ -2601,136 +2778,98 @@ function isCrossFieldSchema(config, context) {
2601
2778
  return isCrossFieldExpression(config.condition, context);
2602
2779
  }
2603
2780
 
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;
2781
+ function applyEmailValidator(path) {
2782
+ email(path);
2611
2783
  }
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;
2784
+ function applyMinValidator(path, value, expression) {
2785
+ if (expression) {
2786
+ min(path, createDynamicValueFunction(expression));
2787
+ }
2788
+ else {
2789
+ min(path, value);
2790
+ }
2627
2791
  }
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;
2792
+ function applyMaxValidator(path, value, expression) {
2793
+ if (expression) {
2794
+ max(path, createDynamicValueFunction(expression));
2654
2795
  }
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
2796
+ else {
2797
+ max(path, value);
2798
+ }
2799
+ }
2800
+ function applyMinLengthValidator(path, value, expression) {
2801
+ if (expression) {
2802
+ minLength(path, createDynamicValueFunction(expression));
2803
+ }
2804
+ else {
2805
+ minLength(path, value);
2806
+ }
2807
+ }
2808
+ function applyMaxLengthValidator(path, value, expression) {
2809
+ if (expression) {
2810
+ maxLength(path, createDynamicValueFunction(expression));
2811
+ }
2812
+ else {
2813
+ maxLength(path, value);
2814
+ }
2815
+ }
2816
+ function applyPatternValidator(path, value, expression) {
2817
+ if (expression) {
2818
+ pattern(path, createDynamicValueFunction(expression));
2819
+ }
2820
+ else {
2821
+ pattern(path, value);
2822
+ }
2823
+ }
2824
+ function createConditionalLogic(when) {
2825
+ return when ? createLogicFunction(when) : undefined;
2826
+ }
2827
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic forms require any at the Angular API boundary
2828
+ function applyValidator(config, fieldPath) {
2829
+ const path = fieldPath;
2830
+ if (isCrossFieldBuiltInValidator(config) || hasCrossFieldWhenCondition(config)) {
2659
2831
  return;
2660
2832
  }
2661
2833
  switch (config.type) {
2662
2834
  case 'required':
2663
2835
  if (config.when) {
2664
- const whenLogic = createLogicFunction(config.when);
2665
- required(path, { when: whenLogic });
2836
+ required(path, { when: createLogicFunction(config.when) });
2666
2837
  }
2667
2838
  else {
2668
2839
  required(path);
2669
2840
  }
2670
2841
  break;
2671
2842
  case 'email':
2672
- email(fieldPath);
2843
+ applyEmailValidator(fieldPath);
2673
2844
  break;
2674
2845
  case 'min':
2675
2846
  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
- }
2847
+ applyMinValidator(fieldPath, config.value, config.expression);
2683
2848
  }
2684
2849
  break;
2685
2850
  case 'max':
2686
2851
  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
- }
2852
+ applyMaxValidator(fieldPath, config.value, config.expression);
2694
2853
  }
2695
2854
  break;
2696
2855
  case 'minLength':
2697
2856
  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
- }
2857
+ applyMinLengthValidator(fieldPath, config.value, config.expression);
2705
2858
  }
2706
2859
  break;
2707
2860
  case 'maxLength':
2708
2861
  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
- }
2862
+ applyMaxLengthValidator(fieldPath, config.value, config.expression);
2717
2863
  }
2718
2864
  break;
2719
2865
  case 'pattern':
2720
2866
  if (config.value instanceof RegExp || typeof config.value === 'string') {
2721
2867
  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
- }
2868
+ applyPatternValidator(fieldPath, regexPattern, config.expression);
2730
2869
  }
2731
2870
  break;
2732
2871
  case 'custom':
2733
- applyCustomValidator(config, path, fieldKey);
2872
+ applyCustomValidator(config, path);
2734
2873
  break;
2735
2874
  case 'customAsync':
2736
2875
  applyAsyncValidator(config, path);
@@ -2740,95 +2879,70 @@ function applyValidator(config, fieldPath, fieldKey) {
2740
2879
  break;
2741
2880
  }
2742
2881
  }
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
2882
+ function applyCustomValidator(config, fieldPath) {
2756
2883
  if (isCrossFieldValidator(config)) {
2757
- // Cross-field validators are handled at form level via validateTree
2758
2884
  return;
2759
2885
  }
2760
- // Non-cross-field validators: apply at field level as before
2761
2886
  let validatorFn;
2762
2887
  if (config.expression) {
2763
- // Expression-based validator
2764
2888
  validatorFn = createExpressionValidator(config);
2765
2889
  }
2766
2890
  else if (config.functionName) {
2767
- // Function-based validator
2768
2891
  validatorFn = createFunctionValidator(config);
2769
2892
  }
2770
2893
  else {
2771
- console.warn('[Dynamic Forms] Custom validator must have either "expression" or "functionName"');
2894
+ const logger = inject(DYNAMIC_FORM_LOGGER);
2895
+ logger.warn('Custom validator must have either "expression" or "functionName"');
2772
2896
  return;
2773
2897
  }
2774
- // Apply with conditional logic if specified
2775
2898
  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
- }
2899
+ validate(fieldPath, (ctx) => {
2900
+ if (whenLogic && !whenLogic(ctx))
2901
+ return null;
2902
+ return validatorFn(ctx);
2903
+ });
2788
2904
  }
2789
- /**
2790
- * Create a function-based validator using registered validator functions
2791
- */
2792
2905
  function createFunctionValidator(config) {
2906
+ const logger = inject(DYNAMIC_FORM_LOGGER);
2907
+ const functionName = config.functionName;
2908
+ if (!functionName) {
2909
+ logger.warn('Custom validator missing functionName');
2910
+ return () => null;
2911
+ }
2793
2912
  const registry = inject(FunctionRegistryService);
2794
- const validatorFn = registry.getValidator(config.functionName);
2913
+ const validatorFn = registry.getValidator(functionName);
2795
2914
  if (!validatorFn) {
2796
- console.warn(`[Dynamic Forms] Custom validator "${config.functionName}" not found in registry. ` +
2915
+ logger.warn(`Custom validator "${functionName}" not found in registry. ` +
2797
2916
  `Ensure it's registered using customFnConfig.validators or check the function name for typos.`);
2798
2917
  return () => null;
2799
2918
  }
2800
- return (ctx) => {
2801
- return validatorFn(ctx, config.params);
2802
- };
2919
+ return (ctx) => validatorFn(ctx, config.params);
2803
2920
  }
2804
- /**
2805
- * Create an expression-based validator using secure AST evaluation
2806
- */
2807
2921
  function createExpressionValidator(config) {
2922
+ const logger = inject(DYNAMIC_FORM_LOGGER);
2923
+ const expression = config.expression;
2924
+ if (!expression) {
2925
+ logger.warn('Custom validator missing expression');
2926
+ return () => null;
2927
+ }
2808
2928
  const fieldContextRegistry = inject(FieldContextRegistryService);
2809
2929
  const functionRegistry = inject(FunctionRegistryService);
2810
2930
  return (ctx) => {
2811
2931
  try {
2812
- // Create evaluation context from FieldContext
2813
2932
  const evaluationContext = fieldContextRegistry.createEvaluationContext(ctx, functionRegistry.getCustomFunctions());
2814
- // Evaluate expression securely using AST parser
2815
2933
  const result = ExpressionParser.evaluate(config.expression, evaluationContext);
2816
- // If expression returns truthy, validation passes (no error)
2817
- // If expression returns falsy, validation fails
2818
2934
  if (result) {
2819
- return null; // Validation passes
2935
+ return null;
2820
2936
  }
2821
- // Validation failed - return error with kind
2822
2937
  const kind = config.kind || 'custom';
2823
2938
  const validationError = { kind };
2824
- // Evaluate and include errorParams for message interpolation
2825
2939
  if (config.errorParams) {
2826
2940
  Object.entries(config.errorParams).forEach(([key, expression]) => {
2827
2941
  try {
2828
2942
  validationError[key] = ExpressionParser.evaluate(expression, evaluationContext);
2829
2943
  }
2830
2944
  catch (err) {
2831
- console.warn(`[Dynamic Forms] Error evaluating errorParam "${key}":`, expression, err);
2945
+ logger.warn(`Error evaluating errorParam "${key}":`, expression, err);
2832
2946
  }
2833
2947
  });
2834
2948
  }
@@ -2836,8 +2950,8 @@ function createExpressionValidator(config) {
2836
2950
  }
2837
2951
  catch (error) {
2838
2952
  // 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);
2953
+ // Log for debugging while keeping form functional
2954
+ logger.error('Error evaluating custom validator expression:', expression, error);
2841
2955
  return { kind: config.kind || 'custom' };
2842
2956
  }
2843
2957
  };
@@ -2852,42 +2966,26 @@ function createExpressionValidator(config) {
2852
2966
  * - onError: Optional handler for resource errors
2853
2967
  */
2854
2968
  function applyAsyncValidator(config, fieldPath) {
2969
+ const logger = inject(DYNAMIC_FORM_LOGGER);
2855
2970
  const registry = inject(FunctionRegistryService);
2856
- // Get async validator config from registry
2857
2971
  const validatorConfig = registry.getAsyncValidator(config.functionName);
2858
2972
  if (!validatorConfig) {
2859
- console.warn(`[Dynamic Forms] Async validator "${config.functionName}" not found in registry. ` +
2973
+ logger.warn(`Async validator "${config.functionName}" not found in registry. ` +
2860
2974
  `Ensure it's registered using customFnConfig.asyncValidators or check the function name for typos.`);
2861
2975
  return;
2862
2976
  }
2863
- // Build Angular's AsyncValidatorOptions
2977
+ const whenLogic = createConditionalLogic(config.when);
2864
2978
  const asyncOptions = {
2865
2979
  params: (ctx) => {
2980
+ if (whenLogic && !whenLogic(ctx))
2981
+ return undefined;
2866
2982
  return validatorConfig.params(ctx, config.params);
2867
2983
  },
2868
2984
  factory: validatorConfig.factory,
2869
2985
  onSuccess: validatorConfig.onSuccess,
2870
2986
  onError: validatorConfig.onError,
2871
2987
  };
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
- }
2988
+ validateAsync(fieldPath, asyncOptions);
2891
2989
  }
2892
2990
  /**
2893
2991
  * Apply HTTP validator to field path using Angular's public validateHttp() API
@@ -2898,88 +2996,40 @@ function applyAsyncValidator(config, fieldPath) {
2898
2996
  * - onError: Optional handler for HTTP errors
2899
2997
  */
2900
2998
  function applyHttpValidator(config, fieldPath) {
2999
+ const logger = inject(DYNAMIC_FORM_LOGGER);
2901
3000
  const registry = inject(FunctionRegistryService);
2902
- // Get HTTP validator config from registry
2903
3001
  const httpValidatorConfig = registry.getHttpValidator(config.functionName);
2904
3002
  if (!httpValidatorConfig) {
2905
- console.warn(`[Dynamic Forms] HTTP validator "${config.functionName}" not found in registry. ` +
3003
+ logger.warn(`HTTP validator "${config.functionName}" not found in registry. ` +
2906
3004
  `Ensure it's registered using customFnConfig.httpValidators or check the function name for typos.`);
2907
3005
  return;
2908
3006
  }
2909
- // Build Angular's HttpValidatorOptions
3007
+ const whenLogic = createConditionalLogic(config.when);
2910
3008
  const httpOptions = {
2911
3009
  request: (ctx) => {
3010
+ if (whenLogic && !whenLogic(ctx))
3011
+ return undefined;
2912
3012
  return httpValidatorConfig.request(ctx, config.params);
2913
3013
  },
2914
3014
  onSuccess: httpValidatorConfig.onSuccess,
2915
3015
  onError: httpValidatorConfig.onError,
2916
3016
  };
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
- }
3017
+ validateHttp(fieldPath, httpOptions);
2936
3018
  }
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));
3019
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic forms require any at the Angular API boundary
3020
+ function applyValidators(configs, fieldPath) {
3021
+ configs.forEach((config) => applyValidator(config, fieldPath));
2946
3022
  }
2947
3023
 
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
3024
  function applyLogic(config, fieldPath) {
2967
- const path = toSupportedPath$1(fieldPath);
2968
- // Handle static boolean conditions
3025
+ const path = fieldPath;
2969
3026
  if (typeof config.condition === 'boolean') {
2970
- const staticLogicFn = () => config.condition;
2971
- applyLogicFn(config.type, path, staticLogicFn);
3027
+ applyLogicFn(config.type, path, () => config.condition);
2972
3028
  return;
2973
3029
  }
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);
3030
+ const logicFn = createLogicFunction(config.condition);
2978
3031
  applyLogicFn(config.type, path, logicFn);
2979
3032
  }
2980
- /**
2981
- * Applies a logic function to a path for a specific logic type.
2982
- */
2983
3033
  function applyLogicFn(type, path, logicFn) {
2984
3034
  switch (type) {
2985
3035
  case 'hidden':
@@ -2996,12 +3046,6 @@ function applyLogicFn(type, path, logicFn) {
2996
3046
  break;
2997
3047
  }
2998
3048
  }
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
3049
  function applyMultipleLogic(configs, fieldPath) {
3006
3050
  configs.forEach((config) => applyLogic(config, fieldPath));
3007
3051
  }
@@ -3049,16 +3093,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImpor
3049
3093
  }] });
3050
3094
 
3051
3095
  /**
3052
- * Create a type predicate function from a predicate string
3096
+ * Create a type predicate function from a predicate string.
3097
+ *
3098
+ * Uses the secure ExpressionParser with whitelist-based evaluation instead of
3099
+ * new Function() / eval. This provides security through:
3100
+ * - AST-based parsing (no code execution)
3101
+ * - Whitelisted operators only
3102
+ * - Whitelisted methods only (Array.isArray, typeof, instanceof, etc.)
3103
+ * - No access to global scope beyond whitelisted constructors
3104
+ *
3105
+ * @param predicate - A JavaScript expression that evaluates to boolean. The value to check is available as `value`.
3106
+ * @param logger - Logger for error reporting
3107
+ * @returns A type predicate function
3108
+ *
3109
+ * @example
3110
+ * createTypePredicateFunction('typeof value === "string"')
3111
+ * createTypePredicateFunction('Array.isArray(value)')
3112
+ * createTypePredicateFunction('value instanceof Date')
3053
3113
  */
3054
- function createTypePredicateFunction(predicate) {
3114
+ function createTypePredicateFunction(predicate, logger) {
3055
3115
  return (value) => {
3056
3116
  try {
3057
- const func = new Function('value', `return ${predicate};`);
3058
- return func(value);
3117
+ // Evaluate using secure AST-based parser with value in scope
3118
+ const result = ExpressionParser.evaluate(predicate, { value });
3119
+ return Boolean(result);
3059
3120
  }
3060
3121
  catch (error) {
3061
- console.error('[Dynamic Forms] Error evaluating type predicate:', predicate, error);
3122
+ logger.error('Error evaluating type predicate:', predicate, error);
3062
3123
  return false;
3063
3124
  }
3064
3125
  };
@@ -3074,11 +3135,12 @@ function createTypePredicateFunction(predicate) {
3074
3135
  * 3. The actual schema application happens at runtime via the schema function
3075
3136
  */
3076
3137
  function applySchema(config, fieldPath) {
3138
+ const logger = inject(DYNAMIC_FORM_LOGGER);
3077
3139
  const schemaRegistry = inject(SchemaRegistryService);
3078
3140
  const schema = schemaRegistry.resolveSchema(config.schema);
3079
3141
  if (!schema) {
3080
3142
  const availableSchemas = Array.from(schemaRegistry.getAllSchemas().keys()).join(', ') || '<none>';
3081
- console.error(`[Dynamic Forms] Schema not found: '${config.schema}'. ` +
3143
+ logger.error(`Schema not found: '${config.schema}'. ` +
3082
3144
  `Available schemas: ${availableSchemas}. ` +
3083
3145
  `Ensure the schema is registered in your schema registry.`);
3084
3146
  return;
@@ -3098,7 +3160,7 @@ function applySchema(config, fieldPath) {
3098
3160
  break;
3099
3161
  case 'applyWhenValue':
3100
3162
  if (config.typePredicate) {
3101
- const predicate = createTypePredicateFunction(config.typePredicate);
3163
+ const predicate = createTypePredicateFunction(config.typePredicate, logger);
3102
3164
  applyWhenValue(path, predicate, schemaFn);
3103
3165
  }
3104
3166
  break;
@@ -3193,259 +3255,208 @@ function hasNestedPages(fields) {
3193
3255
  }
3194
3256
 
3195
3257
  /**
3196
- * Safely cast a SchemaPathTree to SchemaPath with Supported rules.
3197
- * See validator-factory.ts for detailed explanation of why this is safe.
3258
+ * Applies string-specific validators to a schema path.
3259
+ * This function is properly typed - no casts needed internally.
3260
+ */
3261
+ function applyStringValidators(path, config) {
3262
+ if (config.email) {
3263
+ email(path);
3264
+ }
3265
+ if (config.minLength !== undefined) {
3266
+ minLength(path, config.minLength);
3267
+ }
3268
+ if (config.maxLength !== undefined) {
3269
+ maxLength(path, config.maxLength);
3270
+ }
3271
+ if (config.pattern) {
3272
+ const regex = typeof config.pattern === 'string' ? new RegExp(config.pattern) : config.pattern;
3273
+ pattern(path, regex);
3274
+ }
3275
+ }
3276
+ /**
3277
+ * Applies number-specific validators to a schema path.
3278
+ * This function is properly typed - no casts needed internally.
3279
+ */
3280
+ function applyNumberValidators(path, config) {
3281
+ if (config.min !== undefined) {
3282
+ min(path, config.min);
3283
+ }
3284
+ if (config.max !== undefined) {
3285
+ max(path, config.max);
3286
+ }
3287
+ }
3288
+ /**
3289
+ * Extracts string validation config from a field definition.
3290
+ * Returns undefined if no string validators are configured.
3291
+ */
3292
+ function getStringValidationConfig(fieldDef) {
3293
+ if (!fieldDef.email && fieldDef.minLength === undefined && fieldDef.maxLength === undefined && !fieldDef.pattern) {
3294
+ return undefined;
3295
+ }
3296
+ return {
3297
+ email: fieldDef.email,
3298
+ minLength: fieldDef.minLength,
3299
+ maxLength: fieldDef.maxLength,
3300
+ pattern: fieldDef.pattern,
3301
+ };
3302
+ }
3303
+ /**
3304
+ * Extracts number validation config from a field definition.
3305
+ * Handles both 'min'/'max' (standard) and 'minValue'/'maxValue' (SliderField).
3306
+ * Returns undefined if no number validators are configured.
3198
3307
  */
3199
- function toSupportedPath(path) {
3200
- return path;
3308
+ function getNumberValidationConfig(fieldDef) {
3309
+ const minVal = fieldDef.min ?? fieldDef.minValue;
3310
+ const maxVal = fieldDef.max ?? fieldDef.maxValue;
3311
+ if (minVal === undefined && maxVal === undefined) {
3312
+ return undefined;
3313
+ }
3314
+ return { min: minVal, max: maxVal };
3201
3315
  }
3202
3316
  /**
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
3317
+ * Maps a field definition to the Angular Signal Forms schema.
3205
3318
  *
3206
- * Cross-field logic (formValue.*) is handled automatically by createLogicFunction
3207
- * which uses RootFormRegistryService. No special context needed.
3319
+ * This is the main entry point that should be called from the dynamic form component.
3320
+ * It handles all field types: leaf fields, containers (page, row, group), and arrays.
3208
3321
  *
3209
- * Cross-field validators are skipped at field level - they are collected and
3210
- * applied at form level using validateTree.
3322
+ * Cross-field logic (formValue.*) is handled automatically by createLogicFunction
3323
+ * which uses RootFormRegistryService.
3211
3324
  *
3212
- * @param fieldDef The field definition to map
3213
- * @param fieldPath The Angular Signal Forms schema path
3325
+ * Cross-field validators are skipped at field level and applied at form level via validateTree.
3214
3326
  */
3215
3327
  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);
3328
+ // Layout containers (page, row) - flatten children to current level
3329
+ if (isPageField(fieldDef) || isRowField(fieldDef)) {
3330
+ mapContainerChildren(fieldDef.fields, fieldPath);
3221
3331
  return;
3222
3332
  }
3223
- // Special handling for row fields - flatten child fields to root level (like pages)
3224
- if (isRowField(fieldDef)) {
3225
- mapRowFieldToForm(fieldDef, fieldPath);
3226
- return;
3227
- }
3228
- // Special handling for group fields - apply child field validations to the parent form schema
3333
+ // Group fields - map children under the group's path
3229
3334
  if (isGroupField(fieldDef)) {
3230
- mapGroupFieldToForm(fieldDef, fieldPath);
3335
+ mapContainerChildren(fieldDef.fields, fieldPath);
3231
3336
  return;
3232
3337
  }
3233
- // Special handling for array fields - apply child field validations to the parent form schema
3338
+ // Array fields - use applyEach for item schema
3234
3339
  if (isArrayField(fieldDef)) {
3235
3340
  mapArrayFieldToForm(fieldDef, fieldPath);
3236
3341
  return;
3237
3342
  }
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
3343
+ // Leaf field - apply validation, logic, and configuration
3344
+ mapLeafField(fieldDef, fieldPath);
3345
+ }
3346
+ /**
3347
+ * Maps children of a container field (page, row, group) to the form schema.
3348
+ */
3349
+ function mapContainerChildren(fields, parentPath) {
3350
+ if (!fields)
3351
+ return;
3352
+ const pathRecord = parentPath;
3353
+ for (const field of fields) {
3354
+ if (!field.key)
3355
+ continue;
3356
+ const childPath = pathRecord[field.key];
3357
+ if (childPath) {
3358
+ mapFieldToForm(field, childPath);
3359
+ }
3360
+ }
3361
+ }
3362
+ /**
3363
+ * Maps a leaf field (value-bearing field) to the form schema.
3364
+ */
3365
+ function mapLeafField(fieldDef, fieldPath) {
3366
+ const validationField = fieldDef;
3367
+ const path = fieldPath;
3368
+ // Apply simple validation rules from field properties
3369
+ applySimpleValidationRules(validationField, path);
3243
3370
  if (validationField.validators) {
3244
- validationField.validators.forEach((validatorConfig) => {
3245
- applyValidator(validatorConfig, fieldPath, fieldDef.key);
3246
- });
3371
+ for (const config of validationField.validators) {
3372
+ applyValidator(config, fieldPath);
3373
+ }
3247
3374
  }
3248
- // Apply logic if it exists
3249
- // Cross-field logic (formValue.*) is handled automatically by createLogicFunction
3375
+ // Apply logic rules
3250
3376
  if (validationField.logic) {
3251
- validationField.logic.forEach((logicConfig) => {
3252
- applyLogic(logicConfig, fieldPath);
3253
- });
3377
+ for (const config of validationField.logic) {
3378
+ applyLogic(config, fieldPath);
3379
+ }
3254
3380
  }
3255
- // Apply schemas if they exist
3381
+ // Apply schemas
3256
3382
  if (validationField.schemas) {
3257
- validationField.schemas.forEach((schemaConfig) => {
3258
- applySchema(schemaConfig, fieldPath);
3259
- });
3383
+ for (const config of validationField.schemas) {
3384
+ applySchema(config, fieldPath);
3385
+ }
3260
3386
  }
3261
- // Handle any additional field-specific configuration
3262
- mapFieldSpecificConfiguration(fieldDef, fieldPath);
3387
+ // Apply field state configuration
3388
+ applyFieldState(fieldDef, path);
3263
3389
  }
3264
3390
  /**
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
3391
+ * Applies simple validation rules from field properties.
3392
+ * Casts are isolated to the boundary between untyped field definitions and typed validator functions.
3274
3393
  */
3275
- function applySimpleValidationRules(fieldDef, fieldPath) {
3276
- const path = toSupportedPath(fieldPath);
3277
- // Required validator
3394
+ function applySimpleValidationRules(fieldDef, path) {
3278
3395
  if (fieldDef.required) {
3279
3396
  required(path);
3280
3397
  }
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);
3302
- }
3303
- if (fieldDef.maxLength !== undefined) {
3304
- // MaxLength validator expects SchemaPath<string>
3305
- maxLength(path, fieldDef.maxLength);
3398
+ // String validators - single cast at boundary
3399
+ const stringConfig = getStringValidationConfig(fieldDef);
3400
+ if (stringConfig) {
3401
+ applyStringValidators(path, stringConfig);
3306
3402
  }
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);
3403
+ // Number validators - single cast at boundary
3404
+ const numberConfig = getNumberValidationConfig(fieldDef);
3405
+ if (numberConfig) {
3406
+ applyNumberValidators(path, numberConfig);
3312
3407
  }
3313
3408
  }
3314
3409
  /**
3315
- * Handle field-specific configuration that doesn't fit into validators/logic/schemas
3410
+ * Applies field state configuration (disabled, readonly, hidden).
3316
3411
  */
3317
- function mapFieldSpecificConfiguration(fieldDef, fieldPath) {
3318
- // Handle disabled state
3412
+ function applyFieldState(fieldDef, path) {
3319
3413
  if (fieldDef.disabled) {
3320
- disabled(toSupportedPath(fieldPath));
3414
+ disabled(path);
3321
3415
  }
3322
- // Handle readonly state
3323
3416
  if (fieldDef.readonly) {
3324
- readonly(toSupportedPath(fieldPath));
3417
+ readonly(path);
3325
3418
  }
3326
- // Handle hidden state (hidden() requires a logic function, so we provide a static true)
3327
3419
  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
3420
+ hidden(path, () => true);
3336
3421
  }
3337
3422
  }
3338
3423
  /**
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
3424
+ * Maps an array field to the form schema using applyEach.
3425
+ *
3426
+ * Array items use a template field definition. applyEach creates proper
3427
+ * FieldTree structure for each array item.
3342
3428
  */
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
- }
3429
+ function mapArrayFieldToForm(arrayField, fieldPath) {
3430
+ if (!isArrayField(arrayField) || !arrayField.fields?.length) {
3431
+ return;
3353
3432
  }
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]
3402
- */
3403
- function mapArrayFieldToForm(arrayField, fieldPath) {
3404
- if (!isArrayField(arrayField) || !arrayField.fields || arrayField.fields.length === 0) {
3405
- return;
3406
- }
3407
- // Get the template field (first field in array.fields)
3408
3433
  const templateField = arrayField.fields[0];
3409
- // Create an item schema from the template field
3410
3434
  const itemSchema = schema((itemPath) => {
3411
- // Handle container fields (row, group) that have nested fields
3435
+ const pathRecord = itemPath;
3412
3436
  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
- }
3437
+ // Row/page templates flatten their children
3438
+ mapContainerChildren(templateField.fields, itemPath);
3424
3439
  }
3425
3440
  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
- }
3441
+ // Group template - access group's path first
3442
+ const groupKey = templateField.key;
3443
+ if (groupKey) {
3444
+ const groupPath = pathRecord[groupKey];
3445
+ if (groupPath) {
3446
+ mapContainerChildren(templateField.fields, groupPath);
3435
3447
  }
3436
3448
  }
3449
+ else {
3450
+ // No group key - apply children directly (edge case)
3451
+ mapContainerChildren(templateField.fields, itemPath);
3452
+ }
3437
3453
  }
3438
3454
  else {
3439
- // Simple field template - apply validation directly
3455
+ // Simple field template
3440
3456
  mapFieldToForm(templateField, itemPath);
3441
3457
  }
3442
3458
  });
3443
- // Apply the item schema to each array element
3444
3459
  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
3460
  }
3450
3461
 
3451
3462
  /**
@@ -3531,10 +3542,6 @@ function getFieldDefaultValue(field, registry) {
3531
3542
  }
3532
3543
  return groupDefaults;
3533
3544
  }
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
3545
  // Use explicit value if provided, with type-specific handling for null
3539
3546
  if ('value' in field) {
3540
3547
  // If value is explicitly set (even to null/undefined), respect it
@@ -3603,7 +3610,75 @@ function isDisplayOnlyField(field) {
3603
3610
  return field.type === 'text' || isContainerField(field);
3604
3611
  }
3605
3612
 
3613
+ /**
3614
+ * Type guard to check if a value is a DynamicFormFeature
3615
+ */
3616
+ function isDynamicFormFeature(value) {
3617
+ return (typeof value === 'object' &&
3618
+ value !== null &&
3619
+ 'ɵkind' in value &&
3620
+ typeof value.ɵkind === 'string' &&
3621
+ 'ɵproviders' in value &&
3622
+ Array.isArray(value.ɵproviders));
3623
+ }
3624
+ /**
3625
+ * Helper to create a feature with proper typing
3626
+ */
3627
+ function createFeature(kind, providers) {
3628
+ return {
3629
+ ɵkind: kind,
3630
+ ɵproviders: providers,
3631
+ };
3632
+ }
3633
+
3634
+ /**
3635
+ * Configure logging for dynamic forms.
3636
+ *
3637
+ * @example
3638
+ * ```typescript
3639
+ * // Set log level
3640
+ * provideDynamicForm(
3641
+ * ...withMaterialFields(),
3642
+ * withLogger({ level: LogLevel.Debug })
3643
+ * )
3644
+ *
3645
+ * // Disable logging entirely
3646
+ * provideDynamicForm(
3647
+ * ...withMaterialFields(),
3648
+ * withLogger({ level: LogLevel.Off })
3649
+ * )
3650
+ *
3651
+ * // Custom logger implementation
3652
+ * provideDynamicForm(
3653
+ * ...withMaterialFields(),
3654
+ * withLogger({ logger: mySentryLogger })
3655
+ * )
3656
+ * ```
3657
+ *
3658
+ * @param options - Logger configuration options
3659
+ * @returns A DynamicFormFeature that configures logging
3660
+ */
3661
+ function withLogger(options = {}) {
3662
+ let logger;
3663
+ if (options.logger) {
3664
+ // Use custom logger implementation
3665
+ logger = options.logger;
3666
+ }
3667
+ else if (options.level === LogLevel.Off) {
3668
+ // Explicit disable - use noop
3669
+ logger = new NoopLogger();
3670
+ }
3671
+ else {
3672
+ // Use console logger with specified level (default to Warn)
3673
+ logger = new ConsoleLogger(options.level ?? LogLevel.Warn);
3674
+ }
3675
+ return createFeature('logger', [{ provide: DYNAMIC_FORM_LOGGER, useValue: logger }]);
3676
+ }
3677
+
3606
3678
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3679
+ function getFieldTreeByKey(ctx, key) {
3680
+ return ctx.field[key];
3681
+ }
3607
3682
  /**
3608
3683
  * Creates an Angular signal forms schema from field definitions
3609
3684
  * This is the single entry point at dynamic form level that replaces createSchemaFromFields
@@ -3622,6 +3697,7 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3622
3697
  // Inject services for cross-field validation
3623
3698
  // These will be available because createSchemaFromFields is called within runInInjectionContext
3624
3699
  const functionRegistry = inject(FunctionRegistryService);
3700
+ const logger = inject(DYNAMIC_FORM_LOGGER);
3625
3701
  return schema((path) => {
3626
3702
  for (const fieldDef of fields) {
3627
3703
  const valueHandling = getFieldValueHandling(fieldDef.type, registry);
@@ -3631,9 +3707,7 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3631
3707
  continue;
3632
3708
  }
3633
3709
  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) {
3710
+ for (const childField of normalizeFieldsArray(fieldDef.fields)) {
3637
3711
  if (!childField.key)
3638
3712
  continue;
3639
3713
  const childPath = path[childField.key];
@@ -3655,7 +3729,7 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3655
3729
  }
3656
3730
  // Apply cross-field validators using validateTree
3657
3731
  if (crossFieldValidators && crossFieldValidators.length > 0) {
3658
- applyCrossFieldTreeValidator(path, crossFieldValidators, functionRegistry);
3732
+ applyCrossFieldTreeValidator(path, crossFieldValidators, functionRegistry, logger);
3659
3733
  }
3660
3734
  });
3661
3735
  }
@@ -3676,8 +3750,9 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3676
3750
  * @param rootPath The root schema path tree
3677
3751
  * @param validators Array of collected cross-field validators
3678
3752
  * @param functionRegistry Registry containing custom functions for expression evaluation
3753
+ * @param logger Logger for error reporting
3679
3754
  */
3680
- function applyCrossFieldTreeValidator(rootPath, validators, functionRegistry) {
3755
+ function applyCrossFieldTreeValidator(rootPath, validators, functionRegistry, logger) {
3681
3756
  // Get custom functions for expression evaluation
3682
3757
  const customFunctions = functionRegistry.getCustomFunctions();
3683
3758
  validateTree(rootPath, (ctx) => {
@@ -3696,10 +3771,9 @@ function applyCrossFieldTreeValidator(rootPath, validators, functionRegistry) {
3696
3771
  }
3697
3772
  }
3698
3773
  catch (err) {
3699
- console.error(`[Dynamic Forms] Error evaluating cross-field validator for ${sourceFieldKey}:`, err);
3774
+ logger.error(`Error evaluating cross-field validator for ${sourceFieldKey}:`, err);
3700
3775
  // 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];
3776
+ const targetField = getFieldTreeByKey(ctx, sourceFieldKey);
3703
3777
  if (targetField) {
3704
3778
  const customConfig = config;
3705
3779
  errors.push({
@@ -3750,6 +3824,7 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3750
3824
  formValue,
3751
3825
  fieldPath: sourceFieldKey,
3752
3826
  customFunctions: evaluationContext.customFunctions || {},
3827
+ logger: evaluationContext.logger,
3753
3828
  });
3754
3829
  if (!conditionMet) {
3755
3830
  return null; // Condition not met, skip validation
@@ -3762,8 +3837,7 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3762
3837
  return null;
3763
3838
  }
3764
3839
  // 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];
3840
+ const targetField = getFieldTreeByKey(ctx, sourceFieldKey);
3767
3841
  if (!targetField) {
3768
3842
  return null;
3769
3843
  }
@@ -3798,6 +3872,7 @@ function evaluateBuiltInCrossFieldValidator(config, evaluationContext, sourceFie
3798
3872
  formValue,
3799
3873
  fieldPath: sourceFieldKey,
3800
3874
  customFunctions: evaluationContext.customFunctions || {},
3875
+ logger: evaluationContext.logger,
3801
3876
  });
3802
3877
  if (!conditionMet) {
3803
3878
  return null; // Condition not met, skip validation
@@ -3809,8 +3884,7 @@ function evaluateBuiltInCrossFieldValidator(config, evaluationContext, sourceFie
3809
3884
  return null; // Validation passed
3810
3885
  }
3811
3886
  // 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];
3887
+ const targetField = getFieldTreeByKey(ctx, sourceFieldKey);
3814
3888
  if (!targetField) {
3815
3889
  return null;
3816
3890
  }
@@ -4052,7 +4126,7 @@ class ComponentInitializedEvent {
4052
4126
  * ```
4053
4127
  */
4054
4128
  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));
4129
+ return eventBus.on('component-initialized').pipe(scan$1((count, _event) => count + 1, 0), map$1((currentCount) => currentCount >= expectedCount), filter$1((isComplete) => isComplete));
4056
4130
  }
4057
4131
  /**
4058
4132
  * Creates an observable that tracks component initialization progress with detailed status.
@@ -4077,7 +4151,7 @@ function createInitializationTracker(eventBus, expectedCount) {
4077
4151
  * ```
4078
4152
  */
4079
4153
  function createDetailedInitializationTracker(eventBus, expectedCount) {
4080
- return eventBus.on('component-initialized').pipe(scan((count, _event) => count + 1, 0), map$1((currentCount) => ({
4154
+ return eventBus.on('component-initialized').pipe(scan$1((count, _event) => count + 1, 0), map$1((currentCount) => ({
4081
4155
  currentCount,
4082
4156
  expectedCount,
4083
4157
  isComplete: currentCount >= expectedCount,
@@ -4325,10 +4399,8 @@ function flattenFields(fields, registry, options = {}) {
4325
4399
  // Row fields need to render their container element for grid layouts to work
4326
4400
  if (options.preserveRows && isRowField(field)) {
4327
4401
  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
4402
  // Recursively flatten children while preserving row structure
4331
- const flattenedChildren = flattenFields(fieldsArray, registry, options);
4403
+ const flattenedChildren = flattenFields(normalizeFieldsArray(field.fields), registry, options);
4332
4404
  // Keep the row field in the result with its flattened children
4333
4405
  // This allows the row component to render its container while children are flattened
4334
4406
  result.push({
@@ -4342,8 +4414,8 @@ function flattenFields(fields, registry, options = {}) {
4342
4414
  // Step 3: Handle fields with 'flatten' value handling (typically page/row fields)
4343
4415
  // These fields are pure containers - merge their children directly into the parent level
4344
4416
  if (field.fields) {
4345
- const fieldsArray = Array.isArray(field.fields) ? field.fields : Object.values(field.fields);
4346
- const flattenedChildren = flattenFields(fieldsArray, registry, options);
4417
+ const fields = field.fields;
4418
+ const flattenedChildren = flattenFields(normalizeFieldsArray(fields), registry, options);
4347
4419
  // Spread children directly into result - the container field itself is discarded
4348
4420
  // This is used for page fields (form structure) and row fields (form values)
4349
4421
  result.push(...flattenedChildren);
@@ -4405,8 +4477,7 @@ function traverseFields(fields, collection) {
4405
4477
  collectFromField(field, collection);
4406
4478
  // Recursively process container fields (page, row, group, array)
4407
4479
  if (hasChildFields(field)) {
4408
- const childFields = Array.isArray(field.fields) ? field.fields : Object.values(field.fields);
4409
- traverseFields(childFields, collection);
4480
+ traverseFields(normalizeFieldsArray(field.fields), collection);
4410
4481
  }
4411
4482
  }
4412
4483
  }
@@ -4812,14 +4883,16 @@ class PageNavigationStateChangeEvent {
4812
4883
  */
4813
4884
  class PageOrchestratorComponent {
4814
4885
  eventBus = inject(EventBus);
4815
- rootFormRegistry = inject(RootFormRegistryService);
4886
+ fieldContextRegistry = inject(FieldContextRegistryService);
4816
4887
  functionRegistry = inject(FunctionRegistryService);
4888
+ logger = inject(DYNAMIC_FORM_LOGGER);
4817
4889
  /**
4818
4890
  * Array of page field definitions to render
4819
4891
  */
4820
4892
  pageFields = input.required(...(ngDevMode ? [{ debugName: "pageFields" }] : []));
4821
4893
  /**
4822
- * Root form instance from parent DynamicForm
4894
+ * Root form instance from parent DynamicForm.
4895
+ * Uses FieldTree<unknown> to accept any form type.
4823
4896
  */
4824
4897
  form = input.required(...(ngDevMode ? [{ debugName: "form" }] : []));
4825
4898
  /**
@@ -4833,8 +4906,7 @@ class PageOrchestratorComponent {
4833
4906
  */
4834
4907
  pageHiddenStates = computed(() => {
4835
4908
  const pages = this.pageFields();
4836
- const formValue = this.rootFormRegistry.getFormValue();
4837
- return pages.map((page) => this.evaluatePageHidden(page, formValue));
4909
+ return pages.map((page) => this.evaluatePageHidden(page));
4838
4910
  }, ...(ngDevMode ? [{ debugName: "pageHiddenStates" }] : []));
4839
4911
  /**
4840
4912
  * Computed signal that returns indices of visible (non-hidden) pages.
@@ -5056,10 +5128,9 @@ class PageOrchestratorComponent {
5056
5128
  * A page is hidden if ANY of its hidden logic conditions evaluate to true.
5057
5129
  *
5058
5130
  * @param page The page field to evaluate
5059
- * @param formValue The current form value to evaluate against
5060
5131
  * @returns true if the page should be hidden, false otherwise
5061
5132
  */
5062
- evaluatePageHidden(page, formValue) {
5133
+ evaluatePageHidden(page) {
5063
5134
  // If no logic defined, page is visible
5064
5135
  if (!page.logic || page.logic.length === 0) {
5065
5136
  return false;
@@ -5079,14 +5150,9 @@ class PageOrchestratorComponent {
5079
5150
  }
5080
5151
  continue;
5081
5152
  }
5082
- // Evaluate conditional expression
5153
+ // Evaluate conditional expression using centralized context creation
5083
5154
  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
- };
5155
+ const context = this.fieldContextRegistry.createDisplayOnlyContext(page.key || '', this.functionRegistry.getCustomFunctions());
5090
5156
  if (evaluateCondition(condition, context)) {
5091
5157
  return true;
5092
5158
  }
@@ -5124,9 +5190,9 @@ class PageOrchestratorComponent {
5124
5190
  }
5125
5191
  }
5126
5192
  }
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-DjrahUgb.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-DjrahUgb.mjs').then(m => m.default)]] });
5193
+ `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs').then(m => m.default)]] });
5128
5194
  }
5129
- i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.0.5", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-DjrahUgb.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
5195
+ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.0.5", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
5130
5196
  type: Component,
5131
5197
  args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
5132
5198
  @for (pageField of pageFields(); track pageField.key; let i = $index) {
@@ -5195,6 +5261,7 @@ class DynamicForm {
5195
5261
  rootFormRegistry = inject(RootFormRegistryService);
5196
5262
  functionRegistry = inject(FunctionRegistryService);
5197
5263
  schemaRegistry = inject(SchemaRegistryService);
5264
+ logger = inject(DYNAMIC_FORM_LOGGER);
5198
5265
  // ─────────────────────────────────────────────────────────────────────────────
5199
5266
  // Inputs
5200
5267
  // ─────────────────────────────────────────────────────────────────────────────
@@ -5207,17 +5274,26 @@ class DynamicForm {
5207
5274
  // ─────────────────────────────────────────────────────────────────────────────
5208
5275
  // Memoized Functions
5209
5276
  // ─────────────────────────────────────────────────────────────────────────────
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}`;
5277
+ memoizedFlattenFields = memoize((fields, registry) => flattenFields(fields, registry), {
5278
+ resolver: (fields, registry) => {
5279
+ const fieldKeys = fields.map((f) => `${f.key || ''}:${f.type}`).join('|');
5280
+ const registryKeys = Array.from(registry.keys()).sort().join('|');
5281
+ return `${fieldKeys}__${registryKeys}`;
5282
+ },
5283
+ maxSize: 10,
5284
+ });
5285
+ memoizedFlattenFieldsForRendering = memoize((fields, registry) => flattenFields(fields, registry, { preserveRows: true }), {
5286
+ resolver: (fields, registry) => {
5287
+ const fieldKeys = fields.map((f) => `${f.key || ''}:${f.type}`).join('|');
5288
+ const registryKeys = Array.from(registry.keys()).sort().join('|');
5289
+ return `render_${fieldKeys}__${registryKeys}`;
5290
+ },
5291
+ maxSize: 10,
5214
5292
  });
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}`;
5293
+ memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), {
5294
+ resolver: (fields) => fields.map((f) => f.key).join('|'),
5295
+ maxSize: 10,
5219
5296
  });
5220
- memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), (fields) => fields.map((f) => f.key).join('|'));
5221
5297
  memoizedDefaultValues = memoize((fieldsById, registry) => {
5222
5298
  const result = {};
5223
5299
  for (const [key, field] of Object.entries(fieldsById)) {
@@ -5227,10 +5303,13 @@ class DynamicForm {
5227
5303
  }
5228
5304
  }
5229
5305
  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}`;
5306
+ }, {
5307
+ resolver: (fieldsById, registry) => {
5308
+ const fieldKeys = Object.keys(fieldsById).sort().join('|');
5309
+ const registryKeys = Array.from(registry.keys()).sort().join('|');
5310
+ return `defaults_${fieldKeys}__${registryKeys}`;
5311
+ },
5312
+ maxSize: 10,
5234
5313
  });
5235
5314
  // ─────────────────────────────────────────────────────────────────────────────
5236
5315
  // Computed Signals
@@ -5242,10 +5321,10 @@ class DynamicForm {
5242
5321
  const detection = detectFormMode(fields);
5243
5322
  const validation = FormModeValidator.validateFormConfiguration(fields);
5244
5323
  if (!validation.isValid) {
5245
- console.error('[Dynamic Forms] Invalid form configuration:', validation.errors);
5324
+ this.logger.error('Invalid form configuration:', validation.errors);
5246
5325
  }
5247
5326
  if (validation.warnings.length > 0) {
5248
- console.warn('[Dynamic Forms] Form configuration warnings:', validation.warnings);
5327
+ this.logger.warn('Form configuration warnings:', validation.warnings);
5249
5328
  }
5250
5329
  return detection;
5251
5330
  }, ...(ngDevMode ? [{ debugName: "formModeDetection" }] : []));
@@ -5362,11 +5441,6 @@ class DynamicForm {
5362
5441
  }, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
5363
5442
  /** Whether the form is currently submitting. */
5364
5443
  submitting = computed(() => this.form()().submitting(), ...(ngDevMode ? [{ debugName: "submitting" }] : []));
5365
- /**
5366
- * Announcement message for screen readers.
5367
- * Updates when form submission fails due to validation errors.
5368
- */
5369
- errorAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "errorAnnouncement" }] : []));
5370
5444
  // ─────────────────────────────────────────────────────────────────────────────
5371
5445
  // Initialization
5372
5446
  // ─────────────────────────────────────────────────────────────────────────────
@@ -5390,7 +5464,7 @@ class DynamicForm {
5390
5464
  submitted = outputFromObservable(this.eventBus.on('submit').pipe(switchMap(() => {
5391
5465
  const submissionConfig = this.config().submission;
5392
5466
  if (submissionConfig?.action) {
5393
- console.warn('[Dynamic Forms] Both `submission.action` and `(submitted)` output are configured. ' +
5467
+ this.logger.warn('Both `submission.action` and `(submitted)` output are configured. ' +
5394
5468
  'When using `submission.action`, the `(submitted)` output will not emit. ' +
5395
5469
  'Use either `submission.action` OR `(submitted)`, not both.');
5396
5470
  return EMPTY;
@@ -5434,74 +5508,19 @@ class DynamicForm {
5434
5508
  error: error instanceof Error ? error : new Error(String(error)),
5435
5509
  },
5436
5510
  ]);
5437
- console.error(`[Dynamic Forms] Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}). ` +
5511
+ this.logger.error(`Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}). ` +
5438
5512
  `Ensure the field type is registered in your field registry.`, error);
5439
5513
  },
5440
5514
  };
5441
5515
  return forkJoin(fields.map((f) => resolveField(f, context)));
5442
- }), map((fields) => fields.filter((f) => f !== undefined)), scan$1(reconcileFields, [])), { initialValue: [], injector: this.injector });
5443
- // ─────────────────────────────────────────────────────────────────────────────
5444
- // Effects
5445
- // ─────────────────────────────────────────────────────────────────────────────
5446
- syncEntityToValue = explicitEffect([this.entity], ([currentEntity]) => {
5447
- const currentValue = this.value();
5448
- if (!isEqual(currentEntity, currentValue)) {
5449
- this.value.set(currentEntity);
5450
- }
5451
- });
5452
- clearErrorsOnConfigChange = explicitEffect([this.config], () => {
5453
- this.fieldLoadingErrors.set([]);
5454
- });
5455
- emitPagedFormInitialized = explicitEffect([this.formModeDetection, this.pageFieldDefinitions], ([{ mode }, pages]) => {
5456
- if (mode === 'paged' && pages.length > 0) {
5457
- this.eventBus.dispatch(ComponentInitializedEvent, 'dynamic-form', this.componentId);
5458
- }
5459
- });
5460
- emitNonPagedFormInitialized = explicitEffect([this.resolvedFields, this.formModeDetection], ([fields, { mode }]) => {
5461
- if (mode === 'non-paged' && fields.length > 0) {
5462
- afterNextRender(() => {
5463
- this.eventBus.dispatch(ComponentInitializedEvent, 'dynamic-form', this.componentId);
5464
- }, { injector: this.injector });
5465
- }
5466
- });
5516
+ }), map((fields) => fields.filter((f) => f !== undefined)), scan(reconcileFields, [])), { initialValue: [], injector: this.injector });
5467
5517
  // ─────────────────────────────────────────────────────────────────────────────
5468
- // Event Handlers
5518
+ // Constructor
5469
5519
  // ─────────────────────────────────────────────────────────────────────────────
5470
- handleFormReset = this.eventBus
5471
- .on('form-reset')
5472
- .pipe(takeUntilDestroyed())
5473
- .subscribe(() => this.onFormReset());
5474
- handleFormClear = this.eventBus
5475
- .on('form-clear')
5476
- .pipe(takeUntilDestroyed())
5477
- .subscribe(() => this.onFormClear());
5478
- /**
5479
- * Announces form validation errors to screen readers when submission is attempted
5480
- * on an invalid form.
5481
- */
5482
- handleErrorAnnouncement = this.eventBus
5483
- .on('submit')
5484
- .pipe(takeUntilDestroyed())
5485
- .subscribe(() => {
5486
- const formState = this.form()();
5487
- if (formState.invalid()) {
5488
- const errorCount = this.countErrors(formState.errors());
5489
- const message = errorCount === 1
5490
- ? 'Form submission failed. Please fix 1 validation error before submitting.'
5491
- : `Form submission failed. Please fix ${errorCount} validation errors before submitting.`;
5492
- // Clear first to ensure screen readers announce even if message is the same
5493
- this.errorAnnouncement.set('');
5494
- // Use setTimeout to ensure the clear is processed before the new message
5495
- setTimeout(() => this.errorAnnouncement.set(message), 50);
5496
- }
5497
- });
5498
- handleSubmission = createSubmissionHandler({
5499
- eventBus: this.eventBus,
5500
- configSignal: this.config,
5501
- formSignal: this.form,
5502
- })
5503
- .pipe(takeUntilDestroyed())
5504
- .subscribe();
5520
+ constructor() {
5521
+ this.setupEffects();
5522
+ this.setupEventHandlers();
5523
+ }
5505
5524
  /**
5506
5525
  * Handles native form submission triggered by:
5507
5526
  * - Pressing Enter in an input field
@@ -5520,6 +5539,50 @@ class DynamicForm {
5520
5539
  // ─────────────────────────────────────────────────────────────────────────────
5521
5540
  // Private Methods
5522
5541
  // ─────────────────────────────────────────────────────────────────────────────
5542
+ setupEffects() {
5543
+ // Sync entity changes to the value signal
5544
+ explicitEffect([this.entity], ([currentEntity]) => {
5545
+ const currentValue = this.value();
5546
+ if (!isEqual(currentEntity, currentValue)) {
5547
+ this.value.set(currentEntity);
5548
+ }
5549
+ });
5550
+ // Clear loading errors when config changes
5551
+ explicitEffect([this.config], () => {
5552
+ this.fieldLoadingErrors.set([]);
5553
+ });
5554
+ // Emit initialization event for paged forms
5555
+ explicitEffect([this.formModeDetection, this.pageFieldDefinitions], ([{ mode }, pages]) => {
5556
+ if (mode === 'paged' && pages.length > 0) {
5557
+ this.eventBus.dispatch(ComponentInitializedEvent, 'dynamic-form', this.componentId);
5558
+ }
5559
+ });
5560
+ // Emit initialization event for non-paged forms
5561
+ explicitEffect([this.resolvedFields, this.formModeDetection], ([fields, { mode }]) => {
5562
+ if (mode === 'non-paged' && fields.length > 0) {
5563
+ afterNextRender(() => {
5564
+ this.eventBus.dispatch(ComponentInitializedEvent, 'dynamic-form', this.componentId);
5565
+ }, { injector: this.injector });
5566
+ }
5567
+ });
5568
+ }
5569
+ setupEventHandlers() {
5570
+ this.eventBus
5571
+ .on('form-reset')
5572
+ .pipe(takeUntilDestroyed())
5573
+ .subscribe(() => this.onFormReset());
5574
+ this.eventBus
5575
+ .on('form-clear')
5576
+ .pipe(takeUntilDestroyed())
5577
+ .subscribe(() => this.onFormClear());
5578
+ createSubmissionHandler({
5579
+ eventBus: this.eventBus,
5580
+ configSignal: this.config,
5581
+ formSignal: this.form,
5582
+ })
5583
+ .pipe(takeUntilDestroyed())
5584
+ .subscribe();
5585
+ }
5523
5586
  registerValidatorsFromConfig({ customFnConfig, schemas }) {
5524
5587
  if (schemas) {
5525
5588
  schemas.forEach((schema) => {
@@ -5576,15 +5639,6 @@ class DynamicForm {
5576
5639
  this.form()().value.set(emptyValue);
5577
5640
  this.value.set(emptyValue);
5578
5641
  }
5579
- /**
5580
- * Counts the total number of validation errors across all fields.
5581
- * Used for screen reader announcements.
5582
- */
5583
- countErrors(errors) {
5584
- if (!errors)
5585
- return 0;
5586
- return errors.length;
5587
- }
5588
5642
  // ─────────────────────────────────────────────────────────────────────────────
5589
5643
  // Lifecycle
5590
5644
  // ─────────────────────────────────────────────────────────────────────────────
@@ -5594,28 +5648,24 @@ class DynamicForm {
5594
5648
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
5595
5649
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.5", type: DynamicForm, isStandalone: true, selector: "form[dynamic-form]", inputs: { config: { classPropertyName: "config", publicName: "dynamic-form", isSignal: true, isRequired: true, transformFunction: null }, formOptions: { classPropertyName: "formOptions", publicName: "formOptions", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted", reset: "reset", cleared: "cleared", events: "events", initialized: "initialized", onPageChange: "onPageChange", onPageNavigationStateChange: "onPageNavigationStateChange" }, host: { listeners: { "submit": "onNativeSubmit($event)" }, properties: { "class.disabled": "disabled()", "class.df-form-paged": "formModeDetection().mode === \"paged\"", "class.df-form-non-paged": "formModeDetection().mode === \"non-paged\"", "attr.data-form-mode": "formModeDetection().mode" }, classAttribute: "df-dynamic-form df-form" }, providers: [EventBus, SchemaRegistryService, FunctionRegistryService, RootFormRegistryService, FieldContextRegistryService], ngImport: i0, template: `
5596
5650
  @if (formModeDetection().mode === 'paged') {
5597
- <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="$any(form())" [fieldSignalContext]="fieldSignalContext()"></div>
5651
+ <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
5598
5652
  } @else {
5599
5653
  @for (field of resolvedFields(); track field.key) {
5600
5654
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
5601
5655
  }
5602
5656
  }
5603
- <!-- Live region for screen reader announcements -->
5604
- <div class="df-live-region" role="status" aria-live="polite" aria-atomic="true">{{ errorAnnouncement() }}</div>
5605
5657
  `, 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 });
5606
5658
  }
5607
5659
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: DynamicForm, decorators: [{
5608
5660
  type: Component,
5609
5661
  args: [{ selector: 'form[dynamic-form]', imports: [NgComponentOutlet, PageOrchestratorComponent], template: `
5610
5662
  @if (formModeDetection().mode === 'paged') {
5611
- <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="$any(form())" [fieldSignalContext]="fieldSignalContext()"></div>
5663
+ <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
5612
5664
  } @else {
5613
5665
  @for (field of resolvedFields(); track field.key) {
5614
5666
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
5615
5667
  }
5616
5668
  }
5617
- <!-- Live region for screen reader announcements -->
5618
- <div class="df-live-region" role="status" aria-live="polite" aria-atomic="true">{{ errorAnnouncement() }}</div>
5619
5669
  `, providers: [EventBus, SchemaRegistryService, FunctionRegistryService, RootFormRegistryService, FieldContextRegistryService], host: {
5620
5670
  class: 'df-dynamic-form df-form',
5621
5671
  '[class.disabled]': 'disabled()',
@@ -5624,7 +5674,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImpor
5624
5674
  '[attr.data-form-mode]': 'formModeDetection().mode',
5625
5675
  '(submit)': 'onNativeSubmit($event)',
5626
5676
  }, changeDetection: ChangeDetectionStrategy.OnPush, 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"] }]
5627
- }], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamic-form", required: true }] }], formOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "formOptions", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], validityChange: [{ type: i0.Output, args: ["validityChange"] }], dirtyChange: [{ type: i0.Output, args: ["dirtyChange"] }], submitted: [{ type: i0.Output, args: ["submitted"] }], reset: [{ type: i0.Output, args: ["reset"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], events: [{ type: i0.Output, args: ["events"] }], initialized: [{ type: i0.Output, args: ["initialized"] }], onPageChange: [{ type: i0.Output, args: ["onPageChange"] }], onPageNavigationStateChange: [{ type: i0.Output, args: ["onPageNavigationStateChange"] }] } });
5677
+ }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamic-form", required: true }] }], formOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "formOptions", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], validityChange: [{ type: i0.Output, args: ["validityChange"] }], dirtyChange: [{ type: i0.Output, args: ["dirtyChange"] }], submitted: [{ type: i0.Output, args: ["submitted"] }], reset: [{ type: i0.Output, args: ["reset"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], events: [{ type: i0.Output, args: ["events"] }], initialized: [{ type: i0.Output, args: ["initialized"] }], onPageChange: [{ type: i0.Output, args: ["onPageChange"] }], onPageNavigationStateChange: [{ type: i0.Output, args: ["onPageNavigationStateChange"] }] } });
5628
5678
 
5629
5679
  /**
5630
5680
  * Maps an array field definition to component inputs.
@@ -5707,8 +5757,9 @@ function pageFieldMapper(fieldDef) {
5707
5757
  * @returns Signal containing Record of input names to values for ngComponentOutlet
5708
5758
  */
5709
5759
  function textFieldMapper(fieldDef) {
5710
- const rootFormRegistry = inject(RootFormRegistryService);
5760
+ const fieldContextRegistry = inject(FieldContextRegistryService);
5711
5761
  const functionRegistry = inject(FunctionRegistryService);
5762
+ const logger = inject(DYNAMIC_FORM_LOGGER);
5712
5763
  // Build base inputs (static, from field definition)
5713
5764
  const baseInputs = buildBaseInputs(fieldDef);
5714
5765
  // Create computed signal for hidden state based on logic configuration
@@ -5718,16 +5769,8 @@ function textFieldMapper(fieldDef) {
5718
5769
  const inputs = { ...baseInputs };
5719
5770
  // Evaluate hidden logic if present
5720
5771
  if (hiddenLogic.length > 0) {
5721
- const formValue = rootFormRegistry.getFormValue();
5722
- const customFunctions = functionRegistry.getCustomFunctions();
5723
- // Create evaluation context without a specific field value
5724
- // (text fields don't have their own value in the form)
5725
- const evaluationContext = {
5726
- fieldValue: undefined,
5727
- formValue,
5728
- fieldPath: fieldDef.key,
5729
- customFunctions,
5730
- };
5772
+ // Use centralized context creation for display-only components
5773
+ const evaluationContext = fieldContextRegistry.createDisplayOnlyContext(fieldDef.key, functionRegistry.getCustomFunctions());
5731
5774
  // Evaluate all hidden logic conditions - if ANY is true, the field is hidden
5732
5775
  inputs['hidden'] = hiddenLogic.some((logic) => {
5733
5776
  if (typeof logic.condition === 'boolean') {
@@ -5792,13 +5835,13 @@ const BUILT_IN_FIELDS = [
5792
5835
  },
5793
5836
  {
5794
5837
  name: 'page',
5795
- loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-DjrahUgb.mjs'),
5838
+ loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs'),
5796
5839
  mapper: pageFieldMapper,
5797
5840
  valueHandling: 'flatten',
5798
5841
  },
5799
5842
  {
5800
5843
  name: 'text',
5801
- loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-7QVNm95W.mjs'),
5844
+ loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-CiYq73aF.mjs'),
5802
5845
  mapper: textFieldMapper,
5803
5846
  valueHandling: 'exclude',
5804
5847
  },
@@ -5810,7 +5853,7 @@ const BUILT_IN_FIELDS = [
5810
5853
  * This function creates environment providers that can be used at application or route level
5811
5854
  * to register field types. It provides type-safe field registration with automatic type inference.
5812
5855
  *
5813
- * @param fieldTypes - Custom field type definitions to register alongside built-in types
5856
+ * @param items - Field type definitions and/or features (like withLogger)
5814
5857
  * @returns Environment providers for dependency injection with type inference
5815
5858
  *
5816
5859
  * @example
@@ -5825,6 +5868,19 @@ const BUILT_IN_FIELDS = [
5825
5868
  *
5826
5869
  * @example
5827
5870
  * ```typescript
5871
+ * // With logging configuration
5872
+ * bootstrapApplication(AppComponent, {
5873
+ * providers: [
5874
+ * provideDynamicForm(
5875
+ * ...withMaterialFields(),
5876
+ * withLogger({ level: LogLevel.Debug })
5877
+ * )
5878
+ * ]
5879
+ * });
5880
+ * ```
5881
+ *
5882
+ * @example
5883
+ * ```typescript
5828
5884
  * // Route-level setup
5829
5885
  * const routes: Routes = [
5830
5886
  * {
@@ -5851,7 +5907,10 @@ const BUILT_IN_FIELDS = [
5851
5907
  *
5852
5908
  * @public
5853
5909
  */
5854
- function provideDynamicForm(...fieldTypes) {
5910
+ function provideDynamicForm(...items) {
5911
+ // Separate field types from features
5912
+ const fieldTypes = items.filter((item) => !isDynamicFormFeature(item));
5913
+ const features = items.filter(isDynamicFormFeature);
5855
5914
  const fields = [...BUILT_IN_FIELDS, ...fieldTypes];
5856
5915
  // Extract config providers from field type arrays
5857
5916
  const configProviders = [];
@@ -5861,15 +5920,21 @@ function provideDynamicForm(...fieldTypes) {
5861
5920
  configProviders.push(...fieldTypeWithConfig.__configProviders);
5862
5921
  }
5863
5922
  });
5923
+ // Extract providers from features
5924
+ const featureProviders = [];
5925
+ features.forEach((feature) => {
5926
+ featureProviders.push(...feature.ɵproviders);
5927
+ });
5864
5928
  return makeEnvironmentProviders([
5865
5929
  {
5866
5930
  provide: FIELD_REGISTRY,
5867
5931
  useFactory: () => {
5932
+ const logger = inject(DYNAMIC_FORM_LOGGER);
5868
5933
  const registry = new Map();
5869
5934
  // Add custom field types
5870
5935
  fields.forEach((fieldType) => {
5871
5936
  if (registry.has(fieldType.name)) {
5872
- console.warn(`[Dynamic Forms] Field type "${fieldType.name}" is already registered. Overwriting.`);
5937
+ logger.warn(`Field type "${fieldType.name}" is already registered. Overwriting.`);
5873
5938
  }
5874
5939
  registry.set(fieldType.name, fieldType);
5875
5940
  });
@@ -5877,9 +5942,24 @@ function provideDynamicForm(...fieldTypes) {
5877
5942
  },
5878
5943
  },
5879
5944
  ...configProviders,
5945
+ ...featureProviders,
5880
5946
  ]);
5881
5947
  }
5882
5948
 
5949
+ /**
5950
+ * No-op logger for when no logger is provided.
5951
+ * Used as fallback in button logic evaluation to avoid breaking downstream packages.
5952
+ */
5953
+ const noOpLogger = {
5954
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
5955
+ debug: () => { },
5956
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
5957
+ info: () => { },
5958
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
5959
+ warn: () => { },
5960
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
5961
+ error: () => { },
5962
+ };
5883
5963
  /**
5884
5964
  * Default options for submit button disabled behavior.
5885
5965
  */
@@ -5936,6 +6016,7 @@ function evaluateLogicCondition(condition, ctx) {
5936
6016
  fieldValue: undefined,
5937
6017
  formValue,
5938
6018
  fieldPath: '',
6019
+ logger: ctx.logger ?? noOpLogger,
5939
6020
  };
5940
6021
  return evaluateCondition(condition, evaluationContext);
5941
6022
  }
@@ -6120,244 +6201,6 @@ function resolveTokens(args, context) {
6120
6201
  });
6121
6202
  }
6122
6203
 
6123
- /**
6124
- * Type guard to check if a value has the FormInternals structure.
6125
- */
6126
- function isFormInternals(value) {
6127
- return (typeof value === 'object' &&
6128
- value !== null &&
6129
- 'structure' in value &&
6130
- typeof value.structure?.childrenMap === 'function');
6131
- }
6132
- /**
6133
- * Type guard that unwraps signals and checks for FormInternals structure.
6134
- * Use this when the input might be a signal wrapping FormInternals.
6135
- */
6136
- function hasFormStructure(form) {
6137
- const unwrapped = isSignal(form) ? form() : form;
6138
- return isFormInternals(unwrapped);
6139
- }
6140
- /**
6141
- * Unwraps a signal if needed and returns the FormInternals or null.
6142
- */
6143
- function unwrapFormInput(form) {
6144
- const unwrapped = isSignal(form) ? form() : form;
6145
- return isFormInternals(unwrapped) ? unwrapped : null;
6146
- }
6147
- function getChildrenMap(form) {
6148
- const target = unwrapFormInput(form);
6149
- if (!target) {
6150
- return null;
6151
- }
6152
- const result = target.structure.childrenMap();
6153
- const normalizedMap = new Map();
6154
- for (const [key, childData] of result.byPropertyKey) {
6155
- normalizedMap.set(key, childData.reader);
6156
- }
6157
- return normalizedMap;
6158
- }
6159
- function getFieldProxy(node) {
6160
- if (!isFormInternals(node)) {
6161
- return null;
6162
- }
6163
- return node.fieldProxy ?? null;
6164
- }
6165
- function getChildFieldTree(form, key) {
6166
- const childrenMap = getChildrenMap(form);
6167
- return childrenMap?.get(key) ?? null;
6168
- }
6169
-
6170
- /**
6171
- * Resolves the field proxy and default validation messages from the form context.
6172
- * Must be called within an injection context.
6173
- *
6174
- * @param fieldKey The key of the field to resolve
6175
- * @returns The resolved context with field proxy and validation messages
6176
- */
6177
- function resolveValueFieldContext(fieldKey) {
6178
- const context = inject(FIELD_SIGNAL_CONTEXT);
6179
- // Get form-level validation messages
6180
- const defaultValidationMessages = context.defaultValidationMessages;
6181
- // Get the form root to access field proxy
6182
- const formRoot = context.form();
6183
- const childrenMap = getChildrenMap(formRoot);
6184
- // Resolve field proxy
6185
- let fieldProxy = undefined;
6186
- if (!childrenMap) {
6187
- // No childrenMap - the form might be a FormRecord where fields are direct properties
6188
- // First check if formRoot itself has fieldProxy (for FieldTree items)
6189
- const rootFieldProxy = getFieldProxy(formRoot);
6190
- if (rootFieldProxy) {
6191
- fieldProxy = rootFieldProxy;
6192
- }
6193
- else {
6194
- // Try accessing the field as a direct property on formRoot (FormRecord case)
6195
- // This handles forms created with form(entity, schema) where fields are accessible as formRoot[key]
6196
- const formField = formRoot[fieldKey];
6197
- const resolvedProxy = getFieldProxy(formField);
6198
- if (resolvedProxy) {
6199
- fieldProxy = resolvedProxy;
6200
- }
6201
- else if (isSignal(formField) || formField) {
6202
- // The field itself might be the proxy (signal), or might have a different structure
6203
- // Try using the field directly as the proxy
6204
- fieldProxy = formField;
6205
- }
6206
- }
6207
- }
6208
- else {
6209
- // Standard field access for non-array keys via childrenMap lookup
6210
- const formField = childrenMap.get(fieldKey);
6211
- if (formField) {
6212
- fieldProxy = formField;
6213
- }
6214
- }
6215
- return { fieldProxy, defaultValidationMessages };
6216
- }
6217
- /**
6218
- * Builds the base inputs for a value field.
6219
- * This is a helper function used by valueFieldMapper and specialized mappers.
6220
- *
6221
- * @param fieldDef The value field definition
6222
- * @param ctx The resolved value field context
6223
- * @returns Record of input names to values
6224
- */
6225
- function buildValueFieldInputs(fieldDef, ctx) {
6226
- const omittedFields = omit(fieldDef, ['value']);
6227
- const baseInputs = buildBaseInputs(omittedFields);
6228
- const inputs = {
6229
- ...baseInputs,
6230
- // Always pass validationMessages (or empty object) - required for error display signals
6231
- validationMessages: fieldDef.validationMessages ?? {},
6232
- };
6233
- // Value field specific properties
6234
- if (fieldDef.placeholder !== undefined) {
6235
- inputs['placeholder'] = fieldDef.placeholder;
6236
- }
6237
- if (ctx.defaultValidationMessages !== undefined) {
6238
- inputs['defaultValidationMessages'] = ctx.defaultValidationMessages;
6239
- }
6240
- if (ctx.fieldProxy !== undefined) {
6241
- inputs['field'] = ctx.fieldProxy;
6242
- }
6243
- return inputs;
6244
- }
6245
- /**
6246
- * Maps a value field definition to component inputs.
6247
- *
6248
- * Value fields are input fields that contribute to the form's value (text, number, etc.).
6249
- * This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field proxy.
6250
- *
6251
- * For fields with specific properties (select, datepicker, textarea, slider), use the specialized mappers:
6252
- * - selectFieldMapper: for fields with options (select, radio, multi-checkbox)
6253
- * - datepickerFieldMapper: for fields with minDate, maxDate, startAt
6254
- * - textareaFieldMapper: for fields with rows, cols
6255
- * - sliderFieldMapper: for fields with minValue, maxValue, step
6256
- *
6257
- * @param fieldDef The value field definition
6258
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6259
- */
6260
- function valueFieldMapper(fieldDef) {
6261
- const ctx = resolveValueFieldContext(fieldDef.key);
6262
- return computed(() => buildValueFieldInputs(fieldDef, ctx));
6263
- }
6264
-
6265
- /**
6266
- * Maps a checkbox/toggle field definition to component inputs.
6267
- *
6268
- * Checkbox fields are checked fields that contribute to the form's value as boolean.
6269
- * This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field proxy.
6270
- *
6271
- * @param fieldDef The checkbox field definition
6272
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6273
- */
6274
- function checkboxFieldMapper(fieldDef) {
6275
- const context = inject(FIELD_SIGNAL_CONTEXT);
6276
- const omittedFields = omit(fieldDef, ['value']);
6277
- // Build base inputs (static, from field definition)
6278
- const baseInputs = buildBaseInputs(omittedFields);
6279
- // Get form-level validation messages (static)
6280
- const defaultValidationMessages = context.defaultValidationMessages;
6281
- // Get the form root to access field proxy
6282
- const formRoot = context.form();
6283
- const childrenMap = getChildrenMap(formRoot);
6284
- // Resolve field proxy (static - determined once during mapping)
6285
- let fieldProxy = undefined;
6286
- if (childrenMap) {
6287
- const formField = childrenMap.get(fieldDef.key);
6288
- if (formField) {
6289
- fieldProxy = formField;
6290
- }
6291
- }
6292
- // Return computed signal for reactive updates
6293
- return computed(() => {
6294
- const inputs = {
6295
- ...baseInputs,
6296
- // Always pass validationMessages (or empty object) - required for error display signals
6297
- validationMessages: fieldDef.validationMessages ?? {},
6298
- };
6299
- // Checked field specific property: placeholder
6300
- if (fieldDef.placeholder !== undefined) {
6301
- inputs['placeholder'] = fieldDef.placeholder;
6302
- }
6303
- if (defaultValidationMessages !== undefined) {
6304
- inputs['defaultValidationMessages'] = defaultValidationMessages;
6305
- }
6306
- if (fieldProxy !== undefined) {
6307
- inputs['field'] = fieldProxy;
6308
- }
6309
- return inputs;
6310
- });
6311
- }
6312
-
6313
- /**
6314
- * Maps a field with options (select, radio, multi-checkbox) to component inputs.
6315
- *
6316
- * Extends the base value field mapper by adding the options property.
6317
- *
6318
- * @param fieldDef The field definition with options
6319
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6320
- */
6321
- function optionsFieldMapper(fieldDef) {
6322
- const ctx = resolveValueFieldContext(fieldDef.key);
6323
- return computed(() => {
6324
- // Cast to BaseValueField - safe because all FieldWithOptions types extend BaseValueField
6325
- const inputs = buildValueFieldInputs(fieldDef, ctx);
6326
- // Add options property
6327
- inputs['options'] = fieldDef.options;
6328
- return inputs;
6329
- });
6330
- }
6331
-
6332
- /**
6333
- * Maps a datepicker field to component inputs.
6334
- *
6335
- * Extends the base value field mapper by adding datepicker-specific properties:
6336
- * - minDate: Minimum selectable date
6337
- * - maxDate: Maximum selectable date
6338
- * - startAt: Initial date to display when opening the picker
6339
- *
6340
- * @param fieldDef The datepicker field definition
6341
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6342
- */
6343
- function datepickerFieldMapper(fieldDef) {
6344
- const ctx = resolveValueFieldContext(fieldDef.key);
6345
- return computed(() => {
6346
- const inputs = buildValueFieldInputs(fieldDef, ctx);
6347
- // Add datepicker-specific properties
6348
- if (fieldDef.minDate !== undefined) {
6349
- inputs['minDate'] = fieldDef.minDate;
6350
- }
6351
- if (fieldDef.maxDate !== undefined) {
6352
- inputs['maxDate'] = fieldDef.maxDate;
6353
- }
6354
- if (fieldDef.startAt !== undefined) {
6355
- inputs['startAt'] = fieldDef.startAt;
6356
- }
6357
- return inputs;
6358
- });
6359
- }
6360
-
6361
6204
  /**
6362
6205
  * Converts DynamicText (string | Observable | Signal) to Observable<string>
6363
6206
  * Unifies all three types into a consistent Observable stream
@@ -6379,187 +6222,6 @@ function dynamicTextToObservable(value, injector) {
6379
6222
  return of(String(value));
6380
6223
  }
6381
6224
 
6382
- /**
6383
- * Computed signal that determines if errors should be displayed
6384
- * Based on field's invalid, touched, and error count
6385
- *
6386
- * @param field - Signal containing FieldTree
6387
- * @returns Signal<boolean> - True if errors should be displayed
6388
- */
6389
- function shouldShowErrors(field) {
6390
- return computed(() => {
6391
- const fieldTree = field();
6392
- const control = fieldTree();
6393
- return control.invalid() && control.touched() && control.errors().length > 0;
6394
- });
6395
- }
6396
-
6397
- /**
6398
- * Interpolates {{param}} placeholders in message with error values
6399
- *
6400
- * @param message - Message template with {{param}} placeholders
6401
- * @param error - Validation error containing parameter values
6402
- * @returns Message with interpolated parameters
6403
- *
6404
- * @example
6405
- * interpolateParams("Min value is {{min}}", { kind: 'min', min: 5 })
6406
- * // Returns: "Min value is 5"
6407
- */
6408
- function interpolateParams(message, error) {
6409
- let result = message;
6410
- const params = extractErrorParams(error);
6411
- Object.entries(params).forEach(([key, value]) => {
6412
- const placeholder = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
6413
- result = result.replace(placeholder, safeToString(value));
6414
- });
6415
- return result;
6416
- }
6417
- /**
6418
- * Safely converts a value to a string, handling complex objects
6419
- * @internal
6420
- */
6421
- function safeToString(value) {
6422
- if (value === null || value === undefined) {
6423
- return '';
6424
- }
6425
- // Handle primitives directly
6426
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
6427
- return String(value);
6428
- }
6429
- // Handle RegExp (has its own toString)
6430
- if (value instanceof RegExp) {
6431
- return value.toString();
6432
- }
6433
- // Handle Date
6434
- if (value instanceof Date) {
6435
- return value.toISOString();
6436
- }
6437
- // Handle arrays
6438
- if (Array.isArray(value)) {
6439
- return value.join(', ');
6440
- }
6441
- // For objects, try JSON.stringify first, fall back to empty string
6442
- try {
6443
- // Check if object has a custom toString that's not Object.prototype.toString
6444
- if (typeof value === 'object' && value.toString !== Object.prototype.toString) {
6445
- return value.toString();
6446
- }
6447
- // Otherwise use JSON representation
6448
- return JSON.stringify(value);
6449
- }
6450
- catch {
6451
- // If JSON.stringify fails (circular reference, etc.), return empty string
6452
- return '[object]';
6453
- }
6454
- }
6455
- /**
6456
- * Extracts parameter values from validation error
6457
- * @internal
6458
- */
6459
- function extractErrorParams(error) {
6460
- const params = {};
6461
- // Common validation error parameters
6462
- if ('min' in error)
6463
- params.min = error.min;
6464
- if ('max' in error)
6465
- params.max = error.max;
6466
- if ('requiredLength' in error)
6467
- params.requiredLength = error.requiredLength;
6468
- if ('actualLength' in error)
6469
- params.actualLength = error.actualLength;
6470
- if ('pattern' in error)
6471
- params.pattern = error.pattern;
6472
- if ('actual' in error)
6473
- params.actual = error.actual;
6474
- if ('expected' in error)
6475
- params.expected = error.expected;
6476
- // Include all custom properties from error object (for custom validators)
6477
- // Skip 'kind' as it's the error type identifier, not a parameter
6478
- Object.keys(error).forEach((key) => {
6479
- if (key !== 'kind' && !(key in params)) {
6480
- params[key] = error[key];
6481
- }
6482
- });
6483
- return params;
6484
- }
6485
-
6486
- /**
6487
- * Factory function that creates a signal of resolved validation errors
6488
- * Handles async resolution of DynamicText validation messages
6489
- *
6490
- * @param field - Signal containing FieldTree
6491
- * @param validationMessages - Signal containing custom field-level validation messages
6492
- * @param defaultValidationMessages - Signal containing default validation messages (fallback)
6493
- * @param injector - Optional injector for DynamicText resolution
6494
- * @returns Signal<ResolvedError[]> - Reactively resolved error messages
6495
- *
6496
- * @example
6497
- * ```typescript
6498
- * readonly resolvedErrors = createResolvedErrorsSignal(
6499
- * this.field,
6500
- * this.validationMessages,
6501
- * this.defaultValidationMessages
6502
- * );
6503
- * ```
6504
- */
6505
- function createResolvedErrorsSignal(field, validationMessages, defaultValidationMessages = signal(undefined), injector = inject(Injector)) {
6506
- // Ensure validationMessages is never undefined (mappers pass {} if not defined)
6507
- const messages = computed(() => validationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "messages" }] : []));
6508
- const defaultMessages = computed(() => defaultValidationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "defaultMessages" }] : []));
6509
- // Create a computed signal that reads the actual errors from the field
6510
- // This ensures the signal tracks changes to field().errors(), not just the field reference
6511
- const errors = computed(() => {
6512
- const control = field()();
6513
- return control.errors();
6514
- }, ...(ngDevMode ? [{ debugName: "errors" }] : []));
6515
- // Convert signals to observables using toObservable with injector
6516
- const errors$ = toObservable(errors, { injector });
6517
- const messages$ = toObservable(messages, { injector });
6518
- const defaultMessages$ = toObservable(defaultMessages, { injector });
6519
- // Combine observables and process errors
6520
- const resolvedErrors$ = combineLatest([errors$, messages$, defaultMessages$]).pipe(switchMap$1(([currentErrors, msgs, defaultMsgs]) => {
6521
- // No errors - return empty array
6522
- if (!currentErrors || currentErrors.length === 0) {
6523
- return of([]);
6524
- }
6525
- // Create observable for each error's resolved message
6526
- const errorResolvers = currentErrors.map((error) => resolveErrorMessage(error, msgs, defaultMsgs, injector));
6527
- // Combine all error message observables into single array emission, filtering out nulls
6528
- return errorResolvers.length > 0
6529
- ? combineLatest(errorResolvers).pipe(map$1((errors) => errors.filter((e) => e !== null)))
6530
- : of([]);
6531
- }));
6532
- // Convert observable back to signal using toSignal with injector
6533
- // toSignal properly handles the injection context and manages subscriptions
6534
- return toSignal(resolvedErrors$, { initialValue: [], injector });
6535
- }
6536
- /**
6537
- * Resolves a single error message from DynamicText sources with fallback logic
6538
- * Priority: field-level message → default message → no message (logs warning)
6539
- * @internal
6540
- */
6541
- function resolveErrorMessage(error, fieldMessages, defaultMessages, injector) {
6542
- // Check for field-level custom message first
6543
- const fieldMessage = fieldMessages[error.kind];
6544
- // Fall back to default message if no field-level message exists
6545
- const defaultMessage = defaultMessages[error.kind];
6546
- // Determine which message to use: field-level → default
6547
- const messageToUse = fieldMessage ?? defaultMessage;
6548
- // If no message found, log warning and return null (will be filtered out)
6549
- if (!messageToUse) {
6550
- console.warn(`[Dynamic Forms] No validation message found for error kind "${error.kind}". ` +
6551
- `Please provide a message via field-level validationMessages or form-level defaultValidationMessages.`);
6552
- return of(null);
6553
- }
6554
- // Convert DynamicText to Observable
6555
- const messageObservable = dynamicTextToObservable(messageToUse, injector);
6556
- // Apply parameter interpolation to support {{param}} syntax
6557
- return messageObservable.pipe(map$1((msg) => ({
6558
- kind: error.kind,
6559
- message: interpolateParams(msg, error),
6560
- })));
6561
- }
6562
-
6563
6225
  /**
6564
6226
  * Pipe that handles dynamic text resolution with support for static strings,
6565
6227
  * Observables, and Signals.
@@ -6649,6 +6311,7 @@ class RowFieldComponent {
6649
6311
  fieldRegistry = injectFieldRegistry();
6650
6312
  injector = inject(Injector);
6651
6313
  eventBus = inject(EventBus);
6314
+ logger = inject(DYNAMIC_FORM_LOGGER);
6652
6315
  // ─────────────────────────────────────────────────────────────────────────────
6653
6316
  // Inputs
6654
6317
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6682,20 +6345,29 @@ class RowFieldComponent {
6682
6345
  destroyRef: this.destroyRef,
6683
6346
  onError: (fieldDef, error) => {
6684
6347
  const fieldKey = fieldDef.key || '<no key>';
6685
- console.error(`[Dynamic Forms] Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +
6348
+ this.logger.error(`Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +
6686
6349
  `within row '${rowKey}'. Ensure the field type is registered in your field registry.`, error);
6687
6350
  },
6688
6351
  };
6689
6352
  return forkJoin(fields.map((f) => resolveField(f, context)));
6690
- }), map((fields) => fields.filter((f) => f !== undefined)), scan$1(reconcileFields, [])), { initialValue: [], injector: this.injector });
6353
+ }), map((fields) => fields.filter((f) => f !== undefined)), scan(reconcileFields, [])), { initialValue: [], injector: this.injector });
6691
6354
  // ─────────────────────────────────────────────────────────────────────────────
6692
- // Effects
6355
+ // Constructor
6693
6356
  // ─────────────────────────────────────────────────────────────────────────────
6694
- emitInitializedOnFieldsResolved = explicitEffect([this.resolvedFields], ([fields]) => {
6695
- if (fields.length > 0) {
6696
- emitComponentInitialized(this.eventBus, 'row', this.field().key, this.injector);
6697
- }
6698
- });
6357
+ constructor() {
6358
+ this.setupEffects();
6359
+ }
6360
+ // ─────────────────────────────────────────────────────────────────────────────
6361
+ // Private Methods
6362
+ // ─────────────────────────────────────────────────────────────────────────────
6363
+ setupEffects() {
6364
+ // Emit initialization event when fields are resolved
6365
+ explicitEffect([this.resolvedFields], ([fields]) => {
6366
+ if (fields.length > 0) {
6367
+ emitComponentInitialized(this.eventBus, 'row', this.field().key, this.injector);
6368
+ }
6369
+ });
6370
+ }
6699
6371
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: RowFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6700
6372
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.5", type: RowFieldComponent, isStandalone: true, selector: "div[row-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "class.disabled": "disabled()", "id": "`${key()}`", "attr.data-testid": "key()" }, classAttribute: "df-field df-row" }, ngImport: i0, template: `
6701
6373
  @for (field of resolvedFields(); track field.key) {
@@ -6715,7 +6387,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImpor
6715
6387
  '[id]': '`${key()}`',
6716
6388
  '[attr.data-testid]': 'key()',
6717
6389
  }, changeDetection: ChangeDetectionStrategy.OnPush, 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"] }]
6718
- }], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }] } });
6390
+ }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }] } });
6719
6391
 
6720
6392
  var rowField_component = /*#__PURE__*/Object.freeze({
6721
6393
  __proto__: null,
@@ -6739,12 +6411,22 @@ class GroupFieldComponent {
6739
6411
  parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
6740
6412
  injector = inject(Injector);
6741
6413
  eventBus = inject(EventBus);
6414
+ logger = inject(DYNAMIC_FORM_LOGGER);
6742
6415
  // ─────────────────────────────────────────────────────────────────────────────
6743
6416
  // Memoized Functions
6744
6417
  // ─────────────────────────────────────────────────────────────────────────────
6745
- 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(','));
6746
- memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), (fields) => fields.map((f) => f.key).join(','));
6747
- memoizedDefaultValues = memoize((fieldsById, registry) => mapValues(fieldsById, (field) => getFieldDefaultValue(field, registry)), (fieldsById, registry) => Object.keys(fieldsById).sort().join(',') + '_' + Array.from(registry.keys()).sort().join(','));
6418
+ memoizedFlattenFields = memoize((fields, registry) => flattenFields([...fields], registry), {
6419
+ resolver: (fields, registry) => JSON.stringify(fields.map((f) => ({ key: f.key, type: f.type }))) + '_' + Array.from(registry.keys()).sort().join(','),
6420
+ maxSize: 10,
6421
+ });
6422
+ memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), {
6423
+ resolver: (fields) => fields.map((f) => f.key).join(','),
6424
+ maxSize: 10,
6425
+ });
6426
+ memoizedDefaultValues = memoize((fieldsById, registry) => mapValues(fieldsById, (field) => getFieldDefaultValue(field, registry)), {
6427
+ resolver: (fieldsById, registry) => Object.keys(fieldsById).sort().join(',') + '_' + Array.from(registry.keys()).sort().join(','),
6428
+ maxSize: 10,
6429
+ });
6748
6430
  // ─────────────────────────────────────────────────────────────────────────────
6749
6431
  // Inputs
6750
6432
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6755,35 +6437,25 @@ class GroupFieldComponent {
6755
6437
  // ─────────────────────────────────────────────────────────────────────────────
6756
6438
  rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : []));
6757
6439
  formSetup = computed(() => {
6758
- try {
6759
- const groupField = this.field();
6760
- const registry = this.rawFieldRegistry();
6761
- if (groupField.fields && groupField.fields.length > 0) {
6762
- const flattenedFields = this.memoizedFlattenFields(groupField.fields, registry);
6763
- const fieldsById = this.memoizedKeyBy(flattenedFields);
6764
- const defaultValues = this.memoizedDefaultValues(fieldsById, registry);
6765
- return {
6766
- fields: flattenedFields,
6767
- originalFields: groupField.fields,
6768
- defaultValues,
6769
- registry,
6770
- };
6771
- }
6440
+ const groupField = this.field();
6441
+ const registry = this.rawFieldRegistry();
6442
+ if (groupField.fields && groupField.fields.length > 0) {
6443
+ const flattenedFields = this.memoizedFlattenFields(groupField.fields, registry);
6444
+ const fieldsById = this.memoizedKeyBy(flattenedFields);
6445
+ const defaultValues = this.memoizedDefaultValues(fieldsById, registry);
6772
6446
  return {
6773
- fields: [],
6774
- originalFields: [],
6775
- defaultValues: {},
6447
+ fields: flattenedFields,
6448
+ originalFields: groupField.fields,
6449
+ defaultValues,
6776
6450
  registry,
6777
6451
  };
6778
6452
  }
6779
- catch {
6780
- return {
6781
- fields: [],
6782
- originalFields: [],
6783
- defaultValues: {},
6784
- registry: this.rawFieldRegistry(),
6785
- };
6786
- }
6453
+ return {
6454
+ fields: [],
6455
+ originalFields: [],
6456
+ defaultValues: {},
6457
+ registry,
6458
+ };
6787
6459
  }, ...(ngDevMode ? [{ debugName: "formSetup" }] : []));
6788
6460
  defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : []));
6789
6461
  entity = linkedSignal(() => {
@@ -6813,36 +6485,26 @@ class GroupFieldComponent {
6813
6485
  touched = computed(() => this.form()().touched(), ...(ngDevMode ? [{ debugName: "touched" }] : []));
6814
6486
  errors = computed(() => this.form()().errors(), ...(ngDevMode ? [{ debugName: "errors" }] : []));
6815
6487
  disabled = computed(() => this.form()().disabled(), ...(ngDevMode ? [{ debugName: "disabled" }] : []));
6816
- /**
6817
- * Get the nested FieldTree from the parent form for this group.
6818
- * This allows child fields to update the parent form directly,
6819
- * avoiding the need for separate form synchronization.
6820
- */
6821
6488
  nestedFieldTree = computed(() => {
6822
- const parentForm = this.parentFieldSignalContext.form();
6489
+ const parentForm = this.parentFieldSignalContext.form;
6823
6490
  const groupKey = this.field().key;
6824
- return getChildFieldTree(parentForm, groupKey);
6491
+ const child = parentForm[groupKey];
6492
+ if (!child) {
6493
+ throw new Error(`[Dynamic Forms] Group field "${groupKey}" not found in parent form. ` + `Ensure the parent form schema includes this group field.`);
6494
+ }
6495
+ return child;
6825
6496
  }, ...(ngDevMode ? [{ debugName: "nestedFieldTree" }] : []));
6826
6497
  groupInjector = computed(() => {
6827
- const nestedTree = this.nestedFieldTree();
6828
- // Use the nested FieldTree from parent if available, otherwise fall back to our own form
6829
- // The nested FieldTree ensures changes propagate directly to the parent form
6830
- const formToProvide = nestedTree ?? this.form();
6831
6498
  const groupFieldSignalContext = {
6832
6499
  injector: this.injector,
6833
6500
  value: this.parentFieldSignalContext.value,
6834
6501
  defaultValues: this.defaultValues,
6835
- form: (() => formToProvide),
6502
+ form: this.nestedFieldTree(),
6836
6503
  defaultValidationMessages: this.parentFieldSignalContext.defaultValidationMessages,
6837
6504
  };
6838
6505
  return Injector.create({
6839
6506
  parent: this.injector,
6840
- providers: [
6841
- {
6842
- provide: FIELD_SIGNAL_CONTEXT,
6843
- useValue: groupFieldSignalContext,
6844
- },
6845
- ],
6507
+ providers: [{ provide: FIELD_SIGNAL_CONTEXT, useValue: groupFieldSignalContext }],
6846
6508
  });
6847
6509
  }, ...(ngDevMode ? [{ debugName: "groupInjector" }] : []));
6848
6510
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6855,32 +6517,34 @@ class GroupFieldComponent {
6855
6517
  // Field Resolution
6856
6518
  // ─────────────────────────────────────────────────────────────────────────────
6857
6519
  fieldsSource = computed(() => this.formSetup().fields, ...(ngDevMode ? [{ debugName: "fieldsSource" }] : []));
6858
- resolvedFields = derivedFromDeferred(this.fieldsSource, pipe(switchMap((fields) => {
6859
- if (!fields || fields.length === 0) {
6860
- return of([]);
6861
- }
6862
- const groupKey = this.field().key;
6863
- const context = {
6864
- loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
6865
- registry: this.rawFieldRegistry(),
6866
- injector: this.groupInjector(),
6867
- destroyRef: this.destroyRef,
6868
- onError: (fieldDef, error) => {
6869
- const fieldKey = fieldDef.key || '<no key>';
6870
- console.error(`[Dynamic Forms] Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +
6871
- `within group '${groupKey}'. Ensure the field type is registered in your field registry.`, error);
6872
- },
6873
- };
6874
- return forkJoin(fields.map((f) => resolveField(f, context)));
6875
- }), map((fields) => fields.filter((f) => f !== undefined)), scan$1(reconcileFields, [])), { initialValue: [], injector: this.injector });
6520
+ resolvedFields = derivedFromDeferred(this.fieldsSource, createFieldResolutionPipe(() => ({
6521
+ loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
6522
+ registry: this.rawFieldRegistry(),
6523
+ injector: this.groupInjector(),
6524
+ destroyRef: this.destroyRef,
6525
+ onError: (fieldDef, error) => {
6526
+ const fieldKey = fieldDef.key || '<no key>';
6527
+ this.logger.error(`Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +
6528
+ `within group '${this.field().key}'. Ensure the field type is registered in your field registry.`, error);
6529
+ },
6530
+ })), { initialValue: [], injector: this.injector });
6876
6531
  // ─────────────────────────────────────────────────────────────────────────────
6877
- // Effects
6532
+ // Constructor
6878
6533
  // ─────────────────────────────────────────────────────────────────────────────
6879
- emitInitializedOnFieldsResolved = explicitEffect([this.resolvedFields], ([fields]) => {
6880
- if (fields.length > 0) {
6881
- emitComponentInitialized(this.eventBus, 'group', this.field().key, this.injector);
6882
- }
6883
- });
6534
+ constructor() {
6535
+ this.setupEffects();
6536
+ }
6537
+ // ─────────────────────────────────────────────────────────────────────────────
6538
+ // Private Methods
6539
+ // ─────────────────────────────────────────────────────────────────────────────
6540
+ setupEffects() {
6541
+ // Emit initialization event when fields are resolved
6542
+ explicitEffect([this.resolvedFields], ([fields]) => {
6543
+ if (fields.length > 0) {
6544
+ emitComponentInitialized(this.eventBus, 'group', this.field().key, this.injector);
6545
+ }
6546
+ });
6547
+ }
6884
6548
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6885
6549
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.5", type: GroupFieldComponent, isStandalone: true, selector: "fieldset[group-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted" }, host: { attributes: { "role": "group" }, properties: { "class.disabled": "disabled()", "id": "`${key()}`", "attr.data-testid": "key()" }, classAttribute: "df-field df-group" }, ngImport: i0, template: `
6886
6550
  @for (field of resolvedFields(); track field.key) {
@@ -6901,7 +6565,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImpor
6901
6565
  '[id]': '`${key()}`',
6902
6566
  '[attr.data-testid]': 'key()',
6903
6567
  }, changeDetection: ChangeDetectionStrategy.OnPush, 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"] }]
6904
- }], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], validityChange: [{ type: i0.Output, args: ["validityChange"] }], dirtyChange: [{ type: i0.Output, args: ["dirtyChange"] }], submitted: [{ type: i0.Output, args: ["submitted"] }] } });
6568
+ }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], validityChange: [{ type: i0.Output, args: ["validityChange"] }], dirtyChange: [{ type: i0.Output, args: ["dirtyChange"] }], submitted: [{ type: i0.Output, args: ["submitted"] }] } });
6905
6569
 
6906
6570
  var groupField_component = /*#__PURE__*/Object.freeze({
6907
6571
  __proto__: null,
@@ -6969,8 +6633,8 @@ function syncItemToParent(itemFormInstance, parentFieldSignalContext, arrayKey,
6969
6633
  explicitEffect([() => itemFormInstance().value()], ([itemValue]) => {
6970
6634
  if (isSyncing)
6971
6635
  return;
6972
- const parentForm = parentFieldSignalContext.form();
6973
- const parentValue = untracked(() => parentForm.value());
6636
+ const parentForm = parentFieldSignalContext.form;
6637
+ const parentValue = untracked(() => parentForm().value());
6974
6638
  const currentArray = getArrayValue(parentValue, arrayKey);
6975
6639
  const idx = untracked(() => indexSignal());
6976
6640
  // Only sync if value actually changed
@@ -6978,8 +6642,10 @@ function syncItemToParent(itemFormInstance, parentFieldSignalContext, arrayKey,
6978
6642
  isSyncing = true;
6979
6643
  const newArray = [...currentArray];
6980
6644
  newArray[idx] = itemValue;
6645
+ // Update the parent form with the new array value
6646
+ // The `as any` is required due to Angular Signal Forms' complex conditional types
6981
6647
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6982
- parentForm.value.set({ ...parentValue, [arrayKey]: newArray });
6648
+ parentForm().value.set({ ...parentValue, [arrayKey]: newArray });
6983
6649
  isSyncing = false;
6984
6650
  }
6985
6651
  });
@@ -7026,7 +6692,7 @@ function createArrayItemInjectorAndInputs(options) {
7026
6692
  * The linkedSignal derives from the parent array at the current index.
7027
6693
  *
7028
6694
  * For array items, we always create a form with a schema to ensure proper field structure.
7029
- * This is needed for valueFieldMapper to find fields via childrenMap.
6695
+ * This is needed for valueFieldMapper to find fields via bracket notation.
7030
6696
  */
7031
6697
  function createObjectItemForm(options) {
7032
6698
  const { template, indexSignal, parentFieldSignalContext, parentInjector, registry, arrayKey } = options;
@@ -7062,7 +6728,6 @@ function createItemInjector(options) {
7062
6728
  injector: undefined,
7063
6729
  value: parentFieldSignalContext.value,
7064
6730
  defaultValues: () => ({}),
7065
- // formRef is already the form instance (result of form()), no need to wrap in another function
7066
6731
  form: formRef,
7067
6732
  defaultValidationMessages: parentFieldSignalContext.defaultValidationMessages,
7068
6733
  };
@@ -7113,7 +6778,8 @@ function resolveArrayItem(options) {
7113
6778
  };
7114
6779
  }), catchError((error) => {
7115
6780
  if (!destroyRef.destroyed) {
7116
- console.error(`[Dynamic Forms] Failed to load component for field type '${template.type}' at index ${index} ` +
6781
+ const logger = parentInjector.get(DYNAMIC_FORM_LOGGER);
6782
+ logger.error(`Failed to load component for field type '${template.type}' at index ${index} ` +
7117
6783
  `within array '${arrayField.key}'. Ensure the field type is registered in your field registry.`, error);
7118
6784
  }
7119
6785
  return of(undefined);
@@ -7136,6 +6802,7 @@ class ArrayFieldComponent {
7136
6802
  parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
7137
6803
  parentInjector = inject(Injector);
7138
6804
  eventBus = inject(EventBus);
6805
+ logger = inject(DYNAMIC_FORM_LOGGER);
7139
6806
  // ─────────────────────────────────────────────────────────────────────────────
7140
6807
  // Inputs
7141
6808
  // ─────────────────────────────────────────────────────────────────────────────
@@ -7151,21 +6818,18 @@ class ArrayFieldComponent {
7151
6818
  }, ...(ngDevMode ? [{ debugName: "fieldTemplate" }] : []));
7152
6819
  arrayFieldTrees = computed(() => {
7153
6820
  const arrayKey = this.field().key;
7154
- const parentForm = this.parentFieldSignalContext.form();
6821
+ const parentForm = this.parentFieldSignalContext.form;
7155
6822
  const arrayValue = getArrayValue(this.parentFieldSignalContext.value(), arrayKey);
7156
6823
  if (arrayValue.length === 0)
7157
6824
  return [];
7158
- // Get the array FieldTree from the parent form
7159
- const arrayFieldTree = getChildFieldTree(parentForm, arrayKey);
6825
+ const arrayFieldTree = parentForm[arrayKey];
7160
6826
  if (!arrayFieldTree)
7161
6827
  return arrayValue.map(() => null);
7162
6828
  // Access array items via bracket notation - Angular Signal Forms arrays support this
7163
- // The type is ReadonlyArrayLike<MaybeFieldTree<U, number>>
7164
- const arrayWithIndex = arrayFieldTree;
7165
6829
  const items = [];
7166
6830
  for (let i = 0; i < arrayValue.length; i++) {
7167
- // Access item FieldTree directly via bracket notation
7168
- const itemFieldTree = arrayWithIndex[i];
6831
+ // Access item FieldTree directly via numeric indexing (ArrayFieldTree supports this)
6832
+ const itemFieldTree = arrayFieldTree[i];
7169
6833
  items.push(itemFieldTree ?? null);
7170
6834
  }
7171
6835
  return items;
@@ -7178,27 +6842,35 @@ class ArrayFieldComponent {
7178
6842
  itemOrderSignal = linkedSignal(() => this.resolvedItemsSignal().map((item) => item.id), ...(ngDevMode ? [{ debugName: "itemOrderSignal" }] : []));
7179
6843
  resolvedItems = linkedSignal(() => this.resolvedItemsSignal(), ...(ngDevMode ? [{ debugName: "resolvedItems" }] : []));
7180
6844
  // ─────────────────────────────────────────────────────────────────────────────
7181
- // Effects
7182
- // ─────────────────────────────────────────────────────────────────────────────
7183
- syncFieldsOnArrayChange = explicitEffect([this.arrayFieldTrees], ([fieldTrees]) => {
7184
- this.updateVersion.update((v) => v + 1);
7185
- const currentVersion = this.updateVersion();
7186
- this.performDifferentialUpdate(fieldTrees, currentVersion);
7187
- });
7188
- // ─────────────────────────────────────────────────────────────────────────────
7189
- // Event Handlers
6845
+ // Constructor
7190
6846
  // ─────────────────────────────────────────────────────────────────────────────
7191
- handleAddItem = this.eventBus
7192
- .on('add-array-item')
7193
- .pipe(takeUntilDestroyed(), filter((event) => event.arrayKey === this.key()))
7194
- .subscribe((event) => this.addItem(event.field ?? this.fieldTemplate(), event.index));
7195
- handleRemoveItem = this.eventBus
7196
- .on('remove-array-item')
7197
- .pipe(takeUntilDestroyed(), filter((event) => event.arrayKey === this.key()))
7198
- .subscribe((event) => this.removeItem(event.index));
6847
+ constructor() {
6848
+ this.setupEffects();
6849
+ this.setupEventHandlers();
6850
+ }
7199
6851
  // ─────────────────────────────────────────────────────────────────────────────
7200
6852
  // Private Methods
7201
6853
  // ─────────────────────────────────────────────────────────────────────────────
6854
+ setupEffects() {
6855
+ // Sync field components when array data changes
6856
+ explicitEffect([this.arrayFieldTrees], ([fieldTrees]) => {
6857
+ this.updateVersion.update((v) => v + 1);
6858
+ const currentVersion = this.updateVersion();
6859
+ this.performDifferentialUpdate(fieldTrees, currentVersion);
6860
+ });
6861
+ }
6862
+ setupEventHandlers() {
6863
+ // Handle add item events
6864
+ this.eventBus
6865
+ .on('add-array-item')
6866
+ .pipe(takeUntilDestroyed(), filter((event) => event.arrayKey === this.key()))
6867
+ .subscribe((event) => this.addItem(event.field ?? this.fieldTemplate(), event.index));
6868
+ // Handle remove item events
6869
+ this.eventBus
6870
+ .on('remove-array-item')
6871
+ .pipe(takeUntilDestroyed(), filter((event) => event.arrayKey === this.key()))
6872
+ .subscribe((event) => this.removeItem(event.index));
6873
+ }
7202
6874
  performDifferentialUpdate(fieldTrees, updateId) {
7203
6875
  const resolvedItems = this.resolvedItemsSignal();
7204
6876
  const operation = determineDifferentialOperation(resolvedItems, fieldTrees.length);
@@ -7270,13 +6942,13 @@ class ArrayFieldComponent {
7270
6942
  }
7271
6943
  addItem(fieldTemplate, index) {
7272
6944
  if (!fieldTemplate) {
7273
- console.error(`[Dynamic Forms] Cannot add item to array '${this.field().key}': no field template provided. ` +
6945
+ this.logger.error(`Cannot add item to array '${this.field().key}': no field template provided. ` +
7274
6946
  'Ensure the array field has a fields property with at least one field definition.');
7275
6947
  return;
7276
6948
  }
7277
6949
  const arrayKey = this.field().key;
7278
- const formObject = this.parentFieldSignalContext.form();
7279
- const currentValue = formObject.value();
6950
+ const parentForm = this.parentFieldSignalContext.form;
6951
+ const currentValue = parentForm().value();
7280
6952
  const currentArray = getArrayValue(currentValue, arrayKey);
7281
6953
  const insertIndex = index !== undefined ? Math.min(index, currentArray.length) : currentArray.length;
7282
6954
  const rawValue = getFieldDefaultValue(fieldTemplate, this.rawFieldRegistry());
@@ -7286,21 +6958,25 @@ class ArrayFieldComponent {
7286
6958
  const value = valueHandling === 'include' && fieldTemplate.key ? { [fieldTemplate.key]: rawValue } : rawValue;
7287
6959
  const newArray = [...currentArray];
7288
6960
  newArray.splice(insertIndex, 0, value);
6961
+ // Update the parent form with the new array value
6962
+ // The `as any` is required due to Angular Signal Forms' complex conditional types
7289
6963
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7290
- formObject.value.set({ ...currentValue, [arrayKey]: newArray });
6964
+ parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
7291
6965
  }
7292
6966
  removeItem(index) {
7293
6967
  const arrayKey = this.field().key;
7294
- const formObject = this.parentFieldSignalContext.form();
7295
- const currentValue = formObject.value();
6968
+ const parentForm = this.parentFieldSignalContext.form;
6969
+ const currentValue = parentForm().value();
7296
6970
  const currentArray = getArrayValue(currentValue, arrayKey);
7297
6971
  if (currentArray.length === 0)
7298
6972
  return;
7299
6973
  const removeIndex = index !== undefined ? Math.min(index, currentArray.length - 1) : currentArray.length - 1;
7300
6974
  const newArray = [...currentArray];
7301
6975
  newArray.splice(removeIndex, 1);
6976
+ // Update the parent form with the new array value
6977
+ // The `as any` is required due to Angular Signal Forms' complex conditional types
7302
6978
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7303
- formObject.value.set({ ...currentValue, [arrayKey]: newArray });
6979
+ parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
7304
6980
  }
7305
6981
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7306
6982
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.5", type: ArrayFieldComponent, isStandalone: true, selector: "array-field", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()" }, classAttribute: "df-field df-array" }, ngImport: i0, template: `
@@ -7320,7 +6996,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImpor
7320
6996
  '[id]': '`${key()}`',
7321
6997
  '[attr.data-testid]': 'key()',
7322
6998
  }, changeDetection: ChangeDetectionStrategy.OnPush, 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"] }]
7323
- }], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }] } });
6999
+ }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }] } });
7324
7000
 
7325
7001
  var arrayField_component = /*#__PURE__*/Object.freeze({
7326
7002
  __proto__: null,
@@ -7328,6 +7004,103 @@ var arrayField_component = /*#__PURE__*/Object.freeze({
7328
7004
  default: ArrayFieldComponent
7329
7005
  });
7330
7006
 
7007
+ /**
7008
+ * Get the length of an array FieldTree.
7009
+ */
7010
+ function getArrayLength(arrayFieldTree) {
7011
+ const value = arrayFieldTree().value();
7012
+ return Array.isArray(value) ? value.length : 0;
7013
+ }
7014
+
7015
+ /**
7016
+ * Interpolates {{param}} placeholders in message with error values
7017
+ *
7018
+ * @param message - Message template with {{param}} placeholders
7019
+ * @param error - Validation error containing parameter values
7020
+ * @returns Message with interpolated parameters
7021
+ *
7022
+ * @example
7023
+ * interpolateParams("Min value is {{min}}", { kind: 'min', min: 5 })
7024
+ * // Returns: "Min value is 5"
7025
+ */
7026
+ function interpolateParams(message, error) {
7027
+ let result = message;
7028
+ const params = extractErrorParams(error);
7029
+ Object.entries(params).forEach(([key, value]) => {
7030
+ const placeholder = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
7031
+ result = result.replace(placeholder, safeToString(value));
7032
+ });
7033
+ return result;
7034
+ }
7035
+ /**
7036
+ * Safely converts a value to a string, handling complex objects
7037
+ * @internal
7038
+ */
7039
+ function safeToString(value) {
7040
+ if (value === null || value === undefined) {
7041
+ return '';
7042
+ }
7043
+ // Handle primitives directly
7044
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
7045
+ return String(value);
7046
+ }
7047
+ // Handle RegExp (has its own toString)
7048
+ if (value instanceof RegExp) {
7049
+ return value.toString();
7050
+ }
7051
+ // Handle Date
7052
+ if (value instanceof Date) {
7053
+ return value.toISOString();
7054
+ }
7055
+ // Handle arrays
7056
+ if (Array.isArray(value)) {
7057
+ return value.join(', ');
7058
+ }
7059
+ // For objects, try JSON.stringify first, fall back to empty string
7060
+ try {
7061
+ // Check if object has a custom toString that's not Object.prototype.toString
7062
+ if (typeof value === 'object' && value.toString !== Object.prototype.toString) {
7063
+ return value.toString();
7064
+ }
7065
+ // Otherwise use JSON representation
7066
+ return JSON.stringify(value);
7067
+ }
7068
+ catch {
7069
+ // If JSON.stringify fails (circular reference, etc.), return empty string
7070
+ return '[object]';
7071
+ }
7072
+ }
7073
+ /**
7074
+ * Extracts parameter values from validation error
7075
+ * @internal
7076
+ */
7077
+ function extractErrorParams(error) {
7078
+ const params = {};
7079
+ // Common validation error parameters
7080
+ if ('min' in error)
7081
+ params.min = error.min;
7082
+ if ('max' in error)
7083
+ params.max = error.max;
7084
+ if ('requiredLength' in error)
7085
+ params.requiredLength = error.requiredLength;
7086
+ if ('actualLength' in error)
7087
+ params.actualLength = error.actualLength;
7088
+ if ('pattern' in error)
7089
+ params.pattern = error.pattern;
7090
+ if ('actual' in error)
7091
+ params.actual = error.actual;
7092
+ if ('expected' in error)
7093
+ params.expected = error.expected;
7094
+ // Include all custom properties from error object (for custom validators)
7095
+ // Skip 'kind' as it's the error type identifier, not a parameter
7096
+ Object.keys(error).forEach((key) => {
7097
+ if (key !== 'kind' && !(key in params)) {
7098
+ params[key] = error[key];
7099
+ }
7100
+ });
7101
+ return params;
7102
+ }
7103
+
7331
7104
  /**
7332
7105
  * @ng-forge/dynamic-forms
7333
7106
  *
@@ -7339,11 +7112,16 @@ var arrayField_component = /*#__PURE__*/Object.freeze({
7339
7112
  * - FormConfig and field definition types
7340
7113
  * - Event classes (SubmitEvent, PageChangeEvent, etc.)
7341
7114
  *
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
7115
+ * ## Integration API (for UI library authors)
7116
+ * Import from '@ng-forge/dynamic-forms/integration' for:
7117
+ * - Specific field types (InputField, SelectField, etc.)
7118
+ * - Field mappers (valueFieldMapper, checkboxFieldMapper, etc.)
7119
+ * - Error utilities (createResolvedErrorsSignal, shouldShowErrors)
7120
+ *
7121
+ * ## Testing API
7122
+ * Import from '@ng-forge/dynamic-forms/testing' for:
7123
+ * - Test utilities and harnesses
7124
+ * - Form config builders for testing
7347
7125
  */
7348
7126
  // ============================================================
7349
7127
  // PUBLIC API - For end users
@@ -7354,5 +7132,5 @@ var arrayField_component = /*#__PURE__*/Object.freeze({
7354
7132
  * Generated bundle index. Do not edit.
7355
7133
  */
7356
7134
 
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-Dcd04rfS.mjs.map
7135
+ export { omit as $, AddArrayItemEvent as A, buildBaseInputs as B, arrayFieldMapper as C, DYNAMIC_FORM_LOGGER as D, EventBus as E, FormClearEvent as F, groupFieldMapper as G, pageFieldMapper as H, rowFieldMapper as I, textFieldMapper as J, FIELD_REGISTRY as K, LogLevel as L, ARRAY_CONTEXT as M, NoopLogger as N, FIELD_SIGNAL_CONTEXT as O, PageChangeEvent as P, dynamicTextToObservable as Q, RemoveArrayItemEvent as R, SubmitEvent as S, ArrayFieldComponent as T, GroupFieldComponent as U, RowFieldComponent as V, applyValidator as W, applyValidators as X, getArrayLength as Y, resolveTokens as Z, BUILT_IN_FIELDS as _, DynamicTextPipe as a, interpolateParams as a0, DynamicForm as b, createFieldResolutionPipe as c, derivedFromDeferred as d, emitComponentInitialized as e, isFormStateCondition as f, resolveNextButtonDisabled as g, isCheckedField as h, injectFieldRegistry as i, isValueField as j, isRowField as k, isArrayField as l, isContainerField as m, isDisplayOnlyField as n, isGroupField as o, provideDynamicForm as p, isLeafField as q, resolveSubmitButtonDisabled as r, isPageField as s, isValueBearingField as t, FormResetEvent as u, validatePageNesting as v, withLogger as w, NextPageEvent as x, PreviousPageEvent as y, baseFieldMapper as z };
7136
+ //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-B_676NHl.mjs.map