@ng-forge/dynamic-forms 0.1.2 → 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 (19) hide show
  1. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +269 -0
  2. package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -0
  3. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-BWj75LVN.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-B_676NHl.mjs} +881 -1103
  4. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-B_676NHl.mjs.map +1 -0
  5. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-ZVrJUMNb.mjs → ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs} +15 -22
  6. package/fesm2022/ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs.map +1 -0
  7. package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-BenmCFcM.mjs +31 -0
  8. package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-BenmCFcM.mjs.map +1 -0
  9. package/fesm2022/ng-forge-dynamic-forms-testing.mjs +968 -0
  10. package/fesm2022/ng-forge-dynamic-forms-testing.mjs.map +1 -0
  11. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BKTbxAKn.mjs → ng-forge-dynamic-forms-text-field.component-CiYq73aF.mjs} +2 -2
  12. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BKTbxAKn.mjs.map → ng-forge-dynamic-forms-text-field.component-CiYq73aF.mjs.map} +1 -1
  13. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  14. package/package.json +9 -1
  15. package/types/ng-forge-dynamic-forms-integration.d.ts +315 -0
  16. package/types/ng-forge-dynamic-forms-testing.d.ts +565 -0
  17. package/types/ng-forge-dynamic-forms.d.ts +224 -479
  18. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BWj75LVN.mjs.map +0 -1
  19. package/fesm2022/ng-forge-dynamic-forms-page-field.component-ZVrJUMNb.mjs.map +0 -1
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- 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-ZVrJUMNb.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-ZVrJUMNb.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-ZVrJUMNb.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" }] : []));
@@ -5385,7 +5464,7 @@ class DynamicForm {
5385
5464
  submitted = outputFromObservable(this.eventBus.on('submit').pipe(switchMap(() => {
5386
5465
  const submissionConfig = this.config().submission;
5387
5466
  if (submissionConfig?.action) {
5388
- console.warn('[Dynamic Forms] Both `submission.action` and `(submitted)` output are configured. ' +
5467
+ this.logger.warn('Both `submission.action` and `(submitted)` output are configured. ' +
5389
5468
  'When using `submission.action`, the `(submitted)` output will not emit. ' +
5390
5469
  'Use either `submission.action` OR `(submitted)`, not both.');
5391
5470
  return EMPTY;
@@ -5429,12 +5508,12 @@ class DynamicForm {
5429
5508
  error: error instanceof Error ? error : new Error(String(error)),
5430
5509
  },
5431
5510
  ]);
5432
- 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}). ` +
5433
5512
  `Ensure the field type is registered in your field registry.`, error);
5434
5513
  },
5435
5514
  };
5436
5515
  return forkJoin(fields.map((f) => resolveField(f, context)));
5437
- }), map((fields) => fields.filter((f) => f !== undefined)), scan$1(reconcileFields, [])), { initialValue: [], injector: this.injector });
5516
+ }), map((fields) => fields.filter((f) => f !== undefined)), scan(reconcileFields, [])), { initialValue: [], injector: this.injector });
5438
5517
  // ─────────────────────────────────────────────────────────────────────────────
5439
5518
  // Constructor
5440
5519
  // ─────────────────────────────────────────────────────────────────────────────
@@ -5488,17 +5567,14 @@ class DynamicForm {
5488
5567
  });
5489
5568
  }
5490
5569
  setupEventHandlers() {
5491
- // Handle form reset events
5492
5570
  this.eventBus
5493
5571
  .on('form-reset')
5494
5572
  .pipe(takeUntilDestroyed())
5495
5573
  .subscribe(() => this.onFormReset());
5496
- // Handle form clear events
5497
5574
  this.eventBus
5498
5575
  .on('form-clear')
5499
5576
  .pipe(takeUntilDestroyed())
5500
5577
  .subscribe(() => this.onFormClear());
5501
- // Handle form submission
5502
5578
  createSubmissionHandler({
5503
5579
  eventBus: this.eventBus,
5504
5580
  configSignal: this.config,
@@ -5572,7 +5648,7 @@ class DynamicForm {
5572
5648
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
5573
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: `
5574
5650
  @if (formModeDetection().mode === 'paged') {
5575
- <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="$any(form())" [fieldSignalContext]="fieldSignalContext()"></div>
5651
+ <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
5576
5652
  } @else {
5577
5653
  @for (field of resolvedFields(); track field.key) {
5578
5654
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
@@ -5584,7 +5660,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImpor
5584
5660
  type: Component,
5585
5661
  args: [{ selector: 'form[dynamic-form]', imports: [NgComponentOutlet, PageOrchestratorComponent], template: `
5586
5662
  @if (formModeDetection().mode === 'paged') {
5587
- <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="$any(form())" [fieldSignalContext]="fieldSignalContext()"></div>
5663
+ <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
5588
5664
  } @else {
5589
5665
  @for (field of resolvedFields(); track field.key) {
5590
5666
  <ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
@@ -5681,8 +5757,9 @@ function pageFieldMapper(fieldDef) {
5681
5757
  * @returns Signal containing Record of input names to values for ngComponentOutlet
5682
5758
  */
5683
5759
  function textFieldMapper(fieldDef) {
5684
- const rootFormRegistry = inject(RootFormRegistryService);
5760
+ const fieldContextRegistry = inject(FieldContextRegistryService);
5685
5761
  const functionRegistry = inject(FunctionRegistryService);
5762
+ const logger = inject(DYNAMIC_FORM_LOGGER);
5686
5763
  // Build base inputs (static, from field definition)
5687
5764
  const baseInputs = buildBaseInputs(fieldDef);
5688
5765
  // Create computed signal for hidden state based on logic configuration
@@ -5692,16 +5769,8 @@ function textFieldMapper(fieldDef) {
5692
5769
  const inputs = { ...baseInputs };
5693
5770
  // Evaluate hidden logic if present
5694
5771
  if (hiddenLogic.length > 0) {
5695
- const formValue = rootFormRegistry.getFormValue();
5696
- const customFunctions = functionRegistry.getCustomFunctions();
5697
- // Create evaluation context without a specific field value
5698
- // (text fields don't have their own value in the form)
5699
- const evaluationContext = {
5700
- fieldValue: undefined,
5701
- formValue,
5702
- fieldPath: fieldDef.key,
5703
- customFunctions,
5704
- };
5772
+ // Use centralized context creation for display-only components
5773
+ const evaluationContext = fieldContextRegistry.createDisplayOnlyContext(fieldDef.key, functionRegistry.getCustomFunctions());
5705
5774
  // Evaluate all hidden logic conditions - if ANY is true, the field is hidden
5706
5775
  inputs['hidden'] = hiddenLogic.some((logic) => {
5707
5776
  if (typeof logic.condition === 'boolean') {
@@ -5766,13 +5835,13 @@ const BUILT_IN_FIELDS = [
5766
5835
  },
5767
5836
  {
5768
5837
  name: 'page',
5769
- loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-ZVrJUMNb.mjs'),
5838
+ loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-YHFQcpCS.mjs'),
5770
5839
  mapper: pageFieldMapper,
5771
5840
  valueHandling: 'flatten',
5772
5841
  },
5773
5842
  {
5774
5843
  name: 'text',
5775
- loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-BKTbxAKn.mjs'),
5844
+ loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-CiYq73aF.mjs'),
5776
5845
  mapper: textFieldMapper,
5777
5846
  valueHandling: 'exclude',
5778
5847
  },
@@ -5784,7 +5853,7 @@ const BUILT_IN_FIELDS = [
5784
5853
  * This function creates environment providers that can be used at application or route level
5785
5854
  * to register field types. It provides type-safe field registration with automatic type inference.
5786
5855
  *
5787
- * @param fieldTypes - Custom field type definitions to register alongside built-in types
5856
+ * @param items - Field type definitions and/or features (like withLogger)
5788
5857
  * @returns Environment providers for dependency injection with type inference
5789
5858
  *
5790
5859
  * @example
@@ -5799,6 +5868,19 @@ const BUILT_IN_FIELDS = [
5799
5868
  *
5800
5869
  * @example
5801
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
5802
5884
  * // Route-level setup
5803
5885
  * const routes: Routes = [
5804
5886
  * {
@@ -5825,7 +5907,10 @@ const BUILT_IN_FIELDS = [
5825
5907
  *
5826
5908
  * @public
5827
5909
  */
5828
- 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);
5829
5914
  const fields = [...BUILT_IN_FIELDS, ...fieldTypes];
5830
5915
  // Extract config providers from field type arrays
5831
5916
  const configProviders = [];
@@ -5835,15 +5920,21 @@ function provideDynamicForm(...fieldTypes) {
5835
5920
  configProviders.push(...fieldTypeWithConfig.__configProviders);
5836
5921
  }
5837
5922
  });
5923
+ // Extract providers from features
5924
+ const featureProviders = [];
5925
+ features.forEach((feature) => {
5926
+ featureProviders.push(...feature.ɵproviders);
5927
+ });
5838
5928
  return makeEnvironmentProviders([
5839
5929
  {
5840
5930
  provide: FIELD_REGISTRY,
5841
5931
  useFactory: () => {
5932
+ const logger = inject(DYNAMIC_FORM_LOGGER);
5842
5933
  const registry = new Map();
5843
5934
  // Add custom field types
5844
5935
  fields.forEach((fieldType) => {
5845
5936
  if (registry.has(fieldType.name)) {
5846
- console.warn(`[Dynamic Forms] Field type "${fieldType.name}" is already registered. Overwriting.`);
5937
+ logger.warn(`Field type "${fieldType.name}" is already registered. Overwriting.`);
5847
5938
  }
5848
5939
  registry.set(fieldType.name, fieldType);
5849
5940
  });
@@ -5851,9 +5942,24 @@ function provideDynamicForm(...fieldTypes) {
5851
5942
  },
5852
5943
  },
5853
5944
  ...configProviders,
5945
+ ...featureProviders,
5854
5946
  ]);
5855
5947
  }
5856
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
+ };
5857
5963
  /**
5858
5964
  * Default options for submit button disabled behavior.
5859
5965
  */
@@ -5910,6 +6016,7 @@ function evaluateLogicCondition(condition, ctx) {
5910
6016
  fieldValue: undefined,
5911
6017
  formValue,
5912
6018
  fieldPath: '',
6019
+ logger: ctx.logger ?? noOpLogger,
5913
6020
  };
5914
6021
  return evaluateCondition(condition, evaluationContext);
5915
6022
  }
@@ -6094,244 +6201,6 @@ function resolveTokens(args, context) {
6094
6201
  });
6095
6202
  }
6096
6203
 
6097
- /**
6098
- * Type guard to check if a value has the FormInternals structure.
6099
- */
6100
- function isFormInternals(value) {
6101
- return (typeof value === 'object' &&
6102
- value !== null &&
6103
- 'structure' in value &&
6104
- typeof value.structure?.childrenMap === 'function');
6105
- }
6106
- /**
6107
- * Type guard that unwraps signals and checks for FormInternals structure.
6108
- * Use this when the input might be a signal wrapping FormInternals.
6109
- */
6110
- function hasFormStructure(form) {
6111
- const unwrapped = isSignal(form) ? form() : form;
6112
- return isFormInternals(unwrapped);
6113
- }
6114
- /**
6115
- * Unwraps a signal if needed and returns the FormInternals or null.
6116
- */
6117
- function unwrapFormInput(form) {
6118
- const unwrapped = isSignal(form) ? form() : form;
6119
- return isFormInternals(unwrapped) ? unwrapped : null;
6120
- }
6121
- function getChildrenMap(form) {
6122
- const target = unwrapFormInput(form);
6123
- if (!target) {
6124
- return null;
6125
- }
6126
- const result = target.structure.childrenMap();
6127
- const normalizedMap = new Map();
6128
- for (const [key, childData] of result.byPropertyKey) {
6129
- normalizedMap.set(key, childData.reader);
6130
- }
6131
- return normalizedMap;
6132
- }
6133
- function getFieldProxy(node) {
6134
- if (!isFormInternals(node)) {
6135
- return null;
6136
- }
6137
- return node.fieldProxy ?? null;
6138
- }
6139
- function getChildFieldTree(form, key) {
6140
- const childrenMap = getChildrenMap(form);
6141
- return childrenMap?.get(key) ?? null;
6142
- }
6143
-
6144
- /**
6145
- * Resolves the field proxy and default validation messages from the form context.
6146
- * Must be called within an injection context.
6147
- *
6148
- * @param fieldKey The key of the field to resolve
6149
- * @returns The resolved context with field proxy and validation messages
6150
- */
6151
- function resolveValueFieldContext(fieldKey) {
6152
- const context = inject(FIELD_SIGNAL_CONTEXT);
6153
- // Get form-level validation messages
6154
- const defaultValidationMessages = context.defaultValidationMessages;
6155
- // Get the form root to access field proxy
6156
- const formRoot = context.form();
6157
- const childrenMap = getChildrenMap(formRoot);
6158
- // Resolve field proxy
6159
- let fieldProxy = undefined;
6160
- if (!childrenMap) {
6161
- // No childrenMap - the form might be a FormRecord where fields are direct properties
6162
- // First check if formRoot itself has fieldProxy (for FieldTree items)
6163
- const rootFieldProxy = getFieldProxy(formRoot);
6164
- if (rootFieldProxy) {
6165
- fieldProxy = rootFieldProxy;
6166
- }
6167
- else {
6168
- // Try accessing the field as a direct property on formRoot (FormRecord case)
6169
- // This handles forms created with form(entity, schema) where fields are accessible as formRoot[key]
6170
- const formField = formRoot[fieldKey];
6171
- const resolvedProxy = getFieldProxy(formField);
6172
- if (resolvedProxy) {
6173
- fieldProxy = resolvedProxy;
6174
- }
6175
- else if (isSignal(formField) || formField) {
6176
- // The field itself might be the proxy (signal), or might have a different structure
6177
- // Try using the field directly as the proxy
6178
- fieldProxy = formField;
6179
- }
6180
- }
6181
- }
6182
- else {
6183
- // Standard field access for non-array keys via childrenMap lookup
6184
- const formField = childrenMap.get(fieldKey);
6185
- if (formField) {
6186
- fieldProxy = formField;
6187
- }
6188
- }
6189
- return { fieldProxy, defaultValidationMessages };
6190
- }
6191
- /**
6192
- * Builds the base inputs for a value field.
6193
- * This is a helper function used by valueFieldMapper and specialized mappers.
6194
- *
6195
- * @param fieldDef The value field definition
6196
- * @param ctx The resolved value field context
6197
- * @returns Record of input names to values
6198
- */
6199
- function buildValueFieldInputs(fieldDef, ctx) {
6200
- const omittedFields = omit(fieldDef, ['value']);
6201
- const baseInputs = buildBaseInputs(omittedFields);
6202
- const inputs = {
6203
- ...baseInputs,
6204
- // Always pass validationMessages (or empty object) - required for error display signals
6205
- validationMessages: fieldDef.validationMessages ?? {},
6206
- };
6207
- // Value field specific properties
6208
- if (fieldDef.placeholder !== undefined) {
6209
- inputs['placeholder'] = fieldDef.placeholder;
6210
- }
6211
- if (ctx.defaultValidationMessages !== undefined) {
6212
- inputs['defaultValidationMessages'] = ctx.defaultValidationMessages;
6213
- }
6214
- if (ctx.fieldProxy !== undefined) {
6215
- inputs['field'] = ctx.fieldProxy;
6216
- }
6217
- return inputs;
6218
- }
6219
- /**
6220
- * Maps a value field definition to component inputs.
6221
- *
6222
- * Value fields are input fields that contribute to the form's value (text, number, etc.).
6223
- * This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field proxy.
6224
- *
6225
- * For fields with specific properties (select, datepicker, textarea, slider), use the specialized mappers:
6226
- * - selectFieldMapper: for fields with options (select, radio, multi-checkbox)
6227
- * - datepickerFieldMapper: for fields with minDate, maxDate, startAt
6228
- * - textareaFieldMapper: for fields with rows, cols
6229
- * - sliderFieldMapper: for fields with minValue, maxValue, step
6230
- *
6231
- * @param fieldDef The value field definition
6232
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6233
- */
6234
- function valueFieldMapper(fieldDef) {
6235
- const ctx = resolveValueFieldContext(fieldDef.key);
6236
- return computed(() => buildValueFieldInputs(fieldDef, ctx));
6237
- }
6238
-
6239
- /**
6240
- * Maps a checkbox/toggle field definition to component inputs.
6241
- *
6242
- * Checkbox fields are checked fields that contribute to the form's value as boolean.
6243
- * This mapper injects FIELD_SIGNAL_CONTEXT to access the form structure and retrieve the field proxy.
6244
- *
6245
- * @param fieldDef The checkbox field definition
6246
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6247
- */
6248
- function checkboxFieldMapper(fieldDef) {
6249
- const context = inject(FIELD_SIGNAL_CONTEXT);
6250
- const omittedFields = omit(fieldDef, ['value']);
6251
- // Build base inputs (static, from field definition)
6252
- const baseInputs = buildBaseInputs(omittedFields);
6253
- // Get form-level validation messages (static)
6254
- const defaultValidationMessages = context.defaultValidationMessages;
6255
- // Get the form root to access field proxy
6256
- const formRoot = context.form();
6257
- const childrenMap = getChildrenMap(formRoot);
6258
- // Resolve field proxy (static - determined once during mapping)
6259
- let fieldProxy = undefined;
6260
- if (childrenMap) {
6261
- const formField = childrenMap.get(fieldDef.key);
6262
- if (formField) {
6263
- fieldProxy = formField;
6264
- }
6265
- }
6266
- // Return computed signal for reactive updates
6267
- return computed(() => {
6268
- const inputs = {
6269
- ...baseInputs,
6270
- // Always pass validationMessages (or empty object) - required for error display signals
6271
- validationMessages: fieldDef.validationMessages ?? {},
6272
- };
6273
- // Checked field specific property: placeholder
6274
- if (fieldDef.placeholder !== undefined) {
6275
- inputs['placeholder'] = fieldDef.placeholder;
6276
- }
6277
- if (defaultValidationMessages !== undefined) {
6278
- inputs['defaultValidationMessages'] = defaultValidationMessages;
6279
- }
6280
- if (fieldProxy !== undefined) {
6281
- inputs['field'] = fieldProxy;
6282
- }
6283
- return inputs;
6284
- });
6285
- }
6286
-
6287
- /**
6288
- * Maps a field with options (select, radio, multi-checkbox) to component inputs.
6289
- *
6290
- * Extends the base value field mapper by adding the options property.
6291
- *
6292
- * @param fieldDef The field definition with options
6293
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6294
- */
6295
- function optionsFieldMapper(fieldDef) {
6296
- const ctx = resolveValueFieldContext(fieldDef.key);
6297
- return computed(() => {
6298
- // Cast to BaseValueField - safe because all FieldWithOptions types extend BaseValueField
6299
- const inputs = buildValueFieldInputs(fieldDef, ctx);
6300
- // Add options property
6301
- inputs['options'] = fieldDef.options;
6302
- return inputs;
6303
- });
6304
- }
6305
-
6306
- /**
6307
- * Maps a datepicker field to component inputs.
6308
- *
6309
- * Extends the base value field mapper by adding datepicker-specific properties:
6310
- * - minDate: Minimum selectable date
6311
- * - maxDate: Maximum selectable date
6312
- * - startAt: Initial date to display when opening the picker
6313
- *
6314
- * @param fieldDef The datepicker field definition
6315
- * @returns Signal containing Record of input names to values for ngComponentOutlet
6316
- */
6317
- function datepickerFieldMapper(fieldDef) {
6318
- const ctx = resolveValueFieldContext(fieldDef.key);
6319
- return computed(() => {
6320
- const inputs = buildValueFieldInputs(fieldDef, ctx);
6321
- // Add datepicker-specific properties
6322
- if (fieldDef.minDate !== undefined) {
6323
- inputs['minDate'] = fieldDef.minDate;
6324
- }
6325
- if (fieldDef.maxDate !== undefined) {
6326
- inputs['maxDate'] = fieldDef.maxDate;
6327
- }
6328
- if (fieldDef.startAt !== undefined) {
6329
- inputs['startAt'] = fieldDef.startAt;
6330
- }
6331
- return inputs;
6332
- });
6333
- }
6334
-
6335
6204
  /**
6336
6205
  * Converts DynamicText (string | Observable | Signal) to Observable<string>
6337
6206
  * Unifies all three types into a consistent Observable stream
@@ -6353,187 +6222,6 @@ function dynamicTextToObservable(value, injector) {
6353
6222
  return of(String(value));
6354
6223
  }
6355
6224
 
6356
- /**
6357
- * Computed signal that determines if errors should be displayed
6358
- * Based on field's invalid, touched, and error count
6359
- *
6360
- * @param field - Signal containing FieldTree
6361
- * @returns Signal<boolean> - True if errors should be displayed
6362
- */
6363
- function shouldShowErrors(field) {
6364
- return computed(() => {
6365
- const fieldTree = field();
6366
- const control = fieldTree();
6367
- return control.invalid() && control.touched() && control.errors().length > 0;
6368
- });
6369
- }
6370
-
6371
- /**
6372
- * Interpolates {{param}} placeholders in message with error values
6373
- *
6374
- * @param message - Message template with {{param}} placeholders
6375
- * @param error - Validation error containing parameter values
6376
- * @returns Message with interpolated parameters
6377
- *
6378
- * @example
6379
- * interpolateParams("Min value is {{min}}", { kind: 'min', min: 5 })
6380
- * // Returns: "Min value is 5"
6381
- */
6382
- function interpolateParams(message, error) {
6383
- let result = message;
6384
- const params = extractErrorParams(error);
6385
- Object.entries(params).forEach(([key, value]) => {
6386
- const placeholder = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
6387
- result = result.replace(placeholder, safeToString(value));
6388
- });
6389
- return result;
6390
- }
6391
- /**
6392
- * Safely converts a value to a string, handling complex objects
6393
- * @internal
6394
- */
6395
- function safeToString(value) {
6396
- if (value === null || value === undefined) {
6397
- return '';
6398
- }
6399
- // Handle primitives directly
6400
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
6401
- return String(value);
6402
- }
6403
- // Handle RegExp (has its own toString)
6404
- if (value instanceof RegExp) {
6405
- return value.toString();
6406
- }
6407
- // Handle Date
6408
- if (value instanceof Date) {
6409
- return value.toISOString();
6410
- }
6411
- // Handle arrays
6412
- if (Array.isArray(value)) {
6413
- return value.join(', ');
6414
- }
6415
- // For objects, try JSON.stringify first, fall back to empty string
6416
- try {
6417
- // Check if object has a custom toString that's not Object.prototype.toString
6418
- if (typeof value === 'object' && value.toString !== Object.prototype.toString) {
6419
- return value.toString();
6420
- }
6421
- // Otherwise use JSON representation
6422
- return JSON.stringify(value);
6423
- }
6424
- catch {
6425
- // If JSON.stringify fails (circular reference, etc.), return empty string
6426
- return '[object]';
6427
- }
6428
- }
6429
- /**
6430
- * Extracts parameter values from validation error
6431
- * @internal
6432
- */
6433
- function extractErrorParams(error) {
6434
- const params = {};
6435
- // Common validation error parameters
6436
- if ('min' in error)
6437
- params.min = error.min;
6438
- if ('max' in error)
6439
- params.max = error.max;
6440
- if ('requiredLength' in error)
6441
- params.requiredLength = error.requiredLength;
6442
- if ('actualLength' in error)
6443
- params.actualLength = error.actualLength;
6444
- if ('pattern' in error)
6445
- params.pattern = error.pattern;
6446
- if ('actual' in error)
6447
- params.actual = error.actual;
6448
- if ('expected' in error)
6449
- params.expected = error.expected;
6450
- // Include all custom properties from error object (for custom validators)
6451
- // Skip 'kind' as it's the error type identifier, not a parameter
6452
- Object.keys(error).forEach((key) => {
6453
- if (key !== 'kind' && !(key in params)) {
6454
- params[key] = error[key];
6455
- }
6456
- });
6457
- return params;
6458
- }
6459
-
6460
- /**
6461
- * Factory function that creates a signal of resolved validation errors
6462
- * Handles async resolution of DynamicText validation messages
6463
- *
6464
- * @param field - Signal containing FieldTree
6465
- * @param validationMessages - Signal containing custom field-level validation messages
6466
- * @param defaultValidationMessages - Signal containing default validation messages (fallback)
6467
- * @param injector - Optional injector for DynamicText resolution
6468
- * @returns Signal<ResolvedError[]> - Reactively resolved error messages
6469
- *
6470
- * @example
6471
- * ```typescript
6472
- * readonly resolvedErrors = createResolvedErrorsSignal(
6473
- * this.field,
6474
- * this.validationMessages,
6475
- * this.defaultValidationMessages
6476
- * );
6477
- * ```
6478
- */
6479
- function createResolvedErrorsSignal(field, validationMessages, defaultValidationMessages = signal(undefined), injector = inject(Injector)) {
6480
- // Ensure validationMessages is never undefined (mappers pass {} if not defined)
6481
- const messages = computed(() => validationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "messages" }] : []));
6482
- const defaultMessages = computed(() => defaultValidationMessages() ?? {}, ...(ngDevMode ? [{ debugName: "defaultMessages" }] : []));
6483
- // Create a computed signal that reads the actual errors from the field
6484
- // This ensures the signal tracks changes to field().errors(), not just the field reference
6485
- const errors = computed(() => {
6486
- const control = field()();
6487
- return control.errors();
6488
- }, ...(ngDevMode ? [{ debugName: "errors" }] : []));
6489
- // Convert signals to observables using toObservable with injector
6490
- const errors$ = toObservable(errors, { injector });
6491
- const messages$ = toObservable(messages, { injector });
6492
- const defaultMessages$ = toObservable(defaultMessages, { injector });
6493
- // Combine observables and process errors
6494
- const resolvedErrors$ = combineLatest([errors$, messages$, defaultMessages$]).pipe(switchMap$1(([currentErrors, msgs, defaultMsgs]) => {
6495
- // No errors - return empty array
6496
- if (!currentErrors || currentErrors.length === 0) {
6497
- return of([]);
6498
- }
6499
- // Create observable for each error's resolved message
6500
- const errorResolvers = currentErrors.map((error) => resolveErrorMessage(error, msgs, defaultMsgs, injector));
6501
- // Combine all error message observables into single array emission, filtering out nulls
6502
- return errorResolvers.length > 0
6503
- ? combineLatest(errorResolvers).pipe(map$1((errors) => errors.filter((e) => e !== null)))
6504
- : of([]);
6505
- }));
6506
- // Convert observable back to signal using toSignal with injector
6507
- // toSignal properly handles the injection context and manages subscriptions
6508
- return toSignal(resolvedErrors$, { initialValue: [], injector });
6509
- }
6510
- /**
6511
- * Resolves a single error message from DynamicText sources with fallback logic
6512
- * Priority: field-level message → default message → no message (logs warning)
6513
- * @internal
6514
- */
6515
- function resolveErrorMessage(error, fieldMessages, defaultMessages, injector) {
6516
- // Check for field-level custom message first
6517
- const fieldMessage = fieldMessages[error.kind];
6518
- // Fall back to default message if no field-level message exists
6519
- const defaultMessage = defaultMessages[error.kind];
6520
- // Determine which message to use: field-level → default
6521
- const messageToUse = fieldMessage ?? defaultMessage;
6522
- // If no message found, log warning and return null (will be filtered out)
6523
- if (!messageToUse) {
6524
- console.warn(`[Dynamic Forms] No validation message found for error kind "${error.kind}". ` +
6525
- `Please provide a message via field-level validationMessages or form-level defaultValidationMessages.`);
6526
- return of(null);
6527
- }
6528
- // Convert DynamicText to Observable
6529
- const messageObservable = dynamicTextToObservable(messageToUse, injector);
6530
- // Apply parameter interpolation to support {{param}} syntax
6531
- return messageObservable.pipe(map$1((msg) => ({
6532
- kind: error.kind,
6533
- message: interpolateParams(msg, error),
6534
- })));
6535
- }
6536
-
6537
6225
  /**
6538
6226
  * Pipe that handles dynamic text resolution with support for static strings,
6539
6227
  * Observables, and Signals.
@@ -6623,6 +6311,7 @@ class RowFieldComponent {
6623
6311
  fieldRegistry = injectFieldRegistry();
6624
6312
  injector = inject(Injector);
6625
6313
  eventBus = inject(EventBus);
6314
+ logger = inject(DYNAMIC_FORM_LOGGER);
6626
6315
  // ─────────────────────────────────────────────────────────────────────────────
6627
6316
  // Inputs
6628
6317
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6656,12 +6345,12 @@ class RowFieldComponent {
6656
6345
  destroyRef: this.destroyRef,
6657
6346
  onError: (fieldDef, error) => {
6658
6347
  const fieldKey = fieldDef.key || '<no key>';
6659
- 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}) ` +
6660
6349
  `within row '${rowKey}'. Ensure the field type is registered in your field registry.`, error);
6661
6350
  },
6662
6351
  };
6663
6352
  return forkJoin(fields.map((f) => resolveField(f, context)));
6664
- }), 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 });
6665
6354
  // ─────────────────────────────────────────────────────────────────────────────
6666
6355
  // Constructor
6667
6356
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6722,12 +6411,22 @@ class GroupFieldComponent {
6722
6411
  parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
6723
6412
  injector = inject(Injector);
6724
6413
  eventBus = inject(EventBus);
6414
+ logger = inject(DYNAMIC_FORM_LOGGER);
6725
6415
  // ─────────────────────────────────────────────────────────────────────────────
6726
6416
  // Memoized Functions
6727
6417
  // ─────────────────────────────────────────────────────────────────────────────
6728
- memoizedFlattenFields = memoize((fields, registry) => flattenFields([...fields], registry), (fields, registry) => JSON.stringify(fields.map((f) => ({ key: f.key, type: f.type }))) + '_' + Array.from(registry.keys()).sort().join(','));
6729
- memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), (fields) => fields.map((f) => f.key).join(','));
6730
- memoizedDefaultValues = memoize((fieldsById, registry) => mapValues(fieldsById, (field) => getFieldDefaultValue(field, registry)), (fieldsById, registry) => Object.keys(fieldsById).sort().join(',') + '_' + Array.from(registry.keys()).sort().join(','));
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
+ });
6731
6430
  // ─────────────────────────────────────────────────────────────────────────────
6732
6431
  // Inputs
6733
6432
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6738,35 +6437,25 @@ class GroupFieldComponent {
6738
6437
  // ─────────────────────────────────────────────────────────────────────────────
6739
6438
  rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...(ngDevMode ? [{ debugName: "rawFieldRegistry" }] : []));
6740
6439
  formSetup = computed(() => {
6741
- try {
6742
- const groupField = this.field();
6743
- const registry = this.rawFieldRegistry();
6744
- if (groupField.fields && groupField.fields.length > 0) {
6745
- const flattenedFields = this.memoizedFlattenFields(groupField.fields, registry);
6746
- const fieldsById = this.memoizedKeyBy(flattenedFields);
6747
- const defaultValues = this.memoizedDefaultValues(fieldsById, registry);
6748
- return {
6749
- fields: flattenedFields,
6750
- originalFields: groupField.fields,
6751
- defaultValues,
6752
- registry,
6753
- };
6754
- }
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);
6755
6446
  return {
6756
- fields: [],
6757
- originalFields: [],
6758
- defaultValues: {},
6447
+ fields: flattenedFields,
6448
+ originalFields: groupField.fields,
6449
+ defaultValues,
6759
6450
  registry,
6760
6451
  };
6761
6452
  }
6762
- catch {
6763
- return {
6764
- fields: [],
6765
- originalFields: [],
6766
- defaultValues: {},
6767
- registry: this.rawFieldRegistry(),
6768
- };
6769
- }
6453
+ return {
6454
+ fields: [],
6455
+ originalFields: [],
6456
+ defaultValues: {},
6457
+ registry,
6458
+ };
6770
6459
  }, ...(ngDevMode ? [{ debugName: "formSetup" }] : []));
6771
6460
  defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : []));
6772
6461
  entity = linkedSignal(() => {
@@ -6796,36 +6485,26 @@ class GroupFieldComponent {
6796
6485
  touched = computed(() => this.form()().touched(), ...(ngDevMode ? [{ debugName: "touched" }] : []));
6797
6486
  errors = computed(() => this.form()().errors(), ...(ngDevMode ? [{ debugName: "errors" }] : []));
6798
6487
  disabled = computed(() => this.form()().disabled(), ...(ngDevMode ? [{ debugName: "disabled" }] : []));
6799
- /**
6800
- * Get the nested FieldTree from the parent form for this group.
6801
- * This allows child fields to update the parent form directly,
6802
- * avoiding the need for separate form synchronization.
6803
- */
6804
6488
  nestedFieldTree = computed(() => {
6805
- const parentForm = this.parentFieldSignalContext.form();
6489
+ const parentForm = this.parentFieldSignalContext.form;
6806
6490
  const groupKey = this.field().key;
6807
- 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;
6808
6496
  }, ...(ngDevMode ? [{ debugName: "nestedFieldTree" }] : []));
6809
6497
  groupInjector = computed(() => {
6810
- const nestedTree = this.nestedFieldTree();
6811
- // Use the nested FieldTree from parent if available, otherwise fall back to our own form
6812
- // The nested FieldTree ensures changes propagate directly to the parent form
6813
- const formToProvide = nestedTree ?? this.form();
6814
6498
  const groupFieldSignalContext = {
6815
6499
  injector: this.injector,
6816
6500
  value: this.parentFieldSignalContext.value,
6817
6501
  defaultValues: this.defaultValues,
6818
- form: (() => formToProvide),
6502
+ form: this.nestedFieldTree(),
6819
6503
  defaultValidationMessages: this.parentFieldSignalContext.defaultValidationMessages,
6820
6504
  };
6821
6505
  return Injector.create({
6822
6506
  parent: this.injector,
6823
- providers: [
6824
- {
6825
- provide: FIELD_SIGNAL_CONTEXT,
6826
- useValue: groupFieldSignalContext,
6827
- },
6828
- ],
6507
+ providers: [{ provide: FIELD_SIGNAL_CONTEXT, useValue: groupFieldSignalContext }],
6829
6508
  });
6830
6509
  }, ...(ngDevMode ? [{ debugName: "groupInjector" }] : []));
6831
6510
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6838,24 +6517,17 @@ class GroupFieldComponent {
6838
6517
  // Field Resolution
6839
6518
  // ─────────────────────────────────────────────────────────────────────────────
6840
6519
  fieldsSource = computed(() => this.formSetup().fields, ...(ngDevMode ? [{ debugName: "fieldsSource" }] : []));
6841
- resolvedFields = derivedFromDeferred(this.fieldsSource, pipe(switchMap((fields) => {
6842
- if (!fields || fields.length === 0) {
6843
- return of([]);
6844
- }
6845
- const groupKey = this.field().key;
6846
- const context = {
6847
- loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
6848
- registry: this.rawFieldRegistry(),
6849
- injector: this.groupInjector(),
6850
- destroyRef: this.destroyRef,
6851
- onError: (fieldDef, error) => {
6852
- const fieldKey = fieldDef.key || '<no key>';
6853
- console.error(`[Dynamic Forms] Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}) ` +
6854
- `within group '${groupKey}'. Ensure the field type is registered in your field registry.`, error);
6855
- },
6856
- };
6857
- return forkJoin(fields.map((f) => resolveField(f, context)));
6858
- }), map((fields) => fields.filter((f) => f !== undefined)), scan$1(reconcileFields, [])), { initialValue: [], injector: this.injector });
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 });
6859
6531
  // ─────────────────────────────────────────────────────────────────────────────
6860
6532
  // Constructor
6861
6533
  // ─────────────────────────────────────────────────────────────────────────────
@@ -6961,8 +6633,8 @@ function syncItemToParent(itemFormInstance, parentFieldSignalContext, arrayKey,
6961
6633
  explicitEffect([() => itemFormInstance().value()], ([itemValue]) => {
6962
6634
  if (isSyncing)
6963
6635
  return;
6964
- const parentForm = parentFieldSignalContext.form();
6965
- const parentValue = untracked(() => parentForm.value());
6636
+ const parentForm = parentFieldSignalContext.form;
6637
+ const parentValue = untracked(() => parentForm().value());
6966
6638
  const currentArray = getArrayValue(parentValue, arrayKey);
6967
6639
  const idx = untracked(() => indexSignal());
6968
6640
  // Only sync if value actually changed
@@ -6970,8 +6642,10 @@ function syncItemToParent(itemFormInstance, parentFieldSignalContext, arrayKey,
6970
6642
  isSyncing = true;
6971
6643
  const newArray = [...currentArray];
6972
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
6973
6647
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6974
- parentForm.value.set({ ...parentValue, [arrayKey]: newArray });
6648
+ parentForm().value.set({ ...parentValue, [arrayKey]: newArray });
6975
6649
  isSyncing = false;
6976
6650
  }
6977
6651
  });
@@ -7018,7 +6692,7 @@ function createArrayItemInjectorAndInputs(options) {
7018
6692
  * The linkedSignal derives from the parent array at the current index.
7019
6693
  *
7020
6694
  * For array items, we always create a form with a schema to ensure proper field structure.
7021
- * This is needed for valueFieldMapper to find fields via childrenMap.
6695
+ * This is needed for valueFieldMapper to find fields via bracket notation.
7022
6696
  */
7023
6697
  function createObjectItemForm(options) {
7024
6698
  const { template, indexSignal, parentFieldSignalContext, parentInjector, registry, arrayKey } = options;
@@ -7054,7 +6728,6 @@ function createItemInjector(options) {
7054
6728
  injector: undefined,
7055
6729
  value: parentFieldSignalContext.value,
7056
6730
  defaultValues: () => ({}),
7057
- // formRef is already the form instance (result of form()), no need to wrap in another function
7058
6731
  form: formRef,
7059
6732
  defaultValidationMessages: parentFieldSignalContext.defaultValidationMessages,
7060
6733
  };
@@ -7105,7 +6778,8 @@ function resolveArrayItem(options) {
7105
6778
  };
7106
6779
  }), catchError((error) => {
7107
6780
  if (!destroyRef.destroyed) {
7108
- 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} ` +
7109
6783
  `within array '${arrayField.key}'. Ensure the field type is registered in your field registry.`, error);
7110
6784
  }
7111
6785
  return of(undefined);
@@ -7128,6 +6802,7 @@ class ArrayFieldComponent {
7128
6802
  parentFieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
7129
6803
  parentInjector = inject(Injector);
7130
6804
  eventBus = inject(EventBus);
6805
+ logger = inject(DYNAMIC_FORM_LOGGER);
7131
6806
  // ─────────────────────────────────────────────────────────────────────────────
7132
6807
  // Inputs
7133
6808
  // ─────────────────────────────────────────────────────────────────────────────
@@ -7143,21 +6818,18 @@ class ArrayFieldComponent {
7143
6818
  }, ...(ngDevMode ? [{ debugName: "fieldTemplate" }] : []));
7144
6819
  arrayFieldTrees = computed(() => {
7145
6820
  const arrayKey = this.field().key;
7146
- const parentForm = this.parentFieldSignalContext.form();
6821
+ const parentForm = this.parentFieldSignalContext.form;
7147
6822
  const arrayValue = getArrayValue(this.parentFieldSignalContext.value(), arrayKey);
7148
6823
  if (arrayValue.length === 0)
7149
6824
  return [];
7150
- // Get the array FieldTree from the parent form
7151
- const arrayFieldTree = getChildFieldTree(parentForm, arrayKey);
6825
+ const arrayFieldTree = parentForm[arrayKey];
7152
6826
  if (!arrayFieldTree)
7153
6827
  return arrayValue.map(() => null);
7154
6828
  // Access array items via bracket notation - Angular Signal Forms arrays support this
7155
- // The type is ReadonlyArrayLike<MaybeFieldTree<U, number>>
7156
- const arrayWithIndex = arrayFieldTree;
7157
6829
  const items = [];
7158
6830
  for (let i = 0; i < arrayValue.length; i++) {
7159
- // Access item FieldTree directly via bracket notation
7160
- const itemFieldTree = arrayWithIndex[i];
6831
+ // Access item FieldTree directly via numeric indexing (ArrayFieldTree supports this)
6832
+ const itemFieldTree = arrayFieldTree[i];
7161
6833
  items.push(itemFieldTree ?? null);
7162
6834
  }
7163
6835
  return items;
@@ -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: `
@@ -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-BWj75LVN.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