@ng-forge/dynamic-forms 0.4.0 → 0.5.1

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 (22) hide show
  1. package/README.md +3 -0
  2. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +138 -58
  3. package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
  4. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-DHxoGouT.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-BWFdMPyw.mjs} +3876 -354
  5. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BWFdMPyw.mjs.map +1 -0
  6. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-Bt6blv9I.mjs → ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs} +2 -2
  7. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-Bt6blv9I.mjs.map → ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs.map} +1 -1
  8. package/fesm2022/ng-forge-dynamic-forms-schema.mjs +92 -0
  9. package/fesm2022/ng-forge-dynamic-forms-schema.mjs.map +1 -0
  10. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-DKCx7Z_U.mjs → ng-forge-dynamic-forms-text-field.component-DIVyioMX.mjs} +2 -2
  11. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-DKCx7Z_U.mjs.map → ng-forge-dynamic-forms-text-field.component-DIVyioMX.mjs.map} +1 -1
  12. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  13. package/package.json +5 -4
  14. package/types/ng-forge-dynamic-forms-integration.d.ts +6 -14
  15. package/types/ng-forge-dynamic-forms-schema.d.ts +178 -0
  16. package/types/ng-forge-dynamic-forms.d.ts +1040 -45
  17. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-DHxoGouT.mjs.map +0 -1
  18. package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-C79EBW0o.mjs +0 -31
  19. package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-C79EBW0o.mjs.map +0 -1
  20. package/fesm2022/ng-forge-dynamic-forms-testing.mjs +0 -973
  21. package/fesm2022/ng-forge-dynamic-forms-testing.mjs.map +0 -1
  22. package/types/ng-forge-dynamic-forms-testing.d.ts +0 -603
@@ -1,21 +1,24 @@
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, signal, Injectable, untracked, Injector, isDevMode, isWritableSignal, DestroyRef, input, linkedSignal, ChangeDetectionStrategy, Component, model, afterNextRender, makeEnvironmentProviders, Pipe } from '@angular/core';
3
3
  import { NgComponentOutlet } from '@angular/common';
4
- import { submit, email, min, max, minLength, maxLength, pattern, required, validate, validateAsync, validateHttp, disabled, readonly, hidden, applyEach, applyWhenValue, applyWhen, apply, schema, validateTree, form, provideSignalFormsConfig } 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, validateStandardSchema, validateTree, form, provideSignalFormsConfig } from '@angular/forms/signals';
5
5
  import { toObservable, toSignal, takeUntilDestroyed, outputFromObservable } from '@angular/core/rxjs-interop';
6
- import { defer, combineLatest, from, map, catchError, of, pipe, switchMap, forkJoin, scan, isObservable, firstValueFrom, EMPTY, Subject, filter } from 'rxjs';
6
+ import { defer, combineLatest, from, map, catchError, of, pipe, switchMap, forkJoin, scan, isObservable, firstValueFrom, EMPTY, debounceTime, distinctUntilChanged, startWith, tap, Subject, filter, combineLatestWith, auditTime, exhaustMap, scheduled, queueScheduler, pairwise, merge, timer } from 'rxjs';
7
+ import { isStandardSchemaMarker } from '@ng-forge/dynamic-forms/schema';
7
8
  import { scan as scan$1, map as map$1, filter as filter$1, take, switchMap as switchMap$1, shareReplay } from 'rxjs/operators';
8
9
  import { explicitEffect } from 'ngxtension/explicit-effect';
9
10
  import { NG_STATUS_CLASSES } from '@angular/forms/signals/compat';
10
11
 
11
12
  /**
12
13
  * Type guard to check if a value is an array of Signals.
14
+ * @internal
13
15
  */
14
16
  function isSignalArray(value) {
15
17
  return Array.isArray(value) && value.every((item) => isSignal(item));
16
18
  }
17
19
  /**
18
- * Type guard to check if a value is an object of Signals (record).
20
+ * Type guard to check if a value is a record (object) of Signals.
21
+ * @internal
19
22
  */
20
23
  function isSignalRecord(value) {
21
24
  return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.values(value).every((item) => isSignal(item));
@@ -297,7 +300,7 @@ function resolveField(fieldDef, context) {
297
300
  inputs,
298
301
  };
299
302
  }), catchError((error) => {
300
- // Only handle errors if component hasn't been destroyed
303
+ // Only call onError if component is not destroyed to avoid accessing cleaned-up state
301
304
  if (!context.destroyRef.destroyed) {
302
305
  context.onError?.(fieldDef, error);
303
306
  }
@@ -345,19 +348,22 @@ function createFieldResolutionPipe(getContext) {
345
348
  * Wraps a submission action to handle both Promise and Observable returns.
346
349
  * Converts Observables to Promises for compatibility with Angular Signal Forms' submit().
347
350
  *
351
+ * The return type uses `void` to match Angular Signal Forms' expected action signature.
352
+ * The actual result from the action is preserved but typed as void since Angular's
353
+ * submit() function handles the result internally.
354
+ *
348
355
  * @param action - The submission action function
349
356
  * @returns A wrapped function that always returns a Promise
350
357
  */
351
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Form tree type is dynamic, action signatures vary
352
358
  function wrapSubmissionAction(action) {
353
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
354
- return (formTree) => {
359
+ return async (formTree) => {
355
360
  const result = action(formTree);
356
361
  // If the action returns an Observable, convert it to a Promise
357
362
  if (isObservable(result)) {
358
- return firstValueFrom(result);
363
+ await firstValueFrom(result);
364
+ return;
359
365
  }
360
- return result;
366
+ await Promise.resolve(result);
361
367
  };
362
368
  }
363
369
  /**
@@ -395,6 +401,8 @@ function createSubmissionHandler(options) {
395
401
  if (!submissionConfig?.action) {
396
402
  return EMPTY;
397
403
  }
404
+ // Type assertion needed: submission.action accepts the form tree but its signature
405
+ // is defined broadly in FormConfig. The actual runtime type is FieldTree<TModel>.
398
406
  const wrappedAction = wrapSubmissionAction(submissionConfig.action);
399
407
  // Use Angular Signal Forms' native submit() function
400
408
  // This automatically:
@@ -409,6 +417,133 @@ function createSubmissionHandler(options) {
409
417
  * Native JavaScript utility functions to replace lodash-es
410
418
  * These are simple implementations for common object manipulation patterns
411
419
  */
420
+ /**
421
+ * Performs a deep equality comparison between two values.
422
+ *
423
+ * Handles:
424
+ * - Primitives (including NaN via Object.is)
425
+ * - Dates (by timestamp)
426
+ * - Arrays (deep element comparison)
427
+ * - Plain objects (deep property comparison)
428
+ * - RegExp (by source and flags)
429
+ * - Map and Set (by entries)
430
+ * - Circular references (via WeakMap tracking)
431
+ *
432
+ * @param a - First value
433
+ * @param b - Second value
434
+ * @returns true if values are deeply equal
435
+ *
436
+ * @example
437
+ * ```typescript
438
+ * isEqual({ a: 1 }, { a: 1 }); // true
439
+ * isEqual([1, 2], [1, 2]); // true
440
+ * isEqual({ a: 1 }, { a: 2 }); // false
441
+ * isEqual(new Date('2024-01-01'), new Date('2024-01-01')); // true
442
+ * isEqual(/abc/gi, /abc/gi); // true
443
+ * ```
444
+ */
445
+ function isEqual(a, b) {
446
+ return isEqualInternal(a, b, new WeakMap(), new WeakMap());
447
+ }
448
+ /**
449
+ * Internal implementation with circular reference tracking.
450
+ * @internal
451
+ */
452
+ function isEqualInternal(a, b, seenA, seenB) {
453
+ // Same reference or both null/undefined
454
+ if (a === b)
455
+ return true;
456
+ // Handle NaN (NaN === NaN is false, but we want it to be equal)
457
+ if (typeof a === 'number' && typeof b === 'number') {
458
+ return Object.is(a, b);
459
+ }
460
+ // One is null/undefined but not both
461
+ if (a == null || b == null)
462
+ return false;
463
+ // Different types
464
+ if (typeof a !== typeof b)
465
+ return false;
466
+ // Non-object primitives (strings, booleans, symbols, bigints)
467
+ if (typeof a !== 'object') {
468
+ return Object.is(a, b);
469
+ }
470
+ // At this point, both a and b are objects (typeof a === 'object')
471
+ const objA = a;
472
+ const objB = b;
473
+ // Check for circular references
474
+ if (seenA.has(objA) || seenB.has(objB)) {
475
+ // If we've seen these objects before, check if they were paired together
476
+ return seenA.get(objA) === objB && seenB.get(objB) === objA;
477
+ }
478
+ // Mark these objects as seen (paired together)
479
+ seenA.set(objA, objB);
480
+ seenB.set(objB, objA);
481
+ // Different constructors means different types (e.g., Date vs Object)
482
+ if (objA.constructor !== objB.constructor)
483
+ return false;
484
+ // Handle dates
485
+ if (a instanceof Date && b instanceof Date) {
486
+ return a.getTime() === b.getTime();
487
+ }
488
+ // Handle RegExp
489
+ if (a instanceof RegExp && b instanceof RegExp) {
490
+ return a.source === b.source && a.flags === b.flags;
491
+ }
492
+ // Handle Map
493
+ if (a instanceof Map && b instanceof Map) {
494
+ if (a.size !== b.size)
495
+ return false;
496
+ for (const [key, value] of a) {
497
+ if (!b.has(key) || !isEqualInternal(value, b.get(key), seenA, seenB)) {
498
+ return false;
499
+ }
500
+ }
501
+ return true;
502
+ }
503
+ // Handle Set
504
+ if (a instanceof Set && b instanceof Set) {
505
+ if (a.size !== b.size)
506
+ return false;
507
+ for (const value of a) {
508
+ if (!b.has(value)) {
509
+ // For non-primitive values in Sets, we need deep comparison
510
+ // This is O(n^2) but Sets rarely contain objects
511
+ if (typeof value === 'object' && value !== null) {
512
+ let found = false;
513
+ for (const bValue of b) {
514
+ if (typeof bValue === 'object' && bValue !== null && isEqualInternal(value, bValue, seenA, seenB)) {
515
+ found = true;
516
+ break;
517
+ }
518
+ }
519
+ if (!found)
520
+ return false;
521
+ }
522
+ else {
523
+ return false;
524
+ }
525
+ }
526
+ }
527
+ return true;
528
+ }
529
+ // Handle arrays
530
+ if (Array.isArray(a) && Array.isArray(b)) {
531
+ if (a.length !== b.length)
532
+ return false;
533
+ return a.every((item, index) => isEqualInternal(item, b[index], seenA, seenB));
534
+ }
535
+ // Handle plain objects
536
+ const recordA = a;
537
+ const recordB = b;
538
+ // Get both string and symbol keys
539
+ const keysA = [...Object.keys(recordA), ...Object.getOwnPropertySymbols(recordA)];
540
+ const keysB = [...Object.keys(recordB), ...Object.getOwnPropertySymbols(recordB)];
541
+ if (keysA.length !== keysB.length)
542
+ return false;
543
+ // Create a Set for O(1) lookup
544
+ const keySetB = new Set(keysB);
545
+ return keysA.every((key) => keySetB.has(key) && isEqualInternal(recordA[key], recordB[key], seenA, seenB));
546
+ }
412
547
  /**
413
548
  * Creates a new object without the specified keys
414
549
  * Native replacement for lodash omit()
@@ -516,54 +651,6 @@ function memoize(fn, resolverOrOptions) {
516
651
  return result;
517
652
  });
518
653
  }
519
- /**
520
- * Performs a deep equality comparison between two values
521
- * Native replacement for lodash isEqual()
522
- *
523
- * @param a - First value
524
- * @param b - Second value
525
- * @returns true if values are deeply equal
526
- *
527
- * @example
528
- * ```typescript
529
- * isEqual({ a: 1 }, { a: 1 }); // true
530
- * isEqual([1, 2], [1, 2]); // true
531
- * isEqual({ a: 1 }, { a: 2 }); // false
532
- * ```
533
- */
534
- function isEqual(a, b) {
535
- // Same reference or both null/undefined
536
- if (a === b)
537
- return true;
538
- // One is null/undefined but not both
539
- if (a == null || b == null)
540
- return false;
541
- // Different types
542
- if (typeof a !== typeof b)
543
- return false;
544
- // Handle dates
545
- if (a instanceof Date && b instanceof Date) {
546
- return a.getTime() === b.getTime();
547
- }
548
- // Handle arrays
549
- if (Array.isArray(a) && Array.isArray(b)) {
550
- if (a.length !== b.length)
551
- return false;
552
- return a.every((item, index) => isEqual(item, b[index]));
553
- }
554
- // Handle objects
555
- if (typeof a === 'object' && typeof b === 'object') {
556
- const objA = a;
557
- const objB = b;
558
- const keysA = Object.keys(objA);
559
- const keysB = Object.keys(objB);
560
- if (keysA.length !== keysB.length)
561
- return false;
562
- return keysA.every((key) => keysB.includes(key) && isEqual(objA[key], objB[key]));
563
- }
564
- // Primitives - use Object.is to handle NaN correctly (NaN === NaN is false, but Object.is(NaN, NaN) is true)
565
- return Object.is(a, b);
566
- }
567
654
  /**
568
655
  * Normalizes a fields collection to an array.
569
656
  * Handles both array format and object format (keyed by field key).
@@ -574,6 +661,52 @@ function isEqual(a, b) {
574
661
  function normalizeFieldsArray(fields) {
575
662
  return Array.isArray(fields) ? [...fields] : Object.values(fields);
576
663
  }
664
+ /**
665
+ * Gets the keys that differ between two objects.
666
+ *
667
+ * Compares top-level keys and returns those whose values are different.
668
+ * Uses deep equality comparison for nested values.
669
+ *
670
+ * @param previous - Previous object state
671
+ * @param current - Current object state
672
+ * @returns Set of keys that have different values
673
+ *
674
+ * @example
675
+ * ```typescript
676
+ * const prev = { a: 1, b: 2, c: 3 };
677
+ * const curr = { a: 1, b: 5, d: 4 };
678
+ * getChangedKeys(prev, curr); // Set { 'b', 'c', 'd' }
679
+ * ```
680
+ */
681
+ function getChangedKeys(previous, current) {
682
+ const changedKeys = new Set();
683
+ // Handle null/undefined cases
684
+ if (!previous && !current)
685
+ return changedKeys;
686
+ if (!previous) {
687
+ return new Set(Object.keys(current ?? {}));
688
+ }
689
+ if (!current) {
690
+ return new Set(Object.keys(previous));
691
+ }
692
+ // Collect all keys from both objects
693
+ const allKeys = new Set([...Object.keys(previous), ...Object.keys(current)]);
694
+ // Check each key for differences
695
+ for (const key of allKeys) {
696
+ const prevValue = previous[key];
697
+ const currValue = current[key];
698
+ // Key added or removed
699
+ if (!(key in previous) || !(key in current)) {
700
+ changedKeys.add(key);
701
+ continue;
702
+ }
703
+ // Value changed
704
+ if (!isEqual(prevValue, currValue)) {
705
+ changedKeys.add(key);
706
+ }
707
+ }
708
+ return changedKeys;
709
+ }
577
710
 
578
711
  /**
579
712
  * Injection token for providing field signal context to mappers and components.
@@ -709,6 +842,36 @@ const DEFAULT_VALIDATION_MESSAGES = new InjectionToken('DEFAULT_VALIDATION_MESSA
709
842
  * ```
710
843
  */
711
844
  const FORM_OPTIONS = new InjectionToken('FORM_OPTIONS');
845
+ /**
846
+ * Injection token for external data signals.
847
+ *
848
+ * External data provides a way to inject application state into form expressions.
849
+ * Each property in the record is a Signal that gets unwrapped and made available
850
+ * in the `EvaluationContext` under `externalData`.
851
+ *
852
+ * Like DEFAULT_PROPS, this token is provided ONCE at the DynamicForm level
853
+ * and inherited by all children via Angular's hierarchical injector.
854
+ *
855
+ * The token provides a Signal containing the external data record, which itself
856
+ * contains Signals. This allows the form to react to changes in which external
857
+ * data is available (outer Signal) and to changes in the external data values
858
+ * (inner Signals).
859
+ *
860
+ * @example
861
+ * ```typescript
862
+ * // In FieldContextRegistryService
863
+ * const externalDataSignal = inject(EXTERNAL_DATA, { optional: true });
864
+ *
865
+ * // When creating evaluation context:
866
+ * const externalDataRecord = externalDataSignal?.();
867
+ * const resolvedExternalData = externalDataRecord
868
+ * ? Object.fromEntries(
869
+ * Object.entries(externalDataRecord).map(([k, v]) => [k, v()])
870
+ * )
871
+ * : {};
872
+ * ```
873
+ */
874
+ const EXTERNAL_DATA = new InjectionToken('EXTERNAL_DATA');
712
875
 
713
876
  /**
714
877
  * Base error class for all Dynamic Forms errors.
@@ -1051,7 +1214,7 @@ class RootFormRegistryService {
1051
1214
  }
1052
1215
  }
1053
1216
  catch {
1054
- // Ignore errors when reading form value
1217
+ // Ignore errors when reading form value - can happen during initialization
1055
1218
  }
1056
1219
  }
1057
1220
  return {};
@@ -1115,6 +1278,7 @@ function isChildFieldContext(context) {
1115
1278
  class FieldContextRegistryService {
1116
1279
  rootFormRegistry = inject(RootFormRegistryService);
1117
1280
  logger = inject(DynamicFormLogger);
1281
+ externalDataSignal = inject(EXTERNAL_DATA, { optional: true });
1118
1282
  /**
1119
1283
  * Creates an evaluation context for a field by combining:
1120
1284
  * - The field's current value
@@ -1149,6 +1313,7 @@ class FieldContextRegistryService {
1149
1313
  formValue,
1150
1314
  fieldPath,
1151
1315
  customFunctions: customFunctions || {},
1316
+ externalData: this.resolveExternalData(false),
1152
1317
  logger: this.logger,
1153
1318
  };
1154
1319
  }
@@ -1167,6 +1332,24 @@ class FieldContextRegistryService {
1167
1332
  // For root fields or when key is not available
1168
1333
  return '';
1169
1334
  }
1335
+ /**
1336
+ * Resolves external data signals to their current values.
1337
+ *
1338
+ * @param reactive - If true, reads signals reactively (creates dependencies).
1339
+ * If false, reads signals with untracked() (no dependencies).
1340
+ * @returns Record of resolved external data values, or undefined if no external data.
1341
+ */
1342
+ resolveExternalData(reactive) {
1343
+ const externalDataRecord = reactive ? this.externalDataSignal?.() : untracked(() => this.externalDataSignal?.());
1344
+ if (!externalDataRecord) {
1345
+ return undefined;
1346
+ }
1347
+ const resolved = {};
1348
+ for (const [key, signal] of Object.entries(externalDataRecord)) {
1349
+ resolved[key] = reactive ? signal() : untracked(() => signal());
1350
+ }
1351
+ return resolved;
1352
+ }
1170
1353
  /**
1171
1354
  * Creates a REACTIVE evaluation context for logic functions.
1172
1355
  *
@@ -1195,6 +1378,7 @@ class FieldContextRegistryService {
1195
1378
  formValue,
1196
1379
  fieldPath,
1197
1380
  customFunctions: customFunctions || {},
1381
+ externalData: this.resolveExternalData(true),
1198
1382
  logger: this.logger,
1199
1383
  };
1200
1384
  }
@@ -1219,6 +1403,7 @@ class FieldContextRegistryService {
1219
1403
  formValue,
1220
1404
  fieldPath,
1221
1405
  customFunctions: customFunctions || {},
1406
+ externalData: this.resolveExternalData(true),
1222
1407
  logger: this.logger,
1223
1408
  };
1224
1409
  }
@@ -1286,6 +1471,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
1286
1471
  class FunctionRegistryService {
1287
1472
  customFunctions = new Map();
1288
1473
  customFunctionScopes = new Map();
1474
+ derivationFunctions = new Map();
1289
1475
  validators = new Map();
1290
1476
  asyncValidators = new Map();
1291
1477
  httpValidators = new Map();
@@ -1344,6 +1530,56 @@ class FunctionRegistryService {
1344
1530
  this.customFunctions.clear();
1345
1531
  this.customFunctionScopes.clear();
1346
1532
  }
1533
+ // ─────────────────────────────────────────────────────────────────────────────
1534
+ // Derivation Functions
1535
+ // ─────────────────────────────────────────────────────────────────────────────
1536
+ /**
1537
+ * Register a derivation function for value derivation logic.
1538
+ *
1539
+ * Derivation functions compute derived values and are called when a
1540
+ * `DerivationLogicConfig` references them by `functionName`.
1541
+ *
1542
+ * @param name - Unique identifier for the function
1543
+ * @param fn - Function that receives EvaluationContext and returns the derived value
1544
+ *
1545
+ * @example
1546
+ * ```typescript
1547
+ * registry.registerDerivationFunction('getCurrencyForCountry', (ctx) => {
1548
+ * const countryToCurrency = { 'USA': 'USD', 'Germany': 'EUR', 'UK': 'GBP' };
1549
+ * return countryToCurrency[ctx.formValue.country] ?? 'USD';
1550
+ * });
1551
+ * ```
1552
+ */
1553
+ registerDerivationFunction(name, fn) {
1554
+ this.derivationFunctions.set(name, fn);
1555
+ }
1556
+ /**
1557
+ * Get a derivation function by name
1558
+ */
1559
+ getDerivationFunction(name) {
1560
+ return this.derivationFunctions.get(name);
1561
+ }
1562
+ /**
1563
+ * Get all derivation functions as an object
1564
+ */
1565
+ getDerivationFunctions() {
1566
+ return Object.fromEntries(this.derivationFunctions);
1567
+ }
1568
+ /**
1569
+ * Set derivation functions from a config object.
1570
+ * Only updates functions if their references have changed.
1571
+ *
1572
+ * @param derivations - Object mapping function names to derivation functions
1573
+ */
1574
+ setDerivationFunctions(derivations) {
1575
+ this.setRegistryIfChanged(this.derivationFunctions, derivations);
1576
+ }
1577
+ /**
1578
+ * Clear all derivation functions
1579
+ */
1580
+ clearDerivationFunctions() {
1581
+ this.derivationFunctions.clear();
1582
+ }
1347
1583
  /**
1348
1584
  * Register a custom validator using Angular's public FieldContext API
1349
1585
  *
@@ -1561,6 +1797,7 @@ class FunctionRegistryService {
1561
1797
  */
1562
1798
  clearAll() {
1563
1799
  this.clearCustomFunctions();
1800
+ this.clearDerivationFunctions();
1564
1801
  this.clearValidators();
1565
1802
  this.clearAsyncValidators();
1566
1803
  this.clearHttpValidators();
@@ -2404,7 +2641,7 @@ class Evaluator {
2404
2641
  /**
2405
2642
  * Maximum cache size to prevent memory issues
2406
2643
  */
2407
- const MAX_AST_CACHE_SIZE = 1000;
2644
+ const MAX_AST_CACHE_SIZE$1 = 1000;
2408
2645
  /**
2409
2646
  * LRU Cache implementation for AST nodes
2410
2647
  * Tracks access order to evict least-recently-used entries
@@ -2449,7 +2686,7 @@ class LRUCache {
2449
2686
  /**
2450
2687
  * Cache for parsed AST nodes to improve performance
2451
2688
  */
2452
- const astCache = new LRUCache(MAX_AST_CACHE_SIZE);
2689
+ const astCache = new LRUCache(MAX_AST_CACHE_SIZE$1);
2453
2690
  /**
2454
2691
  * Secure expression parser that uses AST-based evaluation instead of eval() or new Function()
2455
2692
  *
@@ -2506,7 +2743,7 @@ class ExpressionParser {
2506
2743
  static getCacheStats() {
2507
2744
  return {
2508
2745
  size: astCache.size,
2509
- maxSize: MAX_AST_CACHE_SIZE,
2746
+ maxSize: MAX_AST_CACHE_SIZE$1,
2510
2747
  };
2511
2748
  }
2512
2749
  }
@@ -2534,14 +2771,18 @@ function evaluateCondition(expression, context) {
2534
2771
  }
2535
2772
  }
2536
2773
  function evaluateFieldValueCondition(expression, context) {
2537
- if (!expression.fieldPath || !expression.operator)
2774
+ if (!expression.fieldPath || !expression.operator) {
2775
+ context.logger.debug('Invalid fieldValue condition config: missing fieldPath or operator', expression);
2538
2776
  return false;
2777
+ }
2539
2778
  const fieldValue = getNestedValue(context.formValue, expression.fieldPath);
2540
2779
  return compareValues(fieldValue, expression.value, expression.operator);
2541
2780
  }
2542
2781
  function evaluateFormValueCondition(expression, context) {
2543
- if (!expression.operator)
2782
+ if (!expression.operator) {
2783
+ context.logger.debug('Invalid formValue condition config: missing operator', expression);
2544
2784
  return false;
2785
+ }
2545
2786
  return compareValues(context.formValue, expression.value, expression.operator);
2546
2787
  }
2547
2788
  function evaluateJavaScriptExpression(expression, context) {
@@ -2674,6 +2915,105 @@ function createLogicFunction(expression) {
2674
2915
  contextCache.set(cacheKey, fn);
2675
2916
  return fn;
2676
2917
  }
2918
+ /**
2919
+ * Cache for debounced logic functions.
2920
+ * Keyed by injection context similar to logicFunctionCache, but includes debounceMs in the key.
2921
+ */
2922
+ const debouncedLogicFunctionCache = new WeakMap();
2923
+ /**
2924
+ * Gets or creates the cache map for debounced functions in a specific injection context.
2925
+ */
2926
+ function getDebouncedContextCache(ctx) {
2927
+ let outerCache = debouncedLogicFunctionCache.get(ctx.functionRegistry);
2928
+ if (!outerCache) {
2929
+ outerCache = new WeakMap();
2930
+ debouncedLogicFunctionCache.set(ctx.functionRegistry, outerCache);
2931
+ }
2932
+ let innerCache = outerCache.get(ctx.fieldContextRegistry);
2933
+ if (!innerCache) {
2934
+ innerCache = new Map();
2935
+ outerCache.set(ctx.fieldContextRegistry, innerCache);
2936
+ }
2937
+ return innerCache;
2938
+ }
2939
+ /**
2940
+ * Store for debounced signals per field context.
2941
+ * Uses WeakMap with FieldContext objects as keys to automatically clean up
2942
+ * when the context is garbage collected.
2943
+ */
2944
+ const debouncedSignalStore = new WeakMap();
2945
+ /**
2946
+ * Gets or creates the debounced signal store for a function registry.
2947
+ */
2948
+ function getDebouncedSignalStore(functionRegistry) {
2949
+ let store = debouncedSignalStore.get(functionRegistry);
2950
+ if (!store) {
2951
+ store = new WeakMap();
2952
+ debouncedSignalStore.set(functionRegistry, store);
2953
+ }
2954
+ return store;
2955
+ }
2956
+ /**
2957
+ * Create a debounced logic function from a conditional expression.
2958
+ *
2959
+ * This function wraps the condition evaluation in a debounce mechanism,
2960
+ * so state changes only take effect after the specified delay.
2961
+ * Useful for avoiding UI flicker during rapid typing.
2962
+ *
2963
+ * @param expression The conditional expression to evaluate
2964
+ * @param debounceMs The debounce duration in milliseconds
2965
+ * @returns A LogicFn that evaluates the condition with debouncing
2966
+ */
2967
+ function createDebouncedLogicFunction(expression, debounceMs) {
2968
+ // Inject services during factory creation, not during execution
2969
+ const functionRegistry = inject(FunctionRegistryService);
2970
+ const fieldContextRegistry = inject(FieldContextRegistryService);
2971
+ const injector = inject(Injector);
2972
+ // Get cache for this injection context
2973
+ const contextCache = getDebouncedContextCache({ functionRegistry, fieldContextRegistry });
2974
+ // Generate cache key including debounceMs
2975
+ const cacheKey = `${serializeExpression(expression)}:${debounceMs}`;
2976
+ // Check cache first
2977
+ const cached = contextCache.get(cacheKey);
2978
+ if (cached) {
2979
+ return cached;
2980
+ }
2981
+ // Get or create the signal store for this context
2982
+ const signalStore = getDebouncedSignalStore(functionRegistry);
2983
+ const fn = (ctx) => {
2984
+ // Create a unique key using the context object itself as identifier
2985
+ // Since FieldContext doesn't have a path property, we use a WeakMap approach
2986
+ // by storing the debounced signal directly on the context
2987
+ const contextKey = ctx;
2988
+ let signalPair = signalStore.get(contextKey);
2989
+ if (!signalPair) {
2990
+ // Create a signal to hold the immediate evaluation result
2991
+ const immediateValue = signal(false, ...(ngDevMode ? [{ debugName: "immediateValue" }] : []));
2992
+ // Create a debounced signal using RxJS
2993
+ const immediateValue$ = toObservable(immediateValue, { injector }).pipe(debounceTime(debounceMs), distinctUntilChanged(), startWith(false));
2994
+ const debouncedValue = toSignal(immediateValue$, { injector, initialValue: false });
2995
+ signalPair = { immediateValue, debouncedValue };
2996
+ signalStore.set(contextKey, signalPair);
2997
+ }
2998
+ // Create REACTIVE evaluation context for logic functions
2999
+ const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());
3000
+ // Evaluate the condition and update the immediate signal
3001
+ const result = evaluateCondition(expression, evaluationContext);
3002
+ // Update immediate value (this triggers the debounce chain)
3003
+ // signalPair is guaranteed to exist here due to the initialization above
3004
+ const { immediateValue, debouncedValue } = signalPair;
3005
+ untracked(() => {
3006
+ if (immediateValue() !== result) {
3007
+ immediateValue.set(result);
3008
+ }
3009
+ });
3010
+ // Return the debounced value
3011
+ return debouncedValue() ?? false;
3012
+ };
3013
+ // Cache the function
3014
+ contextCache.set(cacheKey, fn);
3015
+ return fn;
3016
+ }
2677
3017
 
2678
3018
  /**
2679
3019
  * Cache for memoized dynamic value functions, keyed by service instance.
@@ -2721,6 +3061,36 @@ function createDynamicValueFunction(expression) {
2721
3061
  return fn;
2722
3062
  }
2723
3063
 
3064
+ /**
3065
+ * Creates the default derivation log configuration.
3066
+ *
3067
+ * Defaults to 'none' (silent). Users can enable logging via `withLoggerConfig`.
3068
+ *
3069
+ * @returns Default DerivationLogConfig
3070
+ *
3071
+ * @public
3072
+ */
3073
+ function createDefaultDerivationLogConfig() {
3074
+ return {
3075
+ level: 'none',
3076
+ };
3077
+ }
3078
+ /**
3079
+ * Checks if logging should occur at the specified level.
3080
+ *
3081
+ * @param config - Current log configuration
3082
+ * @param minLevel - Minimum level required for logging
3083
+ * @returns True if logging should occur
3084
+ *
3085
+ * @public
3086
+ */
3087
+ function shouldLog(config, minLevel) {
3088
+ if (config.level === 'none')
3089
+ return false;
3090
+ if (minLevel === 'summary')
3091
+ return config.level === 'summary' || config.level === 'verbose';
3092
+ return config.level === 'verbose';
3093
+ }
2724
3094
  /**
2725
3095
  * Type guard to check if a condition is a FormStateCondition.
2726
3096
  *
@@ -2732,6 +3102,28 @@ function createDynamicValueFunction(expression) {
2732
3102
  function isFormStateCondition(condition) {
2733
3103
  return typeof condition === 'string' && ['formInvalid', 'formSubmitting', 'pageInvalid'].includes(condition);
2734
3104
  }
3105
+ /**
3106
+ * Type guard to check if a logic config is a StateLogicConfig.
3107
+ *
3108
+ * @param config - The logic config to check
3109
+ * @returns true if the config is for field state logic
3110
+ *
3111
+ * @public
3112
+ */
3113
+ function isStateLogicConfig(config) {
3114
+ return config.type === 'hidden' || config.type === 'readonly' || config.type === 'disabled' || config.type === 'required';
3115
+ }
3116
+ /**
3117
+ * Type guard to check if a logic config is a DerivationLogicConfig.
3118
+ *
3119
+ * @param config - The logic config to check
3120
+ * @returns true if the config is for value derivation
3121
+ *
3122
+ * @public
3123
+ */
3124
+ function isDerivationLogicConfig(config) {
3125
+ return config.type === 'derivation';
3126
+ }
2735
3127
 
2736
3128
  /**
2737
3129
  * Regular expression to detect formValue property access in expressions.
@@ -2740,10 +3132,19 @@ function isFormStateCondition(condition) {
2740
3132
  const FORM_VALUE_ACCESS_PATTERN = /\bformValue\s*(?:\.|\[)/;
2741
3133
  /**
2742
3134
  * Regular expressions to extract field paths from formValue expressions.
3135
+ *
3136
+ * Enhanced to capture full nested paths including dot notation chains:
3137
+ * - formValue.fieldName → captures 'fieldName'
3138
+ * - formValue.parent.child.grandchild → captures 'parent.child.grandchild'
3139
+ * - formValue['field-name'] → captures 'field-name'
3140
+ * - formValue["field.with.dots"] → captures 'field.with.dots'
3141
+ *
3142
+ * Note: Computed property access (formValue[variableName]) is NOT supported
3143
+ * and must use explicit dependsOn configuration.
2743
3144
  */
2744
- const FORM_VALUE_DOT_PATTERN = /\bformValue\.(\w+)/g;
2745
- const FORM_VALUE_BRACKET_SINGLE_PATTERN = /\bformValue\s*\[\s*'(\w+)'\s*\]/g;
2746
- const FORM_VALUE_BRACKET_DOUBLE_PATTERN = /\bformValue\s*\[\s*"(\w+)"\s*\]/g;
3145
+ const FORM_VALUE_DOT_PATTERN = /\bformValue\.([\w.]+)/g;
3146
+ const FORM_VALUE_BRACKET_SINGLE_PATTERN = /\bformValue\s*\[\s*'([\w.-]+)'\s*\]/g;
3147
+ const FORM_VALUE_BRACKET_DOUBLE_PATTERN = /\bformValue\s*\[\s*"([\w.-]+)"\s*\]/g;
2747
3148
  /**
2748
3149
  * Detects if a ConditionalExpression references other fields (cross-field).
2749
3150
  *
@@ -2851,22 +3252,45 @@ function extractStringDependencies(expression) {
2851
3252
  extractFromExpressionString(expression, deps);
2852
3253
  return Array.from(deps);
2853
3254
  }
2854
- /** Internal helper that populates a Set with dependencies from an expression string. */
3255
+ /**
3256
+ * Internal helper that populates a Set with dependencies from an expression string.
3257
+ *
3258
+ * Extracts both root fields and full nested paths for precise dependency tracking:
3259
+ * - 'formValue.parent.child' → adds 'parent' (root) and 'parent.child' (full path)
3260
+ * - 'formValue.simple' → adds 'simple'
3261
+ */
2855
3262
  function extractFromExpressionString(expression, deps) {
2856
- // Extract from dot notation: formValue.fieldName
3263
+ // Extract from dot notation: formValue.fieldName or formValue.parent.child.grandchild
2857
3264
  const dotMatches = expression.matchAll(FORM_VALUE_DOT_PATTERN);
2858
3265
  for (const match of dotMatches) {
2859
- deps.add(match[1]);
3266
+ const fullPath = match[1];
3267
+ // Always add the root field (first segment) as the primary dependency
3268
+ const rootField = fullPath.split('.')[0];
3269
+ deps.add(rootField);
3270
+ // Also add the full path for more precise dependency tracking if nested
3271
+ if (fullPath.includes('.')) {
3272
+ deps.add(fullPath);
3273
+ }
2860
3274
  }
2861
3275
  // Extract from bracket notation with single quotes: formValue['fieldName']
2862
3276
  const bracketSingleMatches = expression.matchAll(FORM_VALUE_BRACKET_SINGLE_PATTERN);
2863
3277
  for (const match of bracketSingleMatches) {
2864
- deps.add(match[1]);
3278
+ const fullPath = match[1];
3279
+ const rootField = fullPath.split('.')[0];
3280
+ deps.add(rootField);
3281
+ if (fullPath.includes('.')) {
3282
+ deps.add(fullPath);
3283
+ }
2865
3284
  }
2866
3285
  // Extract from bracket notation with double quotes: formValue["fieldName"]
2867
3286
  const bracketDoubleMatches = expression.matchAll(FORM_VALUE_BRACKET_DOUBLE_PATTERN);
2868
3287
  for (const match of bracketDoubleMatches) {
2869
- deps.add(match[1]);
3288
+ const fullPath = match[1];
3289
+ const rootField = fullPath.split('.')[0];
3290
+ deps.add(rootField);
3291
+ if (fullPath.includes('.')) {
3292
+ deps.add(fullPath);
3293
+ }
2870
3294
  }
2871
3295
  }
2872
3296
  // ============================================================================
@@ -2919,14 +3343,34 @@ function hasCrossFieldWhenCondition(config, context) {
2919
3343
  // ============================================================================
2920
3344
  // Logic Detection
2921
3345
  // ============================================================================
3346
+ /**
3347
+ * Detects if a StateLogicConfig has a cross-field condition.
3348
+ *
3349
+ * Note: This function only handles state logic (hidden, readonly, disabled, required).
3350
+ * Derivation logic is handled separately by the derivation system.
3351
+ *
3352
+ * @param config The state logic configuration to check
3353
+ * @param context Optional context providing function scope lookup
3354
+ * @returns true if the logic condition references other fields
3355
+ */
3356
+ function isCrossFieldStateLogic(config, context) {
3357
+ return isCrossFieldExpression(config.condition, context);
3358
+ }
2922
3359
  /**
2923
3360
  * Detects if a LogicConfig has a cross-field condition.
2924
3361
  *
2925
3362
  * @param config The logic configuration to check
2926
3363
  * @param context Optional context providing function scope lookup
2927
3364
  * @returns true if the logic condition references other fields
3365
+ *
3366
+ * @deprecated Use `isCrossFieldStateLogic` for state logic configs.
3367
+ * Derivation logic is handled by the derivation system.
2928
3368
  */
2929
3369
  function isCrossFieldLogic(config, context) {
3370
+ // Only check state logic configs
3371
+ if (!isStateLogicConfig(config)) {
3372
+ return false;
3373
+ }
2930
3374
  return isCrossFieldExpression(config.condition, context);
2931
3375
  }
2932
3376
  // ============================================================================
@@ -3080,9 +3524,7 @@ function createFunctionValidator(config) {
3080
3524
  const registry = inject(FunctionRegistryService);
3081
3525
  const validatorFn = registry.getValidator(functionName);
3082
3526
  if (!validatorFn) {
3083
- logger.warn(`Custom validator "${functionName}" not found in registry. ` +
3084
- `Ensure it's registered using customFnConfig.validators or check the function name for typos.`);
3085
- return () => null;
3527
+ throw new DynamicFormError(`Custom validator "${functionName}" not found. Register it with customFnConfig.validators.`);
3086
3528
  }
3087
3529
  return (ctx) => validatorFn(ctx, config.params);
3088
3530
  }
@@ -3134,13 +3576,10 @@ function createExpressionValidator(config) {
3134
3576
  * - onError: Optional handler for resource errors
3135
3577
  */
3136
3578
  function applyAsyncValidator(config, fieldPath) {
3137
- const logger = inject(DynamicFormLogger);
3138
3579
  const registry = inject(FunctionRegistryService);
3139
3580
  const validatorConfig = registry.getAsyncValidator(config.functionName);
3140
3581
  if (!validatorConfig) {
3141
- logger.warn(`Async validator "${config.functionName}" not found in registry. ` +
3142
- `Ensure it's registered using customFnConfig.asyncValidators or check the function name for typos.`);
3143
- return;
3582
+ throw new DynamicFormError(`Async validator "${config.functionName}" not found. Register it with customFnConfig.asyncValidators.`);
3144
3583
  }
3145
3584
  const whenLogic = createConditionalLogic(config.when);
3146
3585
  const asyncOptions = {
@@ -3164,13 +3603,10 @@ function applyAsyncValidator(config, fieldPath) {
3164
3603
  * - onError: Optional handler for HTTP errors
3165
3604
  */
3166
3605
  function applyHttpValidator(config, fieldPath) {
3167
- const logger = inject(DynamicFormLogger);
3168
3606
  const registry = inject(FunctionRegistryService);
3169
3607
  const httpValidatorConfig = registry.getHttpValidator(config.functionName);
3170
3608
  if (!httpValidatorConfig) {
3171
- logger.warn(`HTTP validator "${config.functionName}" not found in registry. ` +
3172
- `Ensure it's registered using customFnConfig.httpValidators or check the function name for typos.`);
3173
- return;
3609
+ throw new DynamicFormError(`HTTP validator "${config.functionName}" not found. Register it with customFnConfig.httpValidators.`);
3174
3610
  }
3175
3611
  const whenLogic = createConditionalLogic(config.when);
3176
3612
  const httpOptions = {
@@ -3189,13 +3625,111 @@ function applyValidators(configs, fieldPath) {
3189
3625
  configs.forEach((config) => applyValidator(config, fieldPath));
3190
3626
  }
3191
3627
 
3192
- function applyLogic(config, fieldPath) {
3193
- const path = fieldPath;
3628
+ /**
3629
+ * Default debounce duration in milliseconds.
3630
+ */
3631
+ const DEFAULT_DEBOUNCE_MS = 500;
3632
+ /**
3633
+ * Creates a debounced effect that triggers a callback after a signal's value
3634
+ * has stabilized for the specified duration.
3635
+ *
3636
+ * Uses Angular's `toObservable` and `toSignal` with RxJS `debounceTime` for proper debouncing.
3637
+ * This approach is cleaner than manual setTimeout management and integrates
3638
+ * well with Angular's signal-based reactivity.
3639
+ *
3640
+ * @param source - The source signal to watch for changes
3641
+ * @param callback - Function to call with the debounced value
3642
+ * @param options - Configuration options
3643
+ * @param options.ms - Debounce duration in milliseconds (default: 500)
3644
+ * @param options.injector - Angular injector for the effect context
3645
+ *
3646
+ * @returns A signal that emits the debounced value (can be ignored if only side effects are needed)
3647
+ *
3648
+ * @example
3649
+ * ```typescript
3650
+ * // In a component or service
3651
+ * const searchInput = signal('');
3652
+ *
3653
+ * createDebouncedEffect(
3654
+ * searchInput,
3655
+ * (value) => console.log('Debounced search:', value),
3656
+ * { ms: 300, injector: this.injector }
3657
+ * );
3658
+ * ```
3659
+ *
3660
+ * @public
3661
+ */
3662
+ function createDebouncedEffect(source, callback, options) {
3663
+ const { ms = DEFAULT_DEBOUNCE_MS, injector } = options;
3664
+ const source$ = toObservable(source, { injector }).pipe(debounceTime(ms), distinctUntilChanged(), tap((value) => callback(value)));
3665
+ return toSignal(source$, { injector });
3666
+ }
3667
+ /**
3668
+ * Creates a debounced signal that emits values from the source signal
3669
+ * only after the value has stabilized for the specified duration.
3670
+ *
3671
+ * Unlike `createDebouncedEffect`, this doesn't execute a side effect callback.
3672
+ * Use this when you just need a debounced version of a signal.
3673
+ *
3674
+ * @param source - The source signal to debounce
3675
+ * @param options - Configuration options
3676
+ * @param options.ms - Debounce duration in milliseconds (default: 500)
3677
+ * @param options.injector - Angular injector for the effect context
3678
+ *
3679
+ * @returns A signal that emits the debounced value
3680
+ *
3681
+ * @example
3682
+ * ```typescript
3683
+ * const input = signal('');
3684
+ * const debouncedInput = createDebouncedSignal(input, { ms: 300, injector });
3685
+ *
3686
+ * effect(() => {
3687
+ * console.log('Debounced value:', debouncedInput());
3688
+ * });
3689
+ * ```
3690
+ *
3691
+ * @public
3692
+ */
3693
+ function createDebouncedSignal(source, options) {
3694
+ const { ms = DEFAULT_DEBOUNCE_MS, injector } = options;
3695
+ const source$ = toObservable(source, { injector }).pipe(debounceTime(ms), distinctUntilChanged());
3696
+ return toSignal(source$, { injector });
3697
+ }
3698
+
3699
+ /**
3700
+ * Extracts the trigger from a StateLogicConfig, handling the discriminated union.
3701
+ */
3702
+ function getConfigTrigger(config) {
3703
+ if (!isStateLogicConfig(config)) {
3704
+ return 'onChange';
3705
+ }
3706
+ return config.trigger ?? 'onChange';
3707
+ }
3708
+ /**
3709
+ * Extracts debounceMs from a StateLogicConfig if trigger is 'debounced'.
3710
+ */
3711
+ function getConfigDebounceMs(config) {
3712
+ if (!isStateLogicConfig(config)) {
3713
+ return undefined;
3714
+ }
3715
+ // Type narrowing: debounceMs only exists when trigger is 'debounced'
3716
+ if (config.trigger === 'debounced') {
3717
+ return config.debounceMs;
3718
+ }
3719
+ return undefined;
3720
+ }
3721
+ function applyLogic(config, fieldPath) {
3722
+ const path = fieldPath;
3723
+ const trigger = getConfigTrigger(config);
3724
+ const debounceMs = getConfigDebounceMs(config) ?? DEFAULT_DEBOUNCE_MS;
3194
3725
  if (typeof config.condition === 'boolean') {
3195
3726
  applyLogicFn(config.type, path, () => config.condition);
3196
3727
  return;
3197
3728
  }
3198
- const logicFn = createLogicFunction(config.condition);
3729
+ // Create appropriate logic function based on trigger
3730
+ const logicFn = trigger === 'debounced'
3731
+ ? createDebouncedLogicFunction(config.condition, debounceMs)
3732
+ : createLogicFunction(config.condition);
3199
3733
  applyLogicFn(config.type, path, logicFn);
3200
3734
  }
3201
3735
  function applyLogicFn(type, path, logicFn) {
@@ -3794,9 +4328,9 @@ function hasChildFields(field) {
3794
4328
  return 'fields' in field && field.fields != null;
3795
4329
  }
3796
4330
  /** Container field type names */
3797
- const CONTAINER_TYPES = new Set(['page', 'row', 'group', 'array']);
4331
+ const CONTAINER_TYPES$1 = new Set(['page', 'row', 'group', 'array']);
3798
4332
  function isContainerField(field) {
3799
- return CONTAINER_TYPES.has(field.type);
4333
+ return CONTAINER_TYPES$1.has(field.type);
3800
4334
  }
3801
4335
  /**
3802
4336
  * Type guard to check if a field is a leaf field (value or display field)
@@ -3821,9 +4355,91 @@ function isDisplayOnlyField(field) {
3821
4355
  return field.type === 'text' || isContainerField(field);
3822
4356
  }
3823
4357
 
4358
+ /**
4359
+ * Applies a form-level schema validation to a schema path.
4360
+ * Supports both Standard Schema (Zod, Valibot, ArkType) and raw Angular schema callbacks.
4361
+ *
4362
+ * This is a helper function used internally by `createSchemaFromFields`
4363
+ * to apply form-level validation after field-level validation.
4364
+ *
4365
+ * @typeParam TModel - The form value type
4366
+ * @param path - The schema path to validate (from Angular's schema callback)
4367
+ * @param formLevelSchema - Form-level schema (Standard Schema marker or Angular callback)
4368
+ *
4369
+ * @remarks
4370
+ * Type assertions are required due to Angular's complex generic constraints:
4371
+ * - `validateStandardSchema` uses `IgnoreUnknownProperties<TSchema>` to accommodate Zod's strict types
4372
+ * - `SchemaPathTree` includes `SchemaPath` via conditional types that TypeScript can't narrow
4373
+ * These assertions are safe because the path originates from Angular's schema() function.
4374
+ *
4375
+ * @internal
4376
+ */
4377
+ function applyFormLevelSchema(path, formLevelSchema) {
4378
+ if (isStandardSchemaMarker(formLevelSchema)) {
4379
+ // Standard Schema (Zod, Valibot, ArkType, etc.)
4380
+ // Cast via Parameters: validateStandardSchema's IgnoreUnknownProperties<TSchema> creates type incompatibility
4381
+ // that TypeScript cannot resolve. The cast is safe as path comes from Angular's schema() callback.
4382
+ validateStandardSchema(path, formLevelSchema.schema);
4383
+ }
4384
+ else if (typeof formLevelSchema === 'function') {
4385
+ // Angular schema callback - execute directly with the path
4386
+ // Cast via Function: AngularSchemaCallback expects intersection type SchemaPath & SchemaPathTree.
4387
+ // SchemaPathTree structurally includes SchemaPath, so this is safe at runtime.
4388
+ formLevelSchema(path);
4389
+ }
4390
+ }
4391
+ /**
4392
+ * Creates a schema from only form-level schema (when no field-level schema exists).
4393
+ *
4394
+ * Use this when the form has fields without validation rules but still
4395
+ * needs form-level validation via a Standard Schema.
4396
+ *
4397
+ * @typeParam TModel - The form value type
4398
+ * @param formLevelSchema - Form-level Standard Schema
4399
+ * @returns Schema that applies form-level validation
4400
+ *
4401
+ * @example
4402
+ * ```typescript
4403
+ * import { z } from 'zod';
4404
+ * import { standardSchema } from '@ng-forge/dynamic-forms/schema';
4405
+ *
4406
+ * const FormSchema = z.object({
4407
+ * email: z.string().email(),
4408
+ * password: z.string().min(8),
4409
+ * });
4410
+ *
4411
+ * // Create schema from form-level schema only
4412
+ * const formSchema = createFormLevelSchema(standardSchema(FormSchema));
4413
+ * ```
4414
+ */
4415
+ function createFormLevelSchema(formLevelSchema) {
4416
+ return schema((path) => {
4417
+ applyFormLevelSchema(path, formLevelSchema);
4418
+ });
4419
+ }
4420
+
3824
4421
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4422
+ /**
4423
+ * Gets a FieldTree by key, supporting nested paths with dot notation.
4424
+ *
4425
+ * @example
4426
+ * getFieldTreeByKey(ctx, 'address.street') // Returns FieldTree for nested 'street' field
4427
+ */
3825
4428
  function getFieldTreeByKey(ctx, key) {
3826
- return ctx.fieldTree[key];
4429
+ // Simple case - no nesting
4430
+ if (!key.includes('.')) {
4431
+ return ctx.fieldTree[key];
4432
+ }
4433
+ // Nested path - traverse through the structure
4434
+ const parts = key.split('.');
4435
+ let current = ctx.fieldTree;
4436
+ for (const part of parts) {
4437
+ if (!current || typeof current !== 'object') {
4438
+ return undefined;
4439
+ }
4440
+ current = current[part];
4441
+ }
4442
+ return current;
3827
4443
  }
3828
4444
  /**
3829
4445
  * Creates an Angular signal forms schema from field definitions
@@ -3837,13 +4453,18 @@ function getFieldTreeByKey(ctx, key) {
3837
4453
  *
3838
4454
  * @param fields Field definitions to create schema from
3839
4455
  * @param registry Field type registry
3840
- * @param crossFieldValidators Optional array of collected cross-field validators
4456
+ * @param optionsOrValidators Optional configuration object or array of cross-field validators (for backwards compatibility)
3841
4457
  */
3842
- function createSchemaFromFields(fields, registry, crossFieldValidators) {
4458
+ function createSchemaFromFields(fields, registry, optionsOrValidators) {
3843
4459
  // Inject services for cross-field validation
3844
4460
  // These will be available because createSchemaFromFields is called within runInInjectionContext
3845
4461
  const functionRegistry = inject(FunctionRegistryService);
3846
4462
  const logger = inject(DynamicFormLogger);
4463
+ // Normalize options - support both old array signature and new options object
4464
+ const options = Array.isArray(optionsOrValidators)
4465
+ ? { crossFieldValidators: optionsOrValidators }
4466
+ : (optionsOrValidators ?? {});
4467
+ const { crossFieldValidators, formLevelSchema } = options;
3847
4468
  return schema((path) => {
3848
4469
  for (const fieldDef of fields) {
3849
4470
  const valueHandling = getFieldValueHandling(fieldDef.type, registry);
@@ -3877,6 +4498,10 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
3877
4498
  if (crossFieldValidators && crossFieldValidators.length > 0) {
3878
4499
  applyCrossFieldTreeValidator(path, crossFieldValidators, functionRegistry, logger);
3879
4500
  }
4501
+ // Apply form-level Standard Schema validation
4502
+ if (formLevelSchema) {
4503
+ applyFormLevelSchema(path, formLevelSchema);
4504
+ }
3880
4505
  });
3881
4506
  }
3882
4507
  /**
@@ -3986,6 +4611,7 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3986
4611
  // Validation failed - create error targeting the source field
3987
4612
  const targetField = getFieldTreeByKey(ctx, sourceFieldKey);
3988
4613
  if (!targetField) {
4614
+ evaluationContext.logger.warn(`Cross-field validator references non-existent field "${sourceFieldKey}"`);
3989
4615
  return null;
3990
4616
  }
3991
4617
  const errorObj = {
@@ -3998,8 +4624,8 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
3998
4624
  try {
3999
4625
  errorObj[key] = ExpressionParser.evaluate(expression, evaluationContext);
4000
4626
  }
4001
- catch {
4002
- // Ignore evaluation errors for params
4627
+ catch (error) {
4628
+ evaluationContext.logger.warn(`Failed to evaluate error param expression "${key}": ${expression}`, error);
4003
4629
  }
4004
4630
  }
4005
4631
  }
@@ -4033,6 +4659,7 @@ function evaluateBuiltInCrossFieldValidator(config, evaluationContext, sourceFie
4033
4659
  // Validation failed - create error targeting the source field
4034
4660
  const targetField = getFieldTreeByKey(ctx, sourceFieldKey);
4035
4661
  if (!targetField) {
4662
+ logger.warn(`Cross-field validator references non-existent field "${sourceFieldKey}"`);
4036
4663
  return null;
4037
4664
  }
4038
4665
  return {
@@ -4133,6 +4760,54 @@ function fieldsToDefaultValues(fields, registry) {
4133
4760
  return defaultValues;
4134
4761
  }
4135
4762
 
4763
+ /**
4764
+ * Injection token for globally enabling form value emission on events.
4765
+ *
4766
+ * When set to `true`, all events dispatched through the EventBus will include
4767
+ * the current form value in the `formValue` property.
4768
+ *
4769
+ * This token is configured via `withEventFormValue()` feature function.
4770
+ * Per-form overrides can be set via `options.emitFormValueOnEvents` in the form config.
4771
+ *
4772
+ * @internal
4773
+ */
4774
+ const EMIT_FORM_VALUE_ON_EVENTS = new InjectionToken('EMIT_FORM_VALUE_ON_EVENTS', {
4775
+ providedIn: 'root',
4776
+ factory: () => false,
4777
+ });
4778
+
4779
+ /**
4780
+ * Safely attempts to inject a dependency using an InjectionToken.
4781
+ * Returns the default value if called outside an injection context.
4782
+ */
4783
+ function safeInjectToken(token, defaultValue) {
4784
+ try {
4785
+ return inject(token, { optional: true }) ?? defaultValue;
4786
+ }
4787
+ catch {
4788
+ return defaultValue;
4789
+ }
4790
+ }
4791
+ /**
4792
+ * Safely attempts to inject a service class.
4793
+ * Returns the default value if called outside an injection context.
4794
+ */
4795
+ function safeInjectClass(token, defaultValue) {
4796
+ try {
4797
+ return inject(token, { optional: true }) ?? defaultValue;
4798
+ }
4799
+ catch {
4800
+ return defaultValue;
4801
+ }
4802
+ }
4803
+ /**
4804
+ * Creates a copy of an event with an additional formValue property.
4805
+ * Preserves the event's prototype chain so instanceof checks still work.
4806
+ */
4807
+ function attachFormValue(event, formValue) {
4808
+ const prototype = Object.getPrototypeOf(event);
4809
+ return Object.assign(Object.create(prototype), event, { formValue });
4810
+ }
4136
4811
  /**
4137
4812
  * Event bus service for form-wide event communication.
4138
4813
  *
@@ -4167,6 +4842,9 @@ function fieldsToDefaultValues(fields, registry) {
4167
4842
  */
4168
4843
  class EventBus {
4169
4844
  pipeline$ = new Subject();
4845
+ globalEmitFormValue = safeInjectToken(EMIT_FORM_VALUE_ON_EVENTS, false);
4846
+ rootFormRegistry = safeInjectClass(RootFormRegistryService, null);
4847
+ formOptions = safeInjectToken(FORM_OPTIONS, null);
4170
4848
  events$ = this.pipeline$.asObservable();
4171
4849
  /**
4172
4850
  * Dispatches an event to all subscribers.
@@ -4174,6 +4852,10 @@ class EventBus {
4174
4852
  * Creates an instance of the provided event constructor and broadcasts it
4175
4853
  * through the event pipeline to all active subscribers.
4176
4854
  *
4855
+ * If `withEventFormValue()` is enabled globally or `options.emitFormValueOnEvents`
4856
+ * is set to `true` in the form config, the current form value will be attached
4857
+ * to the event's `formValue` property.
4858
+ *
4177
4859
  * @param eventConstructor - Constructor function for the event to dispatch
4178
4860
  *
4179
4861
  * @example
@@ -4187,7 +4869,28 @@ class EventBus {
4187
4869
  */
4188
4870
  // TypeScript limitation: Must use ConstructorParameters which relies on `any` in FormEventConstructor
4189
4871
  dispatch(eventConstructor, ...args) {
4190
- this.pipeline$.next(new eventConstructor(...args));
4872
+ const event = new eventConstructor(...args);
4873
+ if (this.shouldEmitFormValue()) {
4874
+ const formValue = this.rootFormRegistry?.getFormValue();
4875
+ // Only attach if form value exists and has at least one field.
4876
+ // Empty objects {} are not attached - use hasFormValue() to check.
4877
+ if (formValue && Object.keys(formValue).length > 0) {
4878
+ this.pipeline$.next(attachFormValue(event, formValue));
4879
+ return;
4880
+ }
4881
+ }
4882
+ this.pipeline$.next(event);
4883
+ }
4884
+ /**
4885
+ * Determines whether form value should be attached to events.
4886
+ *
4887
+ * Precedence rules:
4888
+ * 1. Per-form setting (if defined) takes precedence
4889
+ * 2. Falls back to global setting
4890
+ */
4891
+ shouldEmitFormValue() {
4892
+ const formLevelSetting = this.formOptions?.()?.emitFormValueOnEvents;
4893
+ return formLevelSetting ?? this.globalEmitFormValue;
4191
4894
  }
4192
4895
  /**
4193
4896
  * Subscribes to form events with type-safe filtering.
@@ -4617,19 +5320,19 @@ function createEmptyCollection() {
4617
5320
  /** Traverses field definitions and collects cross-field entries (validators, logic, schemas). */
4618
5321
  function collectCrossFieldEntries(fields) {
4619
5322
  const collection = createEmptyCollection();
4620
- traverseFields(fields, collection);
5323
+ traverseFields$1(fields, collection);
4621
5324
  return collection;
4622
5325
  }
4623
- function traverseFields(fields, collection) {
5326
+ function traverseFields$1(fields, collection) {
4624
5327
  for (const field of fields) {
4625
- collectFromField(field, collection);
5328
+ collectFromField$1(field, collection);
4626
5329
  // Recursively process container fields (page, row, group, array)
4627
5330
  if (hasChildFields(field)) {
4628
- traverseFields(normalizeFieldsArray(field.fields), collection);
5331
+ traverseFields$1(normalizeFieldsArray(field.fields), collection);
4629
5332
  }
4630
5333
  }
4631
5334
  }
4632
- function collectFromField(field, collection) {
5335
+ function collectFromField$1(field, collection) {
4633
5336
  const fieldKey = field.key;
4634
5337
  if (!fieldKey)
4635
5338
  return;
@@ -4700,9 +5403,19 @@ function tryCreateValidatorEntry(fieldKey, config) {
4700
5403
  }
4701
5404
  return null;
4702
5405
  }
4703
- /** Returns a logic entry if cross-field, null otherwise. */
5406
+ /**
5407
+ * Returns a logic entry if cross-field state logic, null otherwise.
5408
+ *
5409
+ * Note: Derivation logic is handled separately by the derivation system
5410
+ * and is not collected here.
5411
+ */
4704
5412
  function tryCreateLogicEntry(fieldKey, config) {
4705
- if (!isCrossFieldLogic(config)) {
5413
+ // Only process state logic (hidden, readonly, disabled, required)
5414
+ // Derivation logic is handled by the derivation collector
5415
+ if (!isStateLogicConfig(config)) {
5416
+ return null;
5417
+ }
5418
+ if (!isCrossFieldStateLogic(config)) {
4706
5419
  return null;
4707
5420
  }
4708
5421
  const condition = config.condition;
@@ -4710,7 +5423,7 @@ function tryCreateLogicEntry(fieldKey, config) {
4710
5423
  sourceFieldKey: fieldKey,
4711
5424
  logicType: config.type,
4712
5425
  condition,
4713
- config,
5426
+ config: config,
4714
5427
  dependsOn: extractExpressionDependencies(condition),
4715
5428
  category: 'logic',
4716
5429
  };
@@ -4750,7 +5463,14 @@ function convertBuiltInToCustomValidator(config) {
4750
5463
  validationExpression = `fieldValue == null || (typeof fieldValue !== 'string' && !Array.isArray(fieldValue)) || fieldValue.length <= (${expression})`;
4751
5464
  break;
4752
5465
  case 'pattern':
4753
- validationExpression = `fieldValue == null || new RegExp(${expression}).test(fieldValue)`;
5466
+ // Validate regex pattern before injection to prevent runtime errors
5467
+ try {
5468
+ new RegExp(expression);
5469
+ }
5470
+ catch {
5471
+ throw new Error(`Invalid regex pattern in cross-field validator: ${expression}`);
5472
+ }
5473
+ validationExpression = `fieldValue == null || new RegExp(${JSON.stringify(expression)}).test(fieldValue)`;
4754
5474
  break;
4755
5475
  default:
4756
5476
  throw new Error(`Cannot convert ${config.type} validator to custom validator`);
@@ -4891,144 +5611,2510 @@ function isValidFormConfiguration(fields) {
4891
5611
  return FormModeValidator.validateFormConfiguration(fields).isValid;
4892
5612
  }
4893
5613
 
4894
- class NextPageEvent {
4895
- type = 'next-page';
4896
- }
4897
-
4898
- class PageChangeEvent {
4899
- currentPageIndex;
4900
- totalPages;
4901
- previousPageIndex;
4902
- type = 'page-change';
4903
- constructor(
4904
- /** The current page index (0-based) */
4905
- currentPageIndex,
4906
- /** Total number of pages */
4907
- totalPages,
4908
- /** Previous page index (0-based), undefined if first navigation */
4909
- previousPageIndex) {
4910
- this.currentPageIndex = currentPageIndex;
4911
- this.totalPages = totalPages;
4912
- this.previousPageIndex = previousPageIndex;
4913
- }
5614
+ /**
5615
+ * Creates an empty derivation collection.
5616
+ *
5617
+ * @returns Empty collection ready for population
5618
+ *
5619
+ * @internal
5620
+ */
5621
+ function createEmptyDerivationCollection() {
5622
+ return { entries: [] };
4914
5623
  }
4915
-
4916
- class PreviousPageEvent {
4917
- type = 'previous-page';
5624
+ /**
5625
+ * Creates a new derivation chain context for tracking applied derivations.
5626
+ *
5627
+ * @returns Fresh context for a new derivation cycle
5628
+ *
5629
+ * @internal
5630
+ */
5631
+ function createDerivationChainContext() {
5632
+ return {
5633
+ appliedDerivations: new Set(),
5634
+ iteration: 0,
5635
+ };
4918
5636
  }
4919
-
4920
5637
  /**
4921
- * Event dispatched to add a new item to an array field.
5638
+ * Creates a unique key for a derivation entry.
4922
5639
  *
4923
- * This event is used to dynamically add items to array fields at runtime.
4924
- * The array field component listens for this event and adds a new item
4925
- * based on the provided field template, or uses the array's own template if not provided.
5640
+ * Since derivations are self-targeting, the key is simply the field key.
5641
+ * This function exists for semantic clarity and to provide a single point
5642
+ * of change if the key format needs to be extended in the future.
4926
5643
  *
4927
- * @example
4928
- * ```typescript
4929
- * // Add item using array's own template
4930
- * eventBus.dispatch(AddArrayItemEvent, 'contacts');
5644
+ * Note: For array derivations, callers must resolve placeholders
5645
+ * (e.g., 'items.$.lineTotal' -> 'items.0.lineTotal') before calling this.
4931
5646
  *
4932
- * // Add item with explicit field template
4933
- * const fieldTemplate = { key: 'email', type: 'input' };
4934
- * eventBus.dispatch(AddArrayItemEvent, 'contacts', fieldTemplate);
5647
+ * @param fieldKey - The field key (with array index already resolved)
5648
+ * @returns Unique key for the derivation
4935
5649
  *
4936
- * // Add item at specific index
4937
- * eventBus.dispatch(AddArrayItemEvent, 'contacts', fieldTemplate, 2);
4938
- * ```
5650
+ * @internal
4939
5651
  */
4940
- class AddArrayItemEvent {
4941
- arrayKey;
4942
- field;
4943
- index;
4944
- type = 'add-array-item';
4945
- constructor(
4946
- /** The key of the array field to add an item to */
4947
- arrayKey,
4948
- /**
4949
- * Optional field template definition to render for the new array item.
4950
- * If not provided, the array field component will use its own template.
4951
- */
4952
- field,
4953
- /** Optional index where to insert the item (defaults to end of array) */
4954
- index) {
4955
- this.arrayKey = arrayKey;
4956
- this.field = field;
4957
- this.index = index;
4958
- }
5652
+ function createDerivationKey(fieldKey) {
5653
+ return fieldKey;
5654
+ }
5655
+ /**
5656
+ * Parses a derivation key back into field key.
5657
+ *
5658
+ * @param key - The derivation key to parse
5659
+ * @returns Object containing the field key
5660
+ *
5661
+ * @internal
5662
+ */
5663
+ function parseDerivationKey(key) {
5664
+ return { fieldKey: key };
4959
5665
  }
4960
5666
 
4961
5667
  /**
4962
- * Event dispatched to remove an item from an array field.
5668
+ * Injection token for the collected derivation entries.
4963
5669
  *
4964
- * This event is used to dynamically remove items from array fields at runtime.
4965
- * The array field component listens for this event and removes the item
4966
- * at the specified index.
5670
+ * Provided at form level, contains the topologically sorted entries
5671
+ * after validation. This is the "core data" that other derivation
5672
+ * components consume.
4967
5673
  *
4968
5674
  * @example
4969
5675
  * ```typescript
4970
- * // Remove item at index 1 from 'contacts' array
4971
- * eventBus.dispatch(RemoveArrayItemEvent, 'contacts', 1);
5676
+ * // In a component or service
5677
+ * private readonly entries = inject(DERIVATION_ENTRIES);
4972
5678
  *
4973
- * // Remove last item
4974
- * eventBus.dispatch(RemoveArrayItemEvent, 'contacts');
5679
+ * // Access the current entries
5680
+ * const currentEntries = this.entries();
4975
5681
  * ```
4976
5682
  */
4977
- class RemoveArrayItemEvent {
4978
- arrayKey;
4979
- index;
4980
- type = 'remove-array-item';
4981
- constructor(
4982
- /** The key of the array field to remove an item from */
4983
- arrayKey,
4984
- /** Index of the item to remove (defaults to last item) */
4985
- index) {
4986
- this.arrayKey = arrayKey;
4987
- this.index = index;
4988
- }
4989
- }
5683
+ const DERIVATION_ENTRIES = new InjectionToken('DerivationEntries');
4990
5684
 
4991
5685
  /**
4992
- * Event dispatched when the form should be reset to its default values.
5686
+ * Utilities for parsing and manipulating field paths in dynamic forms.
4993
5687
  *
4994
- * This event instructs the dynamic form component to restore all field values
4995
- * to their initial default values as defined in the form configuration.
5688
+ * Field paths follow these formats:
5689
+ * - Simple: "fieldName"
5690
+ * - Nested: "parent.child.grandchild"
5691
+ * - Array indexed: "items.0.quantity" or "items[0].quantity"
5692
+ * - Array placeholder: "items.$.quantity" ($ = any index)
4996
5693
  *
4997
- * @example
4998
- * ```typescript
4999
- * // Dispatch from a button component
5000
- * eventBus.dispatch(FormResetEvent);
5001
- * ```
5694
+ * @module path-utils
5695
+ */
5696
+ /**
5697
+ * Parses an array derivation path into its components.
5698
+ *
5699
+ * Array paths use the `$` placeholder to represent "any index".
5700
+ * Format: "arrayPath.$.relativePath"
5701
+ *
5702
+ * @param path - The path to parse (e.g., "items.$.quantity")
5703
+ * @returns Parsed path info with arrayPath and relativePath
5002
5704
  *
5003
5705
  * @example
5004
5706
  * ```typescript
5005
- * // Listen for reset events
5006
- * eventBus.on<FormResetEvent>('form-reset').subscribe(() => {
5007
- * console.log('Form was reset to defaults');
5008
- * });
5707
+ * parseArrayPath('items.$.quantity')
5708
+ * // { arrayPath: 'items', relativePath: 'quantity', isArrayPath: true }
5709
+ *
5710
+ * parseArrayPath('orders.lineItems.$.total')
5711
+ * // { arrayPath: 'orders.lineItems', relativePath: 'total', isArrayPath: true }
5712
+ *
5713
+ * parseArrayPath('simpleField')
5714
+ * // { arrayPath: '', relativePath: '', isArrayPath: false }
5009
5715
  * ```
5716
+ *
5717
+ * @public
5010
5718
  */
5011
- class FormResetEvent {
5012
- type = 'form-reset';
5719
+ function parseArrayPath(path) {
5720
+ const ARRAY_PLACEHOLDER = '.$.';
5721
+ const placeholderIndex = path.indexOf(ARRAY_PLACEHOLDER);
5722
+ if (placeholderIndex === -1) {
5723
+ return {
5724
+ arrayPath: '',
5725
+ relativePath: '',
5726
+ isArrayPath: false,
5727
+ };
5728
+ }
5729
+ return {
5730
+ arrayPath: path.substring(0, placeholderIndex),
5731
+ relativePath: path.substring(placeholderIndex + ARRAY_PLACEHOLDER.length),
5732
+ isArrayPath: true,
5733
+ };
5013
5734
  }
5014
-
5015
5735
  /**
5016
- * Event dispatched when the form should be cleared.
5736
+ * Resolves an array placeholder path to a concrete indexed path.
5017
5737
  *
5018
- * This event instructs the dynamic form component to clear all field values,
5019
- * resetting them to empty/undefined state regardless of their default values.
5738
+ * @param path - The path with $ placeholder (e.g., "items.$.quantity")
5739
+ * @param index - The array index to substitute
5740
+ * @returns Resolved path with index (e.g., "items.0.quantity")
5020
5741
  *
5021
5742
  * @example
5022
5743
  * ```typescript
5023
- * // Dispatch from a button component
5024
- * eventBus.dispatch(FormClearEvent);
5744
+ * resolveArrayPath('items.$.quantity', 2)
5745
+ * // 'items.2.quantity'
5746
+ *
5747
+ * resolveArrayPath('orders.lineItems.$.total', 0)
5748
+ * // 'orders.lineItems.0.total'
5025
5749
  * ```
5026
5750
  *
5027
- * @example
5028
- * ```typescript
5029
- * // Listen for clear events
5030
- * eventBus.on<FormClearEvent>('form-clear').subscribe(() => {
5031
- * console.log('Form was cleared');
5751
+ * @public
5752
+ */
5753
+ function resolveArrayPath(path, index) {
5754
+ const info = parseArrayPath(path);
5755
+ if (!info.isArrayPath) {
5756
+ return path;
5757
+ }
5758
+ return `${info.arrayPath}.${index}.${info.relativePath}`;
5759
+ }
5760
+ /**
5761
+ * Checks if a path contains an array placeholder.
5762
+ *
5763
+ * @param path - The path to check
5764
+ * @returns True if the path contains .$.
5765
+ *
5766
+ * @public
5767
+ */
5768
+ function isArrayPlaceholderPath(path) {
5769
+ return path.includes('.$.');
5770
+ }
5771
+ /**
5772
+ * Extracts the array path from a path that may contain an array placeholder.
5773
+ *
5774
+ * @param path - The path to extract from
5775
+ * @returns The array path, or empty string if not an array path
5776
+ *
5777
+ * @example
5778
+ * ```typescript
5779
+ * extractArrayPath('items.$.quantity')
5780
+ * // 'items'
5781
+ *
5782
+ * extractArrayPath('orders.lineItems.$.total')
5783
+ * // 'orders.lineItems'
5784
+ *
5785
+ * extractArrayPath('simpleField')
5786
+ * // ''
5787
+ * ```
5788
+ *
5789
+ * @public
5790
+ */
5791
+ function extractArrayPath(path) {
5792
+ return parseArrayPath(path).arrayPath;
5793
+ }
5794
+ /**
5795
+ * Splits a path into segments, supporting both dot notation and bracket notation.
5796
+ *
5797
+ * Handles:
5798
+ * - Dot notation: `parent.child.grandchild`
5799
+ * - Bracket notation: `items[0].quantity`
5800
+ * - Mixed notation: `items[0].address.city`
5801
+ *
5802
+ * @param path - The path to split
5803
+ * @returns Array of path segments
5804
+ *
5805
+ * @example
5806
+ * ```typescript
5807
+ * splitPath('parent.child.grandchild')
5808
+ * // ['parent', 'child', 'grandchild']
5809
+ *
5810
+ * splitPath('items.0.quantity')
5811
+ * // ['items', '0', 'quantity']
5812
+ *
5813
+ * splitPath('items[0].quantity')
5814
+ * // ['items', '0', 'quantity']
5815
+ *
5816
+ * splitPath('a[0][1].b')
5817
+ * // ['a', '0', '1', 'b']
5818
+ * ```
5819
+ *
5820
+ * @public
5821
+ */
5822
+ function splitPath(path) {
5823
+ if (!path)
5824
+ return [];
5825
+ // Convert bracket notation [n] to .n, then split by dots
5826
+ // Filter out empty segments from leading/trailing/consecutive dots
5827
+ return path
5828
+ .replace(/\[(\d+)\]/g, '.$1')
5829
+ .split('.')
5830
+ .filter(Boolean);
5831
+ }
5832
+ /**
5833
+ * Joins path segments into a dot-separated path.
5834
+ *
5835
+ * @param segments - The path segments to join
5836
+ * @returns Joined path string
5837
+ *
5838
+ * @public
5839
+ */
5840
+ function joinPath(segments) {
5841
+ return segments.join('.');
5842
+ }
5843
+ /**
5844
+ * Gets the parent path from a nested path.
5845
+ *
5846
+ * @param path - The path to get parent from
5847
+ * @returns The parent path, or empty string if no parent
5848
+ *
5849
+ * @example
5850
+ * ```typescript
5851
+ * getParentPath('parent.child.grandchild')
5852
+ * // 'parent.child'
5853
+ *
5854
+ * getParentPath('topLevel')
5855
+ * // ''
5856
+ * ```
5857
+ *
5858
+ * @public
5859
+ */
5860
+ function getParentPath(path) {
5861
+ const segments = splitPath(path);
5862
+ if (segments.length <= 1)
5863
+ return '';
5864
+ return joinPath(segments.slice(0, -1));
5865
+ }
5866
+ /**
5867
+ * Gets the last segment (leaf) from a path.
5868
+ *
5869
+ * @param path - The path to get leaf from
5870
+ * @returns The last segment
5871
+ *
5872
+ * @example
5873
+ * ```typescript
5874
+ * getLeafPath('parent.child.grandchild')
5875
+ * // 'grandchild'
5876
+ *
5877
+ * getLeafPath('topLevel')
5878
+ * // 'topLevel'
5879
+ * ```
5880
+ *
5881
+ * @public
5882
+ */
5883
+ function getLeafPath(path) {
5884
+ const segments = splitPath(path);
5885
+ return segments[segments.length - 1] ?? '';
5886
+ }
5887
+ /**
5888
+ * Parses a path that may contain multiple array placeholders.
5889
+ *
5890
+ * This supports deeply nested array structures like `orders.$.items.$.quantity`
5891
+ * where multiple levels of arrays need to be traversed.
5892
+ *
5893
+ * @param path - Path with zero or more $ placeholders
5894
+ * @returns Parsed path information
5895
+ *
5896
+ * @example
5897
+ * ```typescript
5898
+ * parseMultiArrayPath('orders.$.items.$.quantity')
5899
+ * // {
5900
+ * // isArrayPath: true,
5901
+ * // placeholderCount: 2,
5902
+ * // segments: ['orders', 'items', 'quantity'],
5903
+ * // placeholderPositions: [1, 3]
5904
+ * // }
5905
+ *
5906
+ * parseMultiArrayPath('items.$.name')
5907
+ * // {
5908
+ * // isArrayPath: true,
5909
+ * // placeholderCount: 1,
5910
+ * // segments: ['items', 'name'],
5911
+ * // placeholderPositions: [1]
5912
+ * // }
5913
+ *
5914
+ * parseMultiArrayPath('simpleField')
5915
+ * // {
5916
+ * // isArrayPath: false,
5917
+ * // placeholderCount: 0,
5918
+ * // segments: ['simpleField'],
5919
+ * // placeholderPositions: []
5920
+ * // }
5921
+ * ```
5922
+ *
5923
+ * @public
5924
+ */
5925
+ function parseMultiArrayPath(path) {
5926
+ const parts = path.split('.');
5927
+ const segments = [];
5928
+ const placeholderPositions = [];
5929
+ for (let i = 0; i < parts.length; i++) {
5930
+ if (parts[i] === '$') {
5931
+ placeholderPositions.push(i);
5932
+ }
5933
+ else {
5934
+ segments.push(parts[i]);
5935
+ }
5936
+ }
5937
+ return {
5938
+ isArrayPath: placeholderPositions.length > 0,
5939
+ placeholderCount: placeholderPositions.length,
5940
+ segments,
5941
+ placeholderPositions,
5942
+ };
5943
+ }
5944
+ /**
5945
+ * Resolves a path with multiple array placeholders using provided indices.
5946
+ *
5947
+ * Each `$` placeholder is replaced with the corresponding index from the
5948
+ * indices array, in order.
5949
+ *
5950
+ * @param path - Path with $ placeholders (e.g., 'orders.$.items.$.quantity')
5951
+ * @param indices - Array of indices to substitute (e.g., [0, 2])
5952
+ * @returns Resolved path (e.g., 'orders.0.items.2.quantity')
5953
+ *
5954
+ * @throws Error if number of indices doesn't match number of placeholders
5955
+ *
5956
+ * @example
5957
+ * ```typescript
5958
+ * resolveMultiArrayPath('orders.$.items.$.quantity', [0, 2])
5959
+ * // 'orders.0.items.2.quantity'
5960
+ *
5961
+ * resolveMultiArrayPath('items.$.name', [5])
5962
+ * // 'items.5.name'
5963
+ * ```
5964
+ *
5965
+ * @public
5966
+ */
5967
+ function resolveMultiArrayPath(path, indices) {
5968
+ const parts = path.split('.');
5969
+ let indexPointer = 0;
5970
+ const resolved = parts.map((part) => {
5971
+ if (part === '$') {
5972
+ if (indexPointer >= indices.length) {
5973
+ throw new Error(`Not enough indices provided for path '${path}'. ` +
5974
+ `Expected ${parts.filter((p) => p === '$').length} indices, got ${indices.length}.`);
5975
+ }
5976
+ return String(indices[indexPointer++]);
5977
+ }
5978
+ return part;
5979
+ });
5980
+ if (indexPointer < indices.length) {
5981
+ throw new Error(`Too many indices provided for path '${path}'. ` + `Expected ${indexPointer} indices, got ${indices.length}.`);
5982
+ }
5983
+ return resolved.join('.');
5984
+ }
5985
+ /**
5986
+ * Counts the number of array placeholders in a path.
5987
+ *
5988
+ * @param path - The path to analyze
5989
+ * @returns Number of $ placeholders in the path
5990
+ *
5991
+ * @example
5992
+ * ```typescript
5993
+ * countArrayPlaceholders('orders.$.items.$.quantity')
5994
+ * // 2
5995
+ *
5996
+ * countArrayPlaceholders('items.$.name')
5997
+ * // 1
5998
+ *
5999
+ * countArrayPlaceholders('simpleField')
6000
+ * // 0
6001
+ * ```
6002
+ *
6003
+ * @public
6004
+ */
6005
+ function countArrayPlaceholders(path) {
6006
+ return path.split('.').filter((part) => part === '$').length;
6007
+ }
6008
+
6009
+ /**
6010
+ * Provides lazy-computed lookup maps for derivation entries.
6011
+ *
6012
+ * Instead of pre-computing all lookup maps at collection time (which caused
6013
+ * hidden mutation and bloated the DerivationCollection god object), this class
6014
+ * computes maps lazily on first access and caches them.
6015
+ *
6016
+ * @example
6017
+ * ```typescript
6018
+ * const lookup = inject(DerivationLookup);
6019
+ *
6020
+ * // Lazy-computed on first access
6021
+ * const entriesForQuantity = lookup.byDependency.get('quantity');
6022
+ * const onChangeEntries = lookup.getOnChangeEntries();
6023
+ * ```
6024
+ */
6025
+ class DerivationLookup {
6026
+ entriesSignal = inject(DERIVATION_ENTRIES);
6027
+ // Track the entries array reference to auto-invalidate on change
6028
+ _cachedEntriesRef;
6029
+ // Cached lookup maps (built lazily on first access)
6030
+ _byField;
6031
+ _byDependency;
6032
+ _byArrayPath;
6033
+ _wildcardEntries;
6034
+ _onChangeEntries;
6035
+ _debouncedEntriesByMs;
6036
+ /**
6037
+ * Current derivation entries (from signal).
6038
+ * Auto-invalidates cache if entries reference changed.
6039
+ */
6040
+ get entries() {
6041
+ const currentEntries = this.entriesSignal();
6042
+ // Auto-invalidate if entries reference changed (schema was updated)
6043
+ if (this._cachedEntriesRef !== currentEntries) {
6044
+ this.invalidateCache();
6045
+ this._cachedEntriesRef = currentEntries;
6046
+ }
6047
+ return currentEntries;
6048
+ }
6049
+ /**
6050
+ * Map of field key to entries that target it.
6051
+ * Since derivations are self-targeting, this maps field keys to their derivations.
6052
+ * Used for quick lookup when processing derivations.
6053
+ */
6054
+ get byField() {
6055
+ return (this._byField ??= this.buildByFieldMap());
6056
+ }
6057
+ /**
6058
+ * Map of dependency field key to entries that depend on it.
6059
+ * Used for efficient O(1) lookup when filtering by changed fields.
6060
+ */
6061
+ get byDependency() {
6062
+ return (this._byDependency ??= this.buildByDependencyMap());
6063
+ }
6064
+ /**
6065
+ * Map of array path to entries that derive array item values.
6066
+ * Used for O(1) lookup when an array field changes.
6067
+ */
6068
+ get byArrayPath() {
6069
+ return (this._byArrayPath ??= this.buildByArrayPathMap());
6070
+ }
6071
+ /**
6072
+ * Entries that have wildcard (*) dependency.
6073
+ * These entries depend on all form values and must always be considered.
6074
+ */
6075
+ get wildcardEntries() {
6076
+ return (this._wildcardEntries ??= this.buildWildcardEntries());
6077
+ }
6078
+ /**
6079
+ * Gets entries that should be processed based on changed fields.
6080
+ *
6081
+ * Handles nested path matching: if 'address' changed, entries depending on
6082
+ * 'address.city' will also be included (parent change implies child changed).
6083
+ *
6084
+ * Uses O(k) lookup via indexed maps for exact matches, plus O(d) scan for
6085
+ * nested dependencies, where k = changed fields, d = unique dependencies.
6086
+ */
6087
+ getEntriesForChangedFields(changedFields) {
6088
+ const entrySet = new Set();
6089
+ // Add entries triggered by each changed field via byDependency map
6090
+ for (const fieldKey of changedFields) {
6091
+ const entries = this.byDependency.get(fieldKey);
6092
+ if (entries) {
6093
+ for (const entry of entries) {
6094
+ entrySet.add(entry);
6095
+ }
6096
+ }
6097
+ // Check if this changed field is an array path - O(1) lookup
6098
+ const arrayEntries = this.byArrayPath.get(fieldKey);
6099
+ if (arrayEntries) {
6100
+ for (const entry of arrayEntries) {
6101
+ entrySet.add(entry);
6102
+ }
6103
+ }
6104
+ // Handle nested dependencies: if 'address' changed, include entries
6105
+ // that depend on 'address.city', 'address.street', etc.
6106
+ // This is O(d) where d = number of unique dependency keys in the map.
6107
+ const fieldKeyPrefix = fieldKey + '.';
6108
+ for (const [depKey, depEntries] of this.byDependency) {
6109
+ if (depKey.startsWith(fieldKeyPrefix)) {
6110
+ for (const entry of depEntries) {
6111
+ entrySet.add(entry);
6112
+ }
6113
+ }
6114
+ }
6115
+ }
6116
+ // Add all wildcard entries - O(w) where w = number of wildcard entries
6117
+ for (const entry of this.wildcardEntries) {
6118
+ entrySet.add(entry);
6119
+ }
6120
+ return Array.from(entrySet);
6121
+ }
6122
+ /**
6123
+ * Gets entries with trigger 'onChange' (or no trigger, which defaults to onChange).
6124
+ */
6125
+ getOnChangeEntries() {
6126
+ return (this._onChangeEntries ??= this.entries.filter((e) => e.trigger !== 'debounced'));
6127
+ }
6128
+ /**
6129
+ * Gets entries with trigger 'debounced' for a specific debounce period.
6130
+ */
6131
+ getDebouncedEntries(debounceMs) {
6132
+ if (!this._debouncedEntriesByMs) {
6133
+ this._debouncedEntriesByMs = this.buildDebouncedEntriesMap();
6134
+ }
6135
+ return this._debouncedEntriesByMs.get(debounceMs) ?? [];
6136
+ }
6137
+ /**
6138
+ * Gets all unique debounce periods that have entries.
6139
+ */
6140
+ getDebouncePeriods() {
6141
+ if (!this._debouncedEntriesByMs) {
6142
+ this._debouncedEntriesByMs = this.buildDebouncedEntriesMap();
6143
+ }
6144
+ return Array.from(this._debouncedEntriesByMs.keys());
6145
+ }
6146
+ /**
6147
+ * Invalidates cached maps. Call this when entries change.
6148
+ */
6149
+ invalidateCache() {
6150
+ this._byField = undefined;
6151
+ this._byDependency = undefined;
6152
+ this._byArrayPath = undefined;
6153
+ this._wildcardEntries = undefined;
6154
+ this._onChangeEntries = undefined;
6155
+ this._debouncedEntriesByMs = undefined;
6156
+ }
6157
+ // ─────────────────────────────────────────────────────────────────
6158
+ // Private builders
6159
+ // ─────────────────────────────────────────────────────────────────
6160
+ buildByFieldMap() {
6161
+ const map = new Map();
6162
+ for (const entry of this.entries) {
6163
+ const fieldEntries = map.get(entry.fieldKey) ?? [];
6164
+ fieldEntries.push(entry);
6165
+ map.set(entry.fieldKey, fieldEntries);
6166
+ }
6167
+ return map;
6168
+ }
6169
+ buildByDependencyMap() {
6170
+ const map = new Map();
6171
+ for (const entry of this.entries) {
6172
+ for (const dep of entry.dependsOn) {
6173
+ if (dep !== '*') {
6174
+ const depEntries = map.get(dep) ?? [];
6175
+ depEntries.push(entry);
6176
+ map.set(dep, depEntries);
6177
+ }
6178
+ }
6179
+ }
6180
+ return map;
6181
+ }
6182
+ buildByArrayPathMap() {
6183
+ const map = new Map();
6184
+ for (const entry of this.entries) {
6185
+ if (isArrayPlaceholderPath(entry.fieldKey)) {
6186
+ const arrayPath = extractArrayPath(entry.fieldKey);
6187
+ if (arrayPath) {
6188
+ const arrayEntries = map.get(arrayPath) ?? [];
6189
+ arrayEntries.push(entry);
6190
+ map.set(arrayPath, arrayEntries);
6191
+ }
6192
+ }
6193
+ }
6194
+ return map;
6195
+ }
6196
+ buildWildcardEntries() {
6197
+ return this.entries.filter((entry) => entry.dependsOn.includes('*'));
6198
+ }
6199
+ buildDebouncedEntriesMap() {
6200
+ const map = new Map();
6201
+ for (const entry of this.entries) {
6202
+ if (entry.trigger === 'debounced') {
6203
+ const ms = entry.debounceMs ?? DEFAULT_DEBOUNCE_MS;
6204
+ const group = map.get(ms) ?? [];
6205
+ group.push(entry);
6206
+ map.set(ms, group);
6207
+ }
6208
+ }
6209
+ return map;
6210
+ }
6211
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DerivationLookup, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
6212
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DerivationLookup });
6213
+ }
6214
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DerivationLookup, decorators: [{
6215
+ type: Injectable
6216
+ }] });
6217
+
6218
+ /**
6219
+ * Sorts derivation entries in topological order based on their dependencies.
6220
+ *
6221
+ * This ensures that derivations are processed in the correct order:
6222
+ * if derivation B depends on field A, and derivation A->A' modifies A,
6223
+ * then A->A' is processed before B.
6224
+ *
6225
+ * Uses Kahn's algorithm for topological sorting, which also provides
6226
+ * natural handling of entries with no dependencies (they come first).
6227
+ *
6228
+ * @param entries - The derivation entries to sort
6229
+ * @returns A new array of entries in topological order
6230
+ *
6231
+ * @example
6232
+ * ```typescript
6233
+ * // Given derivations:
6234
+ * // 1. quantity -> lineTotal (depends on quantity, unitPrice)
6235
+ * // 2. unitPrice -> lineTotal (depends on quantity, unitPrice)
6236
+ * // 3. lineTotal -> grandTotal (depends on lineTotal)
6237
+ * //
6238
+ * // Topological sort ensures lineTotal is computed before grandTotal
6239
+ * const sorted = topologicalSort(entries);
6240
+ * // Result: [1, 2, 3] - derivations that produce lineTotal come before
6241
+ * // those that consume it
6242
+ * ```
6243
+ *
6244
+ * @public
6245
+ */
6246
+ function topologicalSort(entries) {
6247
+ if (entries.length <= 1) {
6248
+ return [...entries];
6249
+ }
6250
+ // Build adjacency list and in-degree count
6251
+ // An edge A -> B exists if B depends on the target of A
6252
+ const graph = new Map();
6253
+ const inDegree = new Map();
6254
+ // Initialize
6255
+ for (const entry of entries) {
6256
+ graph.set(entry, new Set());
6257
+ inDegree.set(entry, 0);
6258
+ }
6259
+ // Pre-build index: field key -> entries that produce/target it
6260
+ // This enables O(1) lookup instead of O(n) scanning
6261
+ const derivationsByField = new Map();
6262
+ for (const entry of entries) {
6263
+ // Index by exact field key
6264
+ const fieldEntries = derivationsByField.get(entry.fieldKey) ?? [];
6265
+ fieldEntries.push(entry);
6266
+ derivationsByField.set(entry.fieldKey, fieldEntries);
6267
+ // Also index array field relative paths for array derivations
6268
+ // If entry targets 'items.$.lineTotal', index under 'lineTotal' too
6269
+ if (entry.fieldKey.includes('.$.')) {
6270
+ const relativePath = entry.fieldKey.split('.$.')[1];
6271
+ if (relativePath) {
6272
+ const relativeEntries = derivationsByField.get(relativePath) ?? [];
6273
+ relativeEntries.push(entry);
6274
+ derivationsByField.set(relativePath, relativeEntries);
6275
+ }
6276
+ }
6277
+ }
6278
+ // Collect entries with wildcard dependencies (need special handling)
6279
+ const wildcardDependents = [];
6280
+ for (const entry of entries) {
6281
+ if (entry.dependsOn.includes('*')) {
6282
+ wildcardDependents.push(entry);
6283
+ }
6284
+ }
6285
+ // Build edges using pre-computed index
6286
+ // For each entry B, find all entries A whose field is in B's dependencies
6287
+ // If B depends on field X, and A produces X, then A must run before B
6288
+ for (const entryB of entries) {
6289
+ for (const dep of entryB.dependsOn) {
6290
+ // Skip wildcards here - handled separately below
6291
+ if (dep === '*')
6292
+ continue;
6293
+ // O(1) lookup: find all derivations that produce this dependency
6294
+ const producers = derivationsByField.get(dep);
6295
+ if (producers) {
6296
+ for (const entryA of producers) {
6297
+ const entryAEdges = graph.get(entryA);
6298
+ if (entryA !== entryB && entryAEdges && !entryAEdges.has(entryB)) {
6299
+ entryAEdges.add(entryB);
6300
+ inDegree.set(entryB, (inDegree.get(entryB) ?? 0) + 1);
6301
+ }
6302
+ }
6303
+ }
6304
+ }
6305
+ }
6306
+ // Handle wildcard dependencies: they depend on ALL producers
6307
+ for (const entryB of wildcardDependents) {
6308
+ for (const entryA of entries) {
6309
+ const entryAEdges = graph.get(entryA);
6310
+ if (entryA !== entryB && entryAEdges && !entryAEdges.has(entryB)) {
6311
+ entryAEdges.add(entryB);
6312
+ inDegree.set(entryB, (inDegree.get(entryB) ?? 0) + 1);
6313
+ }
6314
+ }
6315
+ }
6316
+ // Kahn's algorithm: process nodes with in-degree 0
6317
+ const queue = [];
6318
+ const sorted = [];
6319
+ // Start with entries that have no dependencies on other derivation outputs
6320
+ for (const entry of entries) {
6321
+ if (inDegree.get(entry) === 0) {
6322
+ queue.push(entry);
6323
+ }
6324
+ }
6325
+ while (queue.length > 0) {
6326
+ const current = queue.shift();
6327
+ if (!current)
6328
+ break; // TypeScript guard (shouldn't happen due to while condition)
6329
+ sorted.push(current);
6330
+ // Reduce in-degree for all neighbors
6331
+ const neighbors = graph.get(current) ?? new Set();
6332
+ for (const neighbor of neighbors) {
6333
+ const newDegree = (inDegree.get(neighbor) ?? 0) - 1;
6334
+ inDegree.set(neighbor, newDegree);
6335
+ if (newDegree === 0) {
6336
+ queue.push(neighbor);
6337
+ }
6338
+ }
6339
+ }
6340
+ // If we couldn't sort all entries, there's a cycle
6341
+ // (but cycles should be caught by cycle-detector, so this is defensive)
6342
+ if (sorted.length !== entries.length) {
6343
+ // Return original order for entries in cycles
6344
+ const sortedSet = new Set(sorted);
6345
+ for (const entry of entries) {
6346
+ if (!sortedSet.has(entry)) {
6347
+ sorted.push(entry);
6348
+ }
6349
+ }
6350
+ }
6351
+ return sorted;
6352
+ }
6353
+
6354
+ /**
6355
+ * Collects all derivation entries from field definitions.
6356
+ *
6357
+ * This function traverses the field definition tree and extracts:
6358
+ * - Shorthand `derivation` properties on fields
6359
+ * - Full `logic` array entries with `type: 'derivation'`
6360
+ *
6361
+ * The collected entries include dependency information for cycle detection
6362
+ * and reactive evaluation. Entries are sorted topologically so derivations
6363
+ * are processed in dependency order.
6364
+ *
6365
+ * Lookup maps (byTarget, byDependency, etc.) are NOT built here - they are
6366
+ * computed lazily via {@link DerivationLookup} to avoid hidden mutation
6367
+ * and reduce the size of the returned collection.
6368
+ *
6369
+ * @param fields - Array of field definitions to traverse
6370
+ * @returns Collection containing sorted derivation entries
6371
+ *
6372
+ * @example
6373
+ * ```typescript
6374
+ * const fields = [
6375
+ * { key: 'quantity', type: 'number' },
6376
+ * { key: 'unitPrice', type: 'number' },
6377
+ * {
6378
+ * key: 'total',
6379
+ * type: 'number',
6380
+ * derivation: 'formValue.quantity * formValue.unitPrice',
6381
+ * },
6382
+ * ];
6383
+ *
6384
+ * const collection = collectDerivations(fields);
6385
+ * // collection.entries has 1 entry for the 'total' field derivation
6386
+ * ```
6387
+ *
6388
+ * @public
6389
+ */
6390
+ function collectDerivations(fields) {
6391
+ const entries = [];
6392
+ const context = {};
6393
+ traverseFields(fields, entries, context);
6394
+ // Sort entries in topological order for efficient processing.
6395
+ // This ensures derivations are processed in dependency order,
6396
+ // reducing the number of iterations needed in the applicator.
6397
+ const sortedEntries = topologicalSort(entries);
6398
+ return { entries: sortedEntries };
6399
+ }
6400
+ /**
6401
+ * Recursively traverses field definitions to collect derivations.
6402
+ *
6403
+ * @internal
6404
+ */
6405
+ function traverseFields(fields, entries, context) {
6406
+ for (const field of fields) {
6407
+ collectFromField(field, entries, context);
6408
+ // Recursively process container fields (page, row, group, array)
6409
+ if (hasChildFields(field)) {
6410
+ const childContext = { ...context };
6411
+ // Update array path context if this is an array field
6412
+ if (field.type === 'array') {
6413
+ childContext.arrayPath = field.key;
6414
+ }
6415
+ traverseFields(normalizeFieldsArray(field.fields), entries, childContext);
6416
+ }
6417
+ }
6418
+ }
6419
+ /**
6420
+ * Collects derivation entries from a single field.
6421
+ *
6422
+ * @internal
6423
+ */
6424
+ function collectFromField(field, entries, context) {
6425
+ const fieldKey = field.key;
6426
+ if (!fieldKey)
6427
+ return;
6428
+ const validationField = field;
6429
+ // Collect shorthand derivation property
6430
+ if (validationField.derivation) {
6431
+ const entry = createShorthandEntry(fieldKey, validationField.derivation, context);
6432
+ entries.push(entry);
6433
+ }
6434
+ // Collect logic array derivations
6435
+ if (validationField.logic) {
6436
+ for (const logicConfig of validationField.logic) {
6437
+ if (isDerivationLogicConfig(logicConfig)) {
6438
+ const entry = createLogicEntry(fieldKey, logicConfig, context);
6439
+ entries.push(entry);
6440
+ }
6441
+ }
6442
+ }
6443
+ }
6444
+ /**
6445
+ * Creates a derivation entry from the shorthand `derivation` property.
6446
+ *
6447
+ * Shorthand derivations:
6448
+ * - Target the same field they're defined on (self-targeting)
6449
+ * - Always apply (condition defaults to true)
6450
+ * - Always trigger on change
6451
+ *
6452
+ * @internal
6453
+ */
6454
+ function createShorthandEntry(fieldKey, expression, context) {
6455
+ // Build the effective field key, including array path if in array context
6456
+ const effectiveFieldKey = context.arrayPath ? `${context.arrayPath}.$.${fieldKey}` : fieldKey;
6457
+ return {
6458
+ fieldKey: effectiveFieldKey,
6459
+ dependsOn: extractStringDependencies(expression),
6460
+ condition: true,
6461
+ expression,
6462
+ trigger: 'onChange',
6463
+ isShorthand: true,
6464
+ };
6465
+ }
6466
+ /**
6467
+ * Creates a derivation entry from a full `DerivationLogicConfig`.
6468
+ *
6469
+ * All derivations are self-targeting: the derivation is defined on and targets
6470
+ * the same field (fieldKey). For array fields, the context is used to build
6471
+ * the array placeholder path.
6472
+ *
6473
+ * Handles:
6474
+ * - Condition extraction and dependency analysis
6475
+ * - Array field context for placeholder paths
6476
+ * - Multiple value source types (static, expression, function)
6477
+ * - Trigger and debounce configuration
6478
+ *
6479
+ * @internal
6480
+ */
6481
+ function createLogicEntry(fieldKey, config, context) {
6482
+ // Build the effective field key, including array path if in array context
6483
+ const effectiveFieldKey = context.arrayPath ? `${context.arrayPath}.$.${fieldKey}` : fieldKey;
6484
+ const dependsOn = extractDependencies(config);
6485
+ const condition = config.condition ?? true;
6486
+ const trigger = config.trigger ?? 'onChange';
6487
+ // Extract debounceMs from debounced configs
6488
+ // The type system ensures debounceMs is only present when trigger is 'debounced'
6489
+ const debounceMs = trigger === 'debounced' ? config.debounceMs : undefined;
6490
+ return {
6491
+ fieldKey: effectiveFieldKey,
6492
+ dependsOn,
6493
+ condition,
6494
+ value: config.value,
6495
+ expression: config.expression,
6496
+ functionName: config.functionName,
6497
+ trigger,
6498
+ debounceMs,
6499
+ isShorthand: false,
6500
+ originalConfig: config,
6501
+ debugName: config.debugName,
6502
+ };
6503
+ }
6504
+ /**
6505
+ * Extracts all field dependencies from a derivation config.
6506
+ *
6507
+ * Combines dependencies from:
6508
+ * - Explicit `dependsOn` array (if provided, takes precedence)
6509
+ * - Condition expression
6510
+ * - Value expression
6511
+ * - Function name (defaults to '*' if no explicit dependsOn)
6512
+ *
6513
+ * @internal
6514
+ */
6515
+ function extractDependencies(config) {
6516
+ const deps = new Set();
6517
+ // If explicit dependsOn is provided, use it as the primary source
6518
+ // This allows users to override automatic detection and control
6519
+ // when derivations are triggered
6520
+ if (config.dependsOn && config.dependsOn.length > 0) {
6521
+ config.dependsOn.forEach((dep) => deps.add(dep));
6522
+ }
6523
+ else {
6524
+ // Extract from expression (automatic dependency detection)
6525
+ if (config.expression) {
6526
+ const exprDeps = extractStringDependencies(config.expression);
6527
+ exprDeps.forEach((dep) => deps.add(dep));
6528
+ }
6529
+ // Custom functions assume full form access if no explicit dependsOn
6530
+ if (config.functionName) {
6531
+ deps.add('*');
6532
+ }
6533
+ }
6534
+ // Always extract from condition (these are additional runtime guards)
6535
+ if (config.condition && config.condition !== true) {
6536
+ const conditionDeps = extractExpressionDependencies(config.condition);
6537
+ conditionDeps.forEach((dep) => deps.add(dep));
6538
+ }
6539
+ return Array.from(deps);
6540
+ }
6541
+
6542
+ /**
6543
+ * Detects cycles in the derivation dependency graph.
6544
+ *
6545
+ * Uses Kahn's algorithm variant with DFS to detect cycles.
6546
+ * Bidirectional sync patterns (A→B→A) are allowed because they stabilize
6547
+ * via the equality check at runtime.
6548
+ *
6549
+ * @param collection - The collected derivation entries
6550
+ * @returns Result indicating whether a cycle exists and details if found
6551
+ *
6552
+ * @example
6553
+ * ```typescript
6554
+ * const collection = collectDerivations(fields);
6555
+ * const result = detectCycles(collection);
6556
+ *
6557
+ * if (result.hasCycle) {
6558
+ * console.error(`Cycle detected: ${result.cyclePath?.join(' -> ')}`);
6559
+ * throw new Error(result.errorMessage);
6560
+ * }
6561
+ * ```
6562
+ *
6563
+ * @public
6564
+ */
6565
+ function detectCycles(collection) {
6566
+ // Build directed graph from derivation entries
6567
+ const graph = buildDependencyGraph(collection);
6568
+ // No entries means no cycles
6569
+ if (graph.size === 0) {
6570
+ return { hasCycle: false };
6571
+ }
6572
+ // Build bidirectional pairs set for cycle exemption
6573
+ const bidirectionalPairs = detectBidirectionalPairs(collection);
6574
+ // Run DFS to detect cycles
6575
+ const result = detectCyclesWithDFS(graph, bidirectionalPairs);
6576
+ // Include bidirectional pairs in result for warnings
6577
+ if (bidirectionalPairs.size > 0) {
6578
+ result.bidirectionalPairs = Array.from(bidirectionalPairs);
6579
+ }
6580
+ return result;
6581
+ }
6582
+ /**
6583
+ * Detects bidirectional derivation pairs (A↔B patterns).
6584
+ *
6585
+ * These patterns are allowed because they stabilize via equality checks.
6586
+ * Example: USD/EUR conversion where both fields derive from each other.
6587
+ *
6588
+ * A bidirectional pair exists when:
6589
+ * - Field A derives its value from field B (A depends on B)
6590
+ * - Field B derives its value from field A (B depends on A)
6591
+ *
6592
+ * ## Floating-Point Precision Note
6593
+ *
6594
+ * Bidirectional derivations stabilize via equality checks using exact IEEE 754
6595
+ * comparison with no tolerance. This means:
6596
+ *
6597
+ * - **Integer math**: Safe (e.g., `A = B * 2`, `B = A / 2` where A is even)
6598
+ * - **Floating-point math**: May oscillate due to rounding errors
6599
+ *
6600
+ * For currency conversions or other floating-point operations, consider:
6601
+ * 1. Rounding values in your expression (e.g., `Math.round(value * 100) / 100`)
6602
+ * 2. Using integer cents instead of decimal dollars
6603
+ * 3. Using one-way derivation instead of bidirectional
6604
+ *
6605
+ * @example
6606
+ * ```typescript
6607
+ * // Bidirectional currency conversion
6608
+ * { key: 'amountUSD', derivation: 'formValue.amountEUR * 1.1' }
6609
+ * { key: 'amountEUR', derivation: 'formValue.amountUSD / 1.1' }
6610
+ * ```
6611
+ *
6612
+ * @internal
6613
+ */
6614
+ function detectBidirectionalPairs(collection) {
6615
+ const pairs = new Set();
6616
+ // Build a map of fieldKey -> dependencies (other fields this derivation reads from)
6617
+ const dependencyMap = new Map();
6618
+ for (const entry of collection.entries) {
6619
+ const deps = dependencyMap.get(entry.fieldKey) ?? new Set();
6620
+ for (const dep of entry.dependsOn) {
6621
+ if (dep !== '*' && dep !== entry.fieldKey) {
6622
+ deps.add(dep);
6623
+ }
6624
+ }
6625
+ dependencyMap.set(entry.fieldKey, deps);
6626
+ }
6627
+ // Find bidirectional pairs: A depends on B AND B depends on A
6628
+ for (const [fieldA, depsA] of dependencyMap) {
6629
+ for (const fieldB of depsA) {
6630
+ const depsB = dependencyMap.get(fieldB);
6631
+ if (depsB?.has(fieldA)) {
6632
+ // Normalize pair key (alphabetically sorted)
6633
+ const pairKey = fieldA < fieldB ? `${fieldA}↔${fieldB}` : `${fieldB}↔${fieldA}`;
6634
+ pairs.add(pairKey);
6635
+ }
6636
+ }
6637
+ }
6638
+ return pairs;
6639
+ }
6640
+ /**
6641
+ * Checks if a cycle path represents a bidirectional pair.
6642
+ *
6643
+ * @internal
6644
+ */
6645
+ function isBidirectionalCycle(cyclePath, bidirectionalPairs) {
6646
+ // Bidirectional cycles are exactly: [A, B, A] (length 3 with first=last)
6647
+ if (cyclePath.length !== 3) {
6648
+ return false;
6649
+ }
6650
+ const [first, second, third] = cyclePath;
6651
+ if (first !== third) {
6652
+ return false;
6653
+ }
6654
+ // Check if this pair is in our bidirectional set
6655
+ const pairKey = first < second ? `${first}↔${second}` : `${second}↔${first}`;
6656
+ return bidirectionalPairs.has(pairKey);
6657
+ }
6658
+ /**
6659
+ * Builds a dependency graph from derivation entries.
6660
+ *
6661
+ * Creates nodes for each field involved in derivations and
6662
+ * edges representing the derivation dependencies.
6663
+ *
6664
+ * Edge direction is based on dependencies:
6665
+ * - If field A's derivation depends on field B, then B -> A (changing B triggers A)
6666
+ * - A cycle exists if: A -> B -> C -> A (circular dependency chain)
6667
+ *
6668
+ * @internal
6669
+ */
6670
+ function buildDependencyGraph(collection) {
6671
+ const graph = new Map();
6672
+ // Helper to ensure a node exists
6673
+ const ensureNode = (fieldKey) => {
6674
+ let node = graph.get(fieldKey);
6675
+ if (!node) {
6676
+ node = {
6677
+ fieldKey,
6678
+ dependsOn: new Set(),
6679
+ dependedOnBy: new Set(),
6680
+ };
6681
+ graph.set(fieldKey, node);
6682
+ }
6683
+ return node;
6684
+ };
6685
+ for (const entry of collection.entries) {
6686
+ const fieldNode = ensureNode(entry.fieldKey);
6687
+ // Track dependencies from the derivation expression/condition.
6688
+ // For cycle detection, edges go from dependency -> field:
6689
+ // If field A depends on field B, then when B changes, A gets updated.
6690
+ // A cycle exists when there's a circular chain of such dependencies.
6691
+ for (const dep of entry.dependsOn) {
6692
+ // Skip wildcard dependencies and self-references
6693
+ if (dep !== '*' && dep !== entry.fieldKey) {
6694
+ const depNode = ensureNode(dep);
6695
+ // The field depends on 'dep'
6696
+ fieldNode.dependsOn.add(dep);
6697
+ // 'dep' is depended on by the field
6698
+ depNode.dependedOnBy.add(entry.fieldKey);
6699
+ }
6700
+ }
6701
+ }
6702
+ return graph;
6703
+ }
6704
+ /**
6705
+ * Detects cycles using depth-first search.
6706
+ *
6707
+ * Uses three-color marking:
6708
+ * - Unvisited (white): Not yet processed
6709
+ * - InProgress (gray): Currently in the DFS stack
6710
+ * - Completed (black): Fully processed
6711
+ *
6712
+ * A cycle is detected when we visit a node that's InProgress,
6713
+ * unless it's a bidirectional sync pattern (allowed).
6714
+ *
6715
+ * @internal
6716
+ */
6717
+ function detectCyclesWithDFS(graph, bidirectionalPairs) {
6718
+ const visitState = new Map();
6719
+ const parent = new Map();
6720
+ // Initialize all nodes as unvisited
6721
+ for (const fieldKey of graph.keys()) {
6722
+ visitState.set(fieldKey, 0 /* VisitState.Unvisited */);
6723
+ }
6724
+ // Process each unvisited node
6725
+ for (const fieldKey of graph.keys()) {
6726
+ if (visitState.get(fieldKey) === 0 /* VisitState.Unvisited */) {
6727
+ const cycleResult = dfsVisit(fieldKey, graph, visitState, parent, [], bidirectionalPairs);
6728
+ if (cycleResult) {
6729
+ return cycleResult;
6730
+ }
6731
+ }
6732
+ }
6733
+ return { hasCycle: false };
6734
+ }
6735
+ /**
6736
+ * DFS visit function that detects back edges (cycles).
6737
+ *
6738
+ * @internal
6739
+ */
6740
+ function dfsVisit(fieldKey, graph, visitState, parent, path, bidirectionalPairs) {
6741
+ visitState.set(fieldKey, 1 /* VisitState.InProgress */);
6742
+ path.push(fieldKey);
6743
+ const node = graph.get(fieldKey);
6744
+ if (!node) {
6745
+ visitState.set(fieldKey, 2 /* VisitState.Completed */);
6746
+ path.pop();
6747
+ return null;
6748
+ }
6749
+ // Check all nodes that this field's changes would trigger
6750
+ for (const targetFieldKey of node.dependedOnBy) {
6751
+ const targetState = visitState.get(targetFieldKey);
6752
+ if (targetState === 1 /* VisitState.InProgress */) {
6753
+ // Back edge found - potential cycle
6754
+ const cyclePath = extractCyclePath(path, targetFieldKey);
6755
+ // Allow bidirectional sync patterns (A→B→A)
6756
+ // These stabilize via equality checks at runtime
6757
+ if (isBidirectionalCycle(cyclePath, bidirectionalPairs)) {
6758
+ // Continue DFS without reporting this as a cycle
6759
+ continue;
6760
+ }
6761
+ return {
6762
+ hasCycle: true,
6763
+ cyclePath,
6764
+ errorMessage: formatCycleError(cyclePath),
6765
+ };
6766
+ }
6767
+ if (targetState === 0 /* VisitState.Unvisited */) {
6768
+ parent.set(targetFieldKey, fieldKey);
6769
+ const result = dfsVisit(targetFieldKey, graph, visitState, parent, path, bidirectionalPairs);
6770
+ if (result) {
6771
+ return result;
6772
+ }
6773
+ }
6774
+ }
6775
+ visitState.set(fieldKey, 2 /* VisitState.Completed */);
6776
+ path.pop();
6777
+ return null;
6778
+ }
6779
+ /**
6780
+ * Extracts the cycle path from the DFS path.
6781
+ *
6782
+ * @internal
6783
+ */
6784
+ function extractCyclePath(path, cycleStart) {
6785
+ const cycleStartIndex = path.indexOf(cycleStart);
6786
+ if (cycleStartIndex === -1) {
6787
+ // Shouldn't happen, but handle gracefully
6788
+ return [...path, cycleStart];
6789
+ }
6790
+ // Extract from cycle start to current position, then back to start
6791
+ const cyclePath = path.slice(cycleStartIndex);
6792
+ cyclePath.push(cycleStart); // Close the cycle
6793
+ return cyclePath;
6794
+ }
6795
+ /**
6796
+ * Formats a human-readable error message for a detected cycle.
6797
+ *
6798
+ * @internal
6799
+ */
6800
+ function formatCycleError(cyclePath) {
6801
+ const pathStr = cyclePath.join(' -> ');
6802
+ return (`Derivation cycle detected: ${pathStr}\n` +
6803
+ `This would cause an infinite loop at runtime. ` +
6804
+ `Remove one of the derivations to break the cycle.`);
6805
+ }
6806
+ /**
6807
+ * Validates a derivation collection and throws if cycles are detected.
6808
+ *
6809
+ * This is the main entry point for cycle validation during form initialization.
6810
+ * Should be called after collecting derivations and before setting up effects.
6811
+ *
6812
+ * In dev mode, logs a warning when bidirectional derivation pairs are detected.
6813
+ * These patterns are allowed but may oscillate with floating-point values.
6814
+ *
6815
+ * @param collection - The collected derivation entries to validate
6816
+ * @param logger - Optional logger for dev-mode warnings
6817
+ * @throws Error if a cycle is detected, with details about the cycle
6818
+ *
6819
+ * @example
6820
+ * ```typescript
6821
+ * const collection = collectDerivations(fields);
6822
+ *
6823
+ * // This will throw if cycles exist
6824
+ * validateNoCycles(collection, logger);
6825
+ *
6826
+ * // Safe to set up derivation effects now
6827
+ * setupDerivationEffects(collection);
6828
+ * ```
6829
+ *
6830
+ * @public
6831
+ */
6832
+ function validateNoCycles(collection, logger) {
6833
+ const result = detectCycles(collection);
6834
+ if (result.hasCycle) {
6835
+ throw new Error(result.errorMessage);
6836
+ }
6837
+ // Warn about bidirectional patterns in dev mode
6838
+ if (isDevMode() && result.bidirectionalPairs && result.bidirectionalPairs.length > 0 && logger) {
6839
+ logger.warn('[Derivation] Bidirectional derivation patterns detected. ' +
6840
+ 'These patterns stabilize via equality checks, but may oscillate with floating-point values ' +
6841
+ '(e.g., currency conversions with rounding). ' +
6842
+ 'Consider adding tolerance-based comparisons for numeric values.', result.bidirectionalPairs);
6843
+ }
6844
+ }
6845
+
6846
+ /**
6847
+ * Centralized constants for the derivation system.
6848
+ *
6849
+ * These constants configure the behavior of derivation processing,
6850
+ * caching, and path handling. Centralizing them enables:
6851
+ * - Easy configuration changes
6852
+ * - Consistent values across all derivation modules
6853
+ * - Clear documentation of magic numbers
6854
+ *
6855
+ * @module
6856
+ */
6857
+ /**
6858
+ * Maximum number of derivation iterations before stopping to prevent infinite loops.
6859
+ *
6860
+ * The value of 10 is chosen based on:
6861
+ * - Most derivation chains are 2-3 levels deep (A→B→C)
6862
+ * - Complex forms with conditional cascades rarely exceed 5-6 levels
6863
+ * - 10 provides headroom for bidirectional sync patterns (A↔B) which may need
6864
+ * 2 iterations per pair to stabilize
6865
+ * - Higher values delay detection of actual infinite loops
6866
+ * - Lower values risk false positives on legitimate deep chains
6867
+ *
6868
+ * If you hit this limit legitimately, consider:
6869
+ * 1. Restructuring derivations to reduce chain depth
6870
+ * 2. Using explicit `dependsOn` to control evaluation order
6871
+ * 3. Breaking complex derivations into computed signals outside the form
6872
+ */
6873
+ const MAX_DERIVATION_ITERATIONS = 10;
6874
+ /**
6875
+ * Placeholder string for array index in derivation paths.
6876
+ *
6877
+ * Used in paths like `items.$.quantity` to represent "each item in the array".
6878
+ * The `$` is resolved to actual indices at runtime during array derivation processing.
6879
+ */
6880
+ const ARRAY_PLACEHOLDER = '.$.';
6881
+ /**
6882
+ * Maximum size of the expression AST cache.
6883
+ *
6884
+ * Expressions are parsed into Abstract Syntax Trees (AST) once and cached
6885
+ * to avoid re-parsing on each evaluation. The cache uses LRU eviction.
6886
+ *
6887
+ * 1000 entries handles:
6888
+ * - Large forms with many derivation expressions
6889
+ * - Multiple form instances with different expressions
6890
+ * - Memory efficiency (AST nodes are relatively small)
6891
+ */
6892
+ const MAX_AST_CACHE_SIZE = 1000;
6893
+ /**
6894
+ * Delimiter used to create unique derivation keys from source and target field keys.
6895
+ *
6896
+ * Uses null character (\x00) which is extremely unlikely to appear in field names,
6897
+ * avoiding collision issues that could occur with common delimiters like ':'.
6898
+ */
6899
+ const DERIVATION_KEY_DELIMITER = '\x00';
6900
+
6901
+ /**
6902
+ * Applies all pending derivations from a collection.
6903
+ *
6904
+ * This function processes derivations in order, evaluating conditions
6905
+ * and computing values. It handles loop prevention through:
6906
+ * 1. Chain tracking (prevents same derivation from running twice in one cycle)
6907
+ * 2. Value equality checks (skips if target already has computed value)
6908
+ * 3. Max iteration limit (safety fallback)
6909
+ *
6910
+ * @param collection - The collected derivation entries
6911
+ * @param context - Context for applying derivations
6912
+ * @param changedFields - Set of field keys that changed (for filtering)
6913
+ * @returns Result of the derivation processing
6914
+ *
6915
+ * @example
6916
+ * ```typescript
6917
+ * const result = applyDerivations(collection, {
6918
+ * formValue: formValueSignal,
6919
+ * rootForm: form(),
6920
+ * derivationFunctions: customFnConfig?.derivations,
6921
+ * logger: inject(DynamicFormLogger),
6922
+ * });
6923
+ *
6924
+ * if (result.maxIterationsReached) {
6925
+ * console.warn('Possible derivation loop detected');
6926
+ * }
6927
+ * ```
6928
+ *
6929
+ * @public
6930
+ */
6931
+ function applyDerivations(collection, context, changedFields) {
6932
+ const chainContext = createDerivationChainContext();
6933
+ const { derivationLogger } = context;
6934
+ const maxIterations = context.maxIterations ?? MAX_DERIVATION_ITERATIONS;
6935
+ let appliedCount = 0;
6936
+ let skippedCount = 0;
6937
+ let errorCount = 0;
6938
+ let warnCount = 0;
6939
+ // Filter entries based on changed fields
6940
+ const entriesToProcess = changedFields ? getEntriesForChangedFields(collection.entries, changedFields) : collection.entries;
6941
+ // Log cycle start in verbose mode
6942
+ derivationLogger.cycleStart('onChange', entriesToProcess.length);
6943
+ // Process derivations iteratively until no more changes
6944
+ let hasChanges = true;
6945
+ while (hasChanges && chainContext.iteration < maxIterations) {
6946
+ chainContext.iteration++;
6947
+ hasChanges = false;
6948
+ // Log iteration in verbose mode
6949
+ derivationLogger.iteration(chainContext.iteration);
6950
+ for (const entry of entriesToProcess) {
6951
+ const result = tryApplyDerivation(entry, context, chainContext);
6952
+ if (result.applied) {
6953
+ appliedCount++;
6954
+ hasChanges = true;
6955
+ }
6956
+ else if (result.error) {
6957
+ errorCount++;
6958
+ }
6959
+ else if (result.warning) {
6960
+ warnCount++;
6961
+ skippedCount++;
6962
+ }
6963
+ else {
6964
+ skippedCount++;
6965
+ }
6966
+ }
6967
+ }
6968
+ // Build result
6969
+ const processingResult = {
6970
+ appliedCount,
6971
+ skippedCount,
6972
+ errorCount,
6973
+ warnCount,
6974
+ iterations: chainContext.iteration,
6975
+ maxIterationsReached: chainContext.iteration >= maxIterations,
6976
+ };
6977
+ if (processingResult.maxIterationsReached) {
6978
+ derivationLogger.maxIterationsReached(processingResult, 'onChange');
6979
+ }
6980
+ // Log summary
6981
+ derivationLogger.summary(processingResult, 'onChange');
6982
+ return processingResult;
6983
+ }
6984
+ /**
6985
+ * Gets entries that should be processed based on changed fields.
6986
+ *
6987
+ * Filters entries by checking if any of their dependencies are in the changed fields set.
6988
+ * Also includes all wildcard (*) entries since they depend on any form change.
6989
+ *
6990
+ * Note: For optimal O(k) performance where k = changed fields count, use
6991
+ * DerivationLookup.getEntriesForChangedFields() which uses pre-built indexes.
6992
+ *
6993
+ * @internal
6994
+ */
6995
+ function getEntriesForChangedFields(entries, changedFields) {
6996
+ return entries.filter((entry) => {
6997
+ // Wildcard entries are always included
6998
+ if (entry.dependsOn.includes('*')) {
6999
+ return true;
7000
+ }
7001
+ // Check if any dependency is in changed fields
7002
+ return entry.dependsOn.some((dep) => changedFields.has(dep));
7003
+ });
7004
+ }
7005
+ /**
7006
+ * Attempts to apply a single derivation.
7007
+ *
7008
+ * **Important:** When a derivation's condition evaluates to `false`, the previously
7009
+ * derived value is NOT automatically cleared. If you need to reset a field when a
7010
+ * condition becomes false, use a separate derivation with an inverted condition
7011
+ * that sets the desired fallback value.
7012
+ *
7013
+ * @example
7014
+ * ```typescript
7015
+ * // Two derivations for conditional value with cleanup:
7016
+ * // When country is USA, set phonePrefix to '+1'
7017
+ * { key: 'phonePrefix', logic: [{ type: 'derivation', condition: { field: 'country', operator: '==', value: 'USA' }, value: '+1' }] }
7018
+ * // When country is NOT USA, clear phonePrefix
7019
+ * { key: 'phonePrefix', logic: [{ type: 'derivation', condition: { field: 'country', operator: '!=', value: 'USA' }, value: '' }] }
7020
+ * ```
7021
+ *
7022
+ * @internal
7023
+ */
7024
+ function tryApplyDerivation(entry, context, chainContext) {
7025
+ const { derivationLogger } = context;
7026
+ // Check if this is an array field derivation (has '$' placeholder)
7027
+ if (isArrayPlaceholderPath(entry.fieldKey)) {
7028
+ return tryApplyArrayDerivation(entry, context, chainContext);
7029
+ }
7030
+ const derivationKey = createDerivationKey(entry.fieldKey);
7031
+ // Check if already applied in this cycle
7032
+ if (chainContext.appliedDerivations.has(derivationKey)) {
7033
+ derivationLogger.evaluation({
7034
+ debugName: entry.debugName,
7035
+ fieldKey: entry.fieldKey,
7036
+ result: 'skipped',
7037
+ skipReason: 'already-applied',
7038
+ });
7039
+ return { applied: false, fieldKey: entry.fieldKey };
7040
+ }
7041
+ // Create evaluation context
7042
+ const formValue = untracked(() => context.formValue());
7043
+ const evalContext = createEvaluationContext(entry, formValue, context);
7044
+ // Evaluate condition
7045
+ if (!evaluateDerivationCondition(entry.condition, evalContext)) {
7046
+ derivationLogger.evaluation({
7047
+ debugName: entry.debugName,
7048
+ fieldKey: entry.fieldKey,
7049
+ result: 'skipped',
7050
+ skipReason: 'condition-false',
7051
+ });
7052
+ return { applied: false, fieldKey: entry.fieldKey };
7053
+ }
7054
+ // Compute derived value
7055
+ let newValue;
7056
+ try {
7057
+ newValue = computeDerivedValue(entry, evalContext, context);
7058
+ }
7059
+ catch (error) {
7060
+ const errorMessage = error instanceof Error ? error.message : String(error);
7061
+ context.logger.error(formatDerivationError(entry, 'compute', errorMessage));
7062
+ derivationLogger.evaluation({
7063
+ debugName: entry.debugName,
7064
+ fieldKey: entry.fieldKey,
7065
+ result: 'error',
7066
+ error: errorMessage,
7067
+ });
7068
+ return {
7069
+ applied: false,
7070
+ fieldKey: entry.fieldKey,
7071
+ error: errorMessage,
7072
+ };
7073
+ }
7074
+ // Check if value actually changed using exact equality.
7075
+ // Note: This uses isEqual which performs deep comparison with exact IEEE 754
7076
+ // equality for numbers. Bidirectional derivations with floating-point math
7077
+ // may oscillate due to rounding errors. Use explicit rounding in expressions
7078
+ // or integer arithmetic to avoid this issue.
7079
+ const currentValue = getNestedValue(formValue, entry.fieldKey);
7080
+ if (isEqual(currentValue, newValue)) {
7081
+ derivationLogger.evaluation({
7082
+ debugName: entry.debugName,
7083
+ fieldKey: entry.fieldKey,
7084
+ result: 'skipped',
7085
+ skipReason: 'value-unchanged',
7086
+ });
7087
+ return { applied: false, fieldKey: entry.fieldKey };
7088
+ }
7089
+ // Apply the value
7090
+ try {
7091
+ const wasApplied = applyValueToForm(entry.fieldKey, newValue, context.rootForm, context.logger, context.warningTracker);
7092
+ if (wasApplied) {
7093
+ chainContext.appliedDerivations.add(derivationKey);
7094
+ derivationLogger.evaluation({
7095
+ debugName: entry.debugName,
7096
+ fieldKey: entry.fieldKey,
7097
+ result: 'applied',
7098
+ previousValue: currentValue,
7099
+ newValue,
7100
+ });
7101
+ return { applied: true, fieldKey: entry.fieldKey, newValue };
7102
+ }
7103
+ else {
7104
+ // Field not found - this is a warning, not an error
7105
+ // The warning was already logged by warnMissingField
7106
+ derivationLogger.evaluation({
7107
+ debugName: entry.debugName,
7108
+ fieldKey: entry.fieldKey,
7109
+ result: 'skipped',
7110
+ skipReason: 'target-not-found',
7111
+ });
7112
+ return {
7113
+ applied: false,
7114
+ fieldKey: entry.fieldKey,
7115
+ warning: `Field '${entry.fieldKey}' not found`,
7116
+ };
7117
+ }
7118
+ }
7119
+ catch (error) {
7120
+ const errorMessage = error instanceof Error ? error.message : String(error);
7121
+ context.logger.error(formatDerivationError(entry, 'apply', errorMessage));
7122
+ derivationLogger.evaluation({
7123
+ debugName: entry.debugName,
7124
+ fieldKey: entry.fieldKey,
7125
+ result: 'error',
7126
+ error: errorMessage,
7127
+ });
7128
+ return {
7129
+ applied: false,
7130
+ fieldKey: entry.fieldKey,
7131
+ error: errorMessage,
7132
+ };
7133
+ }
7134
+ }
7135
+ /**
7136
+ * Handles array field derivations with '$' placeholder.
7137
+ *
7138
+ * Iterates over all array items and applies the derivation for each,
7139
+ * resolving '$' to the actual index and creating scoped evaluation contexts.
7140
+ *
7141
+ * @internal
7142
+ */
7143
+ function tryApplyArrayDerivation(entry, context, chainContext) {
7144
+ const formValue = untracked(() => context.formValue());
7145
+ // Parse the field path using path utilities
7146
+ const pathInfo = parseArrayPath(entry.fieldKey);
7147
+ if (!pathInfo.isArrayPath) {
7148
+ return { applied: false, fieldKey: entry.fieldKey, error: 'Invalid array derivation path' };
7149
+ }
7150
+ const { arrayPath } = pathInfo;
7151
+ // Get the array from form values
7152
+ const arrayValue = getNestedValue(formValue, arrayPath);
7153
+ if (!Array.isArray(arrayValue)) {
7154
+ return { applied: false, fieldKey: entry.fieldKey };
7155
+ }
7156
+ let appliedAny = false;
7157
+ // Process each array item
7158
+ for (let i = 0; i < arrayValue.length; i++) {
7159
+ const resolvedPath = resolveArrayPath(entry.fieldKey, i);
7160
+ const derivationKey = createDerivationKey(resolvedPath);
7161
+ // Skip if already applied in this cycle
7162
+ if (chainContext.appliedDerivations.has(derivationKey)) {
7163
+ continue;
7164
+ }
7165
+ // Create evaluation context scoped to this array item
7166
+ const arrayItem = arrayValue[i];
7167
+ const evalContext = createArrayItemEvaluationContext(entry, arrayItem, formValue, i, arrayPath, context);
7168
+ // Evaluate condition
7169
+ if (!evaluateDerivationCondition(entry.condition, evalContext)) {
7170
+ continue;
7171
+ }
7172
+ // Compute derived value
7173
+ let newValue;
7174
+ try {
7175
+ newValue = computeDerivedValue(entry, evalContext, context);
7176
+ }
7177
+ catch (error) {
7178
+ const errorMessage = error instanceof Error ? error.message : String(error);
7179
+ context.logger.error(formatDerivationError(entry, 'compute', `[index ${i}] ${errorMessage}`));
7180
+ continue;
7181
+ }
7182
+ // Check if value actually changed
7183
+ const currentValue = getNestedValue(formValue, resolvedPath);
7184
+ if (isEqual(currentValue, newValue)) {
7185
+ continue;
7186
+ }
7187
+ // Apply the value
7188
+ try {
7189
+ applyValueToForm(resolvedPath, newValue, context.rootForm, context.logger, context.warningTracker);
7190
+ chainContext.appliedDerivations.add(derivationKey);
7191
+ appliedAny = true;
7192
+ }
7193
+ catch (error) {
7194
+ const errorMessage = error instanceof Error ? error.message : String(error);
7195
+ context.logger.error(formatDerivationError(entry, 'apply', `[index ${i}] ${errorMessage}`));
7196
+ }
7197
+ }
7198
+ return { applied: appliedAny, fieldKey: entry.fieldKey };
7199
+ }
7200
+ /**
7201
+ * Creates an evaluation context for derivation processing.
7202
+ *
7203
+ * @internal
7204
+ */
7205
+ function createEvaluationContext(entry, formValue, context) {
7206
+ const fieldValue = getNestedValue(formValue, entry.fieldKey);
7207
+ return {
7208
+ fieldValue,
7209
+ formValue,
7210
+ fieldPath: entry.fieldKey,
7211
+ customFunctions: context.customFunctions,
7212
+ externalData: context.externalData,
7213
+ logger: context.logger,
7214
+ };
7215
+ }
7216
+ /**
7217
+ * Creates an evaluation context scoped to a specific array item.
7218
+ *
7219
+ * For array derivations, `formValue` in the expression should reference
7220
+ * the current array item's values, not the root form values.
7221
+ *
7222
+ * @internal
7223
+ */
7224
+ function createArrayItemEvaluationContext(entry, arrayItem, rootFormValue, itemIndex, arrayPath, context) {
7225
+ // For array item expressions, formValue should be the array item
7226
+ // This allows expressions like 'formValue.quantity * formValue.unitPrice'
7227
+ // to work within the context of each array item
7228
+ return {
7229
+ fieldValue: arrayItem,
7230
+ formValue: arrayItem,
7231
+ fieldPath: `${arrayPath}.${itemIndex}`,
7232
+ customFunctions: context.customFunctions,
7233
+ externalData: context.externalData,
7234
+ logger: context.logger,
7235
+ // Provide access to root form value for cross-scope references
7236
+ rootFormValue,
7237
+ arrayIndex: itemIndex,
7238
+ arrayPath,
7239
+ };
7240
+ }
7241
+ /**
7242
+ * Evaluates the derivation condition.
7243
+ *
7244
+ * @internal
7245
+ */
7246
+ function evaluateDerivationCondition(condition, context) {
7247
+ if (typeof condition === 'boolean') {
7248
+ return condition;
7249
+ }
7250
+ return evaluateCondition(condition, context);
7251
+ }
7252
+ /**
7253
+ * Computes the derived value based on the entry configuration.
7254
+ *
7255
+ * @internal
7256
+ */
7257
+ function computeDerivedValue(entry, evalContext, applicatorContext) {
7258
+ // Static value
7259
+ if (entry.value !== undefined) {
7260
+ return entry.value;
7261
+ }
7262
+ // Expression
7263
+ if (entry.expression) {
7264
+ return ExpressionParser.evaluate(entry.expression, evalContext);
7265
+ }
7266
+ // Custom function
7267
+ if (entry.functionName) {
7268
+ const fn = applicatorContext.derivationFunctions?.[entry.functionName];
7269
+ if (!fn) {
7270
+ throw new Error(`Derivation function '${entry.functionName}' not found in customFnConfig.derivations`);
7271
+ }
7272
+ return fn(evalContext);
7273
+ }
7274
+ // No value source specified
7275
+ throw new Error(`Derivation for ${entry.fieldKey} has no value source. ` + `Specify 'value', 'expression', or 'functionName'.`);
7276
+ }
7277
+ /**
7278
+ * Applies a value to the form at the specified path.
7279
+ *
7280
+ * Uses bracket notation to access child FieldTrees, following the same pattern
7281
+ * as group-field and array-field components. Fields are accessed as signals
7282
+ * and their value is set via the `.value.set()` method.
7283
+ *
7284
+ * Handles nested paths and array paths with '$' placeholder.
7285
+ *
7286
+ * @internal
7287
+ */
7288
+ function applyValueToForm(targetPath, value, rootForm, logger, warningTracker) {
7289
+ // Handle simple top-level fields
7290
+ if (!targetPath.includes('.')) {
7291
+ return setFieldValue(rootForm, targetPath, value, logger, warningTracker);
7292
+ }
7293
+ // Handle nested paths (e.g., 'address.city' or 'items.$.quantity')
7294
+ const parts = targetPath.split('.');
7295
+ let current = rootForm;
7296
+ for (let i = 0; i < parts.length - 1; i++) {
7297
+ const part = parts[i];
7298
+ // Handle array index placeholder '$'
7299
+ if (part === '$') {
7300
+ // This case should be resolved at collection time
7301
+ // If we still have '$', we need to skip for now
7302
+ return false;
7303
+ }
7304
+ // Navigate to the next level using bracket notation
7305
+ const next = current[part];
7306
+ if (next === undefined || next === null) {
7307
+ warnMissingField(targetPath, logger, warningTracker);
7308
+ return false; // Path doesn't exist
7309
+ }
7310
+ current = next;
7311
+ }
7312
+ // Set the final value
7313
+ const finalPart = parts[parts.length - 1];
7314
+ return setFieldValue(current, finalPart, value, logger, warningTracker);
7315
+ }
7316
+ /**
7317
+ * Error message prefix for derivation-related errors.
7318
+ * @internal
7319
+ */
7320
+ const ERROR_PREFIX = '[Derivation]';
7321
+ /**
7322
+ * Creates a standardized error message for derivation errors.
7323
+ * @internal
7324
+ */
7325
+ function formatDerivationError(entry, phase, message) {
7326
+ const source = entry.expression
7327
+ ? `expression: "${entry.expression.substring(0, 50)}${entry.expression.length > 50 ? '...' : ''}"`
7328
+ : entry.functionName
7329
+ ? `function: "${entry.functionName}"`
7330
+ : entry.value !== undefined
7331
+ ? 'static value'
7332
+ : 'unknown source';
7333
+ return `${ERROR_PREFIX} Failed to ${phase} derivation (field: ${entry.fieldKey}, ${source}): ${message}`;
7334
+ }
7335
+ /**
7336
+ * Sets a field value using the Angular Signal Forms pattern.
7337
+ *
7338
+ * Accesses the child field via bracket notation, drills down to find
7339
+ * the WritableSignal, and uses isWritableSignal for type-safe signal detection.
7340
+ *
7341
+ * Angular Signal Forms structure:
7342
+ * - form[key] is a callable function (field accessor)
7343
+ * - form[key]() returns the field instance with { value: WritableSignal<T> }
7344
+ * - form[key]().value.set(newValue) sets the value
7345
+ *
7346
+ * @returns true if the value was successfully set, false if field not found
7347
+ *
7348
+ * @internal
7349
+ */
7350
+ function setFieldValue(parent, fieldKey, value, logger, warningTracker) {
7351
+ // Access child field via bracket notation (same pattern as group-field/array-field)
7352
+ const fieldAccessor = parent[fieldKey];
7353
+ if (fieldAccessor === undefined || fieldAccessor === null) {
7354
+ warnMissingField(fieldKey, logger, warningTracker);
7355
+ return false;
7356
+ }
7357
+ // Angular Signal Forms: field accessor is a callable function
7358
+ if (typeof fieldAccessor !== 'function') {
7359
+ warnMissingField(fieldKey, logger, warningTracker);
7360
+ return false;
7361
+ }
7362
+ // Call the accessor to get the field instance
7363
+ const fieldInstance = fieldAccessor();
7364
+ if (!fieldInstance || typeof fieldInstance !== 'object' || !('value' in fieldInstance)) {
7365
+ warnMissingField(fieldKey, logger, warningTracker);
7366
+ return false;
7367
+ }
7368
+ // Get the value signal and verify it's a WritableSignal
7369
+ const valueSignal = fieldInstance.value;
7370
+ if (isWritableSignal(valueSignal)) {
7371
+ valueSignal.set(value);
7372
+ return true;
7373
+ }
7374
+ else {
7375
+ warnMissingField(fieldKey, logger, warningTracker);
7376
+ return false;
7377
+ }
7378
+ }
7379
+ /**
7380
+ * Logs a warning for a missing target field (once per field per form instance).
7381
+ *
7382
+ * Uses the provided warning tracker to avoid log spam. If no tracker is provided,
7383
+ * the warning will be logged every time.
7384
+ *
7385
+ * @internal
7386
+ */
7387
+ function warnMissingField(fieldKey, logger, warningTracker) {
7388
+ // If tracker is provided, check if we've already warned about this field
7389
+ if (warningTracker) {
7390
+ if (warningTracker.warnedFields.has(fieldKey)) {
7391
+ return;
7392
+ }
7393
+ warningTracker.warnedFields.add(fieldKey);
7394
+ }
7395
+ logger?.warn(`${ERROR_PREFIX} Target field '${fieldKey}' not found in form. ` +
7396
+ `Ensure the field is defined in your form configuration. ` +
7397
+ `This warning is shown once per field.`);
7398
+ }
7399
+ /**
7400
+ * Processes derivations for a specific trigger type.
7401
+ *
7402
+ * Filters entries by trigger type and applies them.
7403
+ *
7404
+ * @param collection - The collected derivation entries
7405
+ * @param trigger - The trigger type to filter by
7406
+ * @param context - Context for applying derivations
7407
+ * @param changedFields - Set of field keys that changed (for filtering)
7408
+ * @returns Result of the derivation processing
7409
+ *
7410
+ * @public
7411
+ */
7412
+ function applyDerivationsForTrigger(collection, trigger, context, changedFields) {
7413
+ // Filter entries by trigger type
7414
+ const filteredEntries = collection.entries.filter((entry) => {
7415
+ if (trigger === 'onChange') {
7416
+ return !entry.trigger || entry.trigger === 'onChange';
7417
+ }
7418
+ return entry.trigger === trigger;
7419
+ });
7420
+ // Create a minimal collection with filtered entries
7421
+ const filteredCollection = {
7422
+ entries: filteredEntries,
7423
+ };
7424
+ return applyDerivations(filteredCollection, context, changedFields);
7425
+ }
7426
+ /**
7427
+ * Gets all debounced derivation entries from a collection.
7428
+ *
7429
+ * Use this to extract debounced entries for separate processing with debounce timers.
7430
+ *
7431
+ * @param collection - The collected derivation entries
7432
+ * @returns Array of debounced derivation entries
7433
+ *
7434
+ * @public
7435
+ */
7436
+ function getDebouncedDerivationEntries(collection) {
7437
+ return collection.entries.filter((entry) => entry.trigger === 'debounced');
7438
+ }
7439
+
7440
+ /**
7441
+ * Injection token for the derivation warning tracker.
7442
+ * Provided at form component level for instance-scoped tracking.
7443
+ *
7444
+ * @public
7445
+ */
7446
+ const DERIVATION_WARNING_TRACKER = new InjectionToken('DerivationWarningTracker', {
7447
+ providedIn: null,
7448
+ factory: () => ({ warnedFields: new Set() }),
7449
+ });
7450
+ /**
7451
+ * Creates a fresh warning tracker instance.
7452
+ *
7453
+ * @returns A new DerivationWarningTracker with an empty warnedFields Set
7454
+ *
7455
+ * @public
7456
+ */
7457
+ function createDerivationWarningTracker() {
7458
+ return { warnedFields: new Set() };
7459
+ }
7460
+
7461
+ /**
7462
+ * Prefix for derivation debug log messages.
7463
+ * @internal
7464
+ */
7465
+ const LOG_PREFIX = '[Derivation]';
7466
+ /**
7467
+ * Logs a summary of derivation processing results.
7468
+ *
7469
+ * Only logs when:
7470
+ * - Config level is 'summary' or 'verbose'
7471
+ * - At least one derivation was applied or an error occurred
7472
+ *
7473
+ * @param result - The derivation processing result
7474
+ * @param trigger - The trigger type ('onChange' or 'debounced')
7475
+ * @param logger - Logger instance
7476
+ * @param config - Debug logging configuration
7477
+ *
7478
+ * @public
7479
+ */
7480
+ function logDerivationSummary(result, trigger, logger, config) {
7481
+ if (!shouldLog(config, 'summary'))
7482
+ return;
7483
+ // Only log if something interesting happened
7484
+ if (result.appliedCount === 0 && result.errorCount === 0)
7485
+ return;
7486
+ logger.debug(`${LOG_PREFIX} Cycle complete (${trigger})`, {
7487
+ applied: result.appliedCount,
7488
+ skipped: result.skippedCount,
7489
+ errors: result.errorCount,
7490
+ iterations: result.iterations,
7491
+ });
7492
+ }
7493
+ /**
7494
+ * Logs when max iterations are reached (always logged as warning).
7495
+ *
7496
+ * @param result - The derivation processing result
7497
+ * @param trigger - The trigger type
7498
+ * @param logger - Logger instance
7499
+ *
7500
+ * @public
7501
+ */
7502
+ function logMaxIterationsReached(result, trigger, logger) {
7503
+ logger.warn(`${LOG_PREFIX} Processing reached max iterations (${trigger}). ` +
7504
+ `This may indicate a loop in derivation logic. ` +
7505
+ `Applied: ${result.appliedCount}, Skipped: ${result.skippedCount}, Errors: ${result.errorCount}`);
7506
+ }
7507
+ /**
7508
+ * Logs a verbose entry for a single derivation evaluation.
7509
+ *
7510
+ * Only logs when config level is 'verbose'.
7511
+ *
7512
+ * @param entry - The derivation log entry with evaluation details
7513
+ * @param logger - Logger instance
7514
+ * @param config - Debug logging configuration
7515
+ *
7516
+ * @internal
7517
+ */
7518
+ function logDerivationEvaluation(entry, logger, config) {
7519
+ if (!shouldLog(config, 'verbose'))
7520
+ return;
7521
+ const name = entry.debugName ? `"${entry.debugName}"` : entry.fieldKey;
7522
+ switch (entry.result) {
7523
+ case 'applied':
7524
+ logger.debug(`${LOG_PREFIX} -> Applied: ${name}`, {
7525
+ field: entry.fieldKey,
7526
+ previousValue: entry.previousValue,
7527
+ newValue: entry.newValue,
7528
+ ...(entry.durationMs !== undefined && { durationMs: entry.durationMs }),
7529
+ });
7530
+ break;
7531
+ case 'skipped':
7532
+ logger.debug(`${LOG_PREFIX} -> Skipped: ${name} (${formatSkipReason(entry.skipReason)})`, {
7533
+ field: entry.fieldKey,
7534
+ reason: entry.skipReason,
7535
+ });
7536
+ break;
7537
+ case 'error':
7538
+ logger.debug(`${LOG_PREFIX} -> Error: ${name}`, {
7539
+ field: entry.fieldKey,
7540
+ error: entry.error,
7541
+ });
7542
+ break;
7543
+ }
7544
+ }
7545
+ /**
7546
+ * Formats the skip reason for human-readable output.
7547
+ *
7548
+ * @internal
7549
+ */
7550
+ function formatSkipReason(reason) {
7551
+ switch (reason) {
7552
+ case 'condition-false':
7553
+ return 'condition not met';
7554
+ case 'value-unchanged':
7555
+ return 'value unchanged';
7556
+ case 'already-applied':
7557
+ return 'already applied this cycle';
7558
+ default:
7559
+ return 'unknown';
7560
+ }
7561
+ }
7562
+ /**
7563
+ * Logs the start of a derivation processing cycle.
7564
+ *
7565
+ * Only logs when config level is 'verbose'.
7566
+ *
7567
+ * @param trigger - The trigger type ('onChange' or 'debounced')
7568
+ * @param entryCount - Number of derivations to process in this cycle
7569
+ * @param logger - Logger instance
7570
+ * @param config - Debug logging configuration
7571
+ *
7572
+ * @internal
7573
+ */
7574
+ function logDerivationCycleStart(trigger, entryCount, logger, config) {
7575
+ if (!shouldLog(config, 'verbose'))
7576
+ return;
7577
+ logger.debug(`${LOG_PREFIX} Starting cycle (${trigger}) with ${entryCount} derivation(s)`);
7578
+ }
7579
+ /**
7580
+ * Logs the start of a derivation iteration within a cycle.
7581
+ *
7582
+ * Only logs when config level is 'verbose'.
7583
+ *
7584
+ * @param iteration - Current iteration number (1-based)
7585
+ * @param logger - Logger instance
7586
+ * @param config - Debug logging configuration
7587
+ *
7588
+ * @internal
7589
+ */
7590
+ function logDerivationIteration(iteration, logger, config) {
7591
+ if (!shouldLog(config, 'verbose'))
7592
+ return;
7593
+ logger.debug(`${LOG_PREFIX} Iteration ${iteration}`);
7594
+ }
7595
+
7596
+ /**
7597
+ * Active implementation of DerivationLogger that performs actual logging.
7598
+ *
7599
+ * @internal
7600
+ */
7601
+ class ActiveDerivationLogger {
7602
+ logger;
7603
+ config = createDefaultDerivationLogConfig();
7604
+ /**
7605
+ * Updates the log configuration.
7606
+ */
7607
+ setConfig(config) {
7608
+ this.config = config;
7609
+ }
7610
+ cycleStart(trigger, entryCount) {
7611
+ if (!shouldLog(this.config, 'verbose'))
7612
+ return;
7613
+ this.logger.debug(`Derivation - Starting cycle (${trigger}) with ${entryCount} derivation(s)`);
7614
+ }
7615
+ iteration(iterationNumber) {
7616
+ if (!shouldLog(this.config, 'verbose'))
7617
+ return;
7618
+ this.logger.debug(`Derivation - Iteration ${iterationNumber}`);
7619
+ }
7620
+ evaluation(entry) {
7621
+ if (!shouldLog(this.config, 'verbose'))
7622
+ return;
7623
+ const name = entry.debugName ? `"${entry.debugName}"` : entry.fieldKey;
7624
+ switch (entry.result) {
7625
+ case 'applied':
7626
+ this.logger.debug(`Derivation - Applied ${name}`, {
7627
+ field: entry.fieldKey,
7628
+ previousValue: entry.previousValue,
7629
+ newValue: entry.newValue,
7630
+ ...(entry.durationMs !== undefined && { durationMs: entry.durationMs }),
7631
+ });
7632
+ break;
7633
+ case 'skipped':
7634
+ this.logger.debug(`Derivation - Skipped ${name} (${this.formatSkipReason(entry.skipReason)})`, {
7635
+ field: entry.fieldKey,
7636
+ reason: entry.skipReason,
7637
+ });
7638
+ break;
7639
+ case 'error':
7640
+ this.logger.debug(`Derivation - Error ${name}`, {
7641
+ field: entry.fieldKey,
7642
+ error: entry.error,
7643
+ });
7644
+ break;
7645
+ }
7646
+ }
7647
+ summary(result, trigger) {
7648
+ if (!shouldLog(this.config, 'summary'))
7649
+ return;
7650
+ // Only log if something interesting happened
7651
+ if (result.appliedCount === 0 && result.errorCount === 0)
7652
+ return;
7653
+ this.logger.debug(`Derivation - Cycle complete (${trigger})`, {
7654
+ applied: result.appliedCount,
7655
+ skipped: result.skippedCount,
7656
+ errors: result.errorCount,
7657
+ iterations: result.iterations,
7658
+ });
7659
+ }
7660
+ maxIterationsReached(result, trigger) {
7661
+ this.logger.warn(`Derivation - Max iterations reached (${trigger}). ` +
7662
+ `This may indicate a loop in derivation logic. ` +
7663
+ `Applied: ${result.appliedCount}, Skipped: ${result.skippedCount}, Errors: ${result.errorCount}`);
7664
+ }
7665
+ formatSkipReason(reason) {
7666
+ switch (reason) {
7667
+ case 'condition-false':
7668
+ return 'condition not met';
7669
+ case 'value-unchanged':
7670
+ return 'value unchanged';
7671
+ case 'already-applied':
7672
+ return 'already applied this cycle';
7673
+ default:
7674
+ return 'unknown';
7675
+ }
7676
+ }
7677
+ }
7678
+ /**
7679
+ * No-op implementation of DerivationLogger.
7680
+ * All methods are empty - no logging overhead when disabled.
7681
+ *
7682
+ * @internal
7683
+ */
7684
+ class NoopDerivationLogger {
7685
+ cycleStart() {
7686
+ /* no-op */
7687
+ }
7688
+ iteration() {
7689
+ /* no-op */
7690
+ }
7691
+ evaluation() {
7692
+ /* no-op */
7693
+ }
7694
+ summary() {
7695
+ /* no-op */
7696
+ }
7697
+ maxIterationsReached() {
7698
+ /* no-op */
7699
+ }
7700
+ }
7701
+ /** Singleton no-op instance to avoid creating new objects */
7702
+ const NOOP_INSTANCE = new NoopDerivationLogger();
7703
+ /**
7704
+ * Injection token for the derivation logger service.
7705
+ *
7706
+ * @public
7707
+ */
7708
+ const DERIVATION_LOGGER = new InjectionToken('DerivationLogger');
7709
+ /**
7710
+ * Factory function to create a DerivationLogger.
7711
+ *
7712
+ * Returns a no-op implementation if config level is 'none',
7713
+ * otherwise returns an active logger with the given config.
7714
+ *
7715
+ * @param config - The derivation log configuration
7716
+ * @param logger - The underlying logger instance
7717
+ * @returns A DerivationLogger instance
7718
+ *
7719
+ * @public
7720
+ */
7721
+ function createDerivationLogger(config, logger) {
7722
+ const effectiveConfig = config ?? createDefaultDerivationLogConfig();
7723
+ // Return no-op if logging is completely disabled
7724
+ if (effectiveConfig.level === 'none') {
7725
+ return NOOP_INSTANCE;
7726
+ }
7727
+ // Create active logger with config
7728
+ const service = new ActiveDerivationLogger();
7729
+ service.logger = logger;
7730
+ service.setConfig(effectiveConfig);
7731
+ return service;
7732
+ }
7733
+
7734
+ /**
7735
+ * Orchestrates derivation processing for a dynamic form.
7736
+ *
7737
+ * Uses RxJS streams for reactive derivation processing:
7738
+ * - `exhaustMap` prevents re-entry during onChange derivation application
7739
+ * - `pairwise` tracks value changes without mutable state
7740
+ * - `takeUntilDestroyed` handles automatic cleanup
7741
+ *
7742
+ * Injects common services directly, requiring only form-specific signals in config.
7743
+ *
7744
+ * @public
7745
+ */
7746
+ class DerivationOrchestrator {
7747
+ config;
7748
+ injector = inject(Injector);
7749
+ destroyRef = inject(DestroyRef);
7750
+ logger = inject(DynamicFormLogger);
7751
+ warningTracker = inject(DERIVATION_WARNING_TRACKER);
7752
+ functionRegistry = inject(FunctionRegistryService);
7753
+ formOptions = inject(FORM_OPTIONS);
7754
+ externalDataSignal = inject(EXTERNAL_DATA, { optional: true });
7755
+ /**
7756
+ * Computed signal containing the collected and validated derivations.
7757
+ * Returns null if no derivations are defined.
7758
+ */
7759
+ derivationCollection;
7760
+ constructor(config) {
7761
+ this.config = config;
7762
+ this.derivationCollection = computed(() => {
7763
+ const fields = config.schemaFields();
7764
+ if (!fields || fields.length === 0) {
7765
+ return null;
7766
+ }
7767
+ const collection = collectDerivations(fields);
7768
+ if (collection.entries.length === 0) {
7769
+ return null;
7770
+ }
7771
+ validateNoCycles(collection, this.logger);
7772
+ this.warnAboutWildcardDependencies(collection.entries, fields.length);
7773
+ return collection;
7774
+ }, ...(ngDevMode ? [{ debugName: "derivationCollection" }] : []));
7775
+ this.setupOnChangeStream();
7776
+ this.setupDebouncedStream();
7777
+ }
7778
+ setupOnChangeStream() {
7779
+ const collection$ = toObservable(this.derivationCollection, { injector: this.injector });
7780
+ const formValue$ = toObservable(this.config.formValue, { injector: this.injector });
7781
+ const form$ = toObservable(this.config.form, { injector: this.injector });
7782
+ collection$
7783
+ .pipe(filter((collection) => collection !== null), combineLatestWith(formValue$, form$),
7784
+ // auditTime(0): Batch synchronous emissions from Angular's change detection.
7785
+ // When a single user action triggers multiple signal updates, this ensures
7786
+ // we only process derivations once after all updates complete (microtask timing).
7787
+ auditTime(0),
7788
+ // exhaustMap: Prevents re-entry while processing derivations.
7789
+ // If form value changes DURING derivation processing (from our own setValue calls),
7790
+ // we ignore those emissions and complete the current cycle first.
7791
+ // switchMap would cancel mid-processing, causing incomplete derivation chains.
7792
+ exhaustMap(([collection, , formAccessor]) => {
7793
+ this.applyOnChangeDerivations(collection, formAccessor);
7794
+ // scheduled with queueScheduler: Ensures the observable completes
7795
+ // in the next microtask, allowing exhaustMap to accept new emissions.
7796
+ // Without this, exhaustMap would block indefinitely.
7797
+ return scheduled([null], queueScheduler);
7798
+ }), takeUntilDestroyed(this.destroyRef))
7799
+ .subscribe({
7800
+ error: (err) => this.logger.error('Derivation onChange stream error', err),
7801
+ });
7802
+ }
7803
+ setupDebouncedStream() {
7804
+ toObservable(this.config.formValue, { injector: this.injector })
7805
+ .pipe(
7806
+ // debounceTime: Wait for value to stabilize before detecting changes.
7807
+ // Uses DEFAULT_DEBOUNCE_MS as the minimum debounce period.
7808
+ debounceTime(DEFAULT_DEBOUNCE_MS),
7809
+ // startWith + pairwise: Track previous and current values to detect changes.
7810
+ // startWith(null) ensures pairwise has an initial value to pair with.
7811
+ startWith(null), pairwise(), filter((pair) => pair[1] !== null), map(([previous, current]) => ({
7812
+ current,
7813
+ changedFields: getChangedKeys(previous, current),
7814
+ })), filter(({ changedFields }) => changedFields.size > 0),
7815
+ // switchMap: For debounced derivations, it's OK to cancel pending work
7816
+ // if new changes come in - we want the latest debounced values.
7817
+ // (Unlike onChange which uses exhaustMap to prevent cancellation)
7818
+ switchMap(({ changedFields }) => {
7819
+ const collection = untracked(() => this.derivationCollection());
7820
+ const formAccessor = untracked(() => this.config.form());
7821
+ if (!collection || !formAccessor)
7822
+ return of(null);
7823
+ // Get unique debounce periods from entries with trigger 'debounced'
7824
+ const debouncePeriods = this.getDebouncePeriods(collection.entries);
7825
+ if (debouncePeriods.length === 0)
7826
+ return of(null);
7827
+ // merge: Process multiple debounce periods concurrently.
7828
+ // Each period stream handles its own timing independently.
7829
+ const periodStreams = debouncePeriods.map((debounceMs) => this.createPeriodStream(debounceMs, collection, formAccessor, changedFields));
7830
+ return merge(...periodStreams);
7831
+ }), takeUntilDestroyed(this.destroyRef))
7832
+ .subscribe({
7833
+ error: (err) => this.logger.error('Derivation debounced stream error', err),
7834
+ });
7835
+ }
7836
+ createPeriodStream(debounceMs, collection, formAccessor, changedFields) {
7837
+ const additionalWait = Math.max(0, debounceMs - DEFAULT_DEBOUNCE_MS);
7838
+ if (additionalWait === 0) {
7839
+ return of(null).pipe(map(() => {
7840
+ this.applyDebouncedEntriesForPeriod(debounceMs, collection, formAccessor, changedFields);
7841
+ }));
7842
+ }
7843
+ return timer(additionalWait).pipe(map(() => {
7844
+ const currentCollection = untracked(() => this.derivationCollection());
7845
+ const currentFormAccessor = untracked(() => this.config.form());
7846
+ if (currentCollection && currentFormAccessor) {
7847
+ this.applyDebouncedEntriesForPeriod(debounceMs, currentCollection, currentFormAccessor, changedFields);
7848
+ }
7849
+ }));
7850
+ }
7851
+ applyOnChangeDerivations(collection, formAccessor) {
7852
+ const applicatorContext = {
7853
+ formValue: this.config.formValue,
7854
+ rootForm: formAccessor,
7855
+ derivationFunctions: this.functionRegistry.getDerivationFunctions(),
7856
+ customFunctions: this.functionRegistry.getCustomFunctions(),
7857
+ externalData: this.resolveExternalData(),
7858
+ logger: this.logger,
7859
+ warningTracker: this.warningTracker,
7860
+ derivationLogger: untracked(() => this.config.derivationLogger()),
7861
+ maxIterations: untracked(() => this.formOptions()?.maxDerivationIterations),
7862
+ };
7863
+ const result = applyDerivationsForTrigger(collection, 'onChange', applicatorContext);
7864
+ if (result.maxIterationsReached) {
7865
+ this.logger.warn(`Derivation processing reached max iterations. ` +
7866
+ `This may indicate a loop in derivation logic that wasn't caught at build time. ` +
7867
+ `Applied: ${result.appliedCount}, Skipped: ${result.skippedCount}, Errors: ${result.errorCount}`);
7868
+ }
7869
+ }
7870
+ applyDebouncedEntriesForPeriod(debounceMs, collection, formAccessor, changedFields) {
7871
+ // Filter entries to just those with the specific debounce period
7872
+ const debouncedEntries = collection.entries.filter((entry) => entry.trigger === 'debounced' && (entry.debounceMs ?? DEFAULT_DEBOUNCE_MS) === debounceMs);
7873
+ if (debouncedEntries.length === 0) {
7874
+ return;
7875
+ }
7876
+ // Create a minimal collection with filtered entries
7877
+ const filteredCollection = {
7878
+ entries: debouncedEntries,
7879
+ };
7880
+ const applicatorContext = {
7881
+ formValue: this.config.formValue,
7882
+ rootForm: formAccessor,
7883
+ derivationFunctions: this.functionRegistry.getDerivationFunctions(),
7884
+ customFunctions: this.functionRegistry.getCustomFunctions(),
7885
+ externalData: this.resolveExternalData(),
7886
+ logger: this.logger,
7887
+ warningTracker: this.warningTracker,
7888
+ derivationLogger: untracked(() => this.config.derivationLogger()),
7889
+ maxIterations: untracked(() => this.formOptions()?.maxDerivationIterations),
7890
+ };
7891
+ const result = applyDerivationsForTrigger(filteredCollection, 'debounced', applicatorContext, changedFields);
7892
+ if (result.maxIterationsReached) {
7893
+ this.logger.warn(`Debounced derivation processing reached max iterations (${debounceMs}ms). ` +
7894
+ `Applied: ${result.appliedCount}, Skipped: ${result.skippedCount}, Errors: ${result.errorCount}`);
7895
+ }
7896
+ }
7897
+ /**
7898
+ * Gets all unique debounce periods from entries with trigger 'debounced'.
7899
+ */
7900
+ getDebouncePeriods(entries) {
7901
+ const periods = new Set();
7902
+ for (const entry of entries) {
7903
+ if (entry.trigger === 'debounced') {
7904
+ periods.add(entry.debounceMs ?? DEFAULT_DEBOUNCE_MS);
7905
+ }
7906
+ }
7907
+ return Array.from(periods);
7908
+ }
7909
+ warnAboutWildcardDependencies(entries, fieldCount) {
7910
+ if (!isDevMode())
7911
+ return;
7912
+ // Find entries with wildcard dependency
7913
+ const wildcardEntries = entries.filter((entry) => entry.dependsOn.includes('*'));
7914
+ if (wildcardEntries.length === 0)
7915
+ return;
7916
+ // Find implicit wildcards (custom functions without explicit dependsOn)
7917
+ const implicitWildcards = wildcardEntries.filter((entry) => entry.functionName && (!entry.originalConfig?.dependsOn || entry.originalConfig.dependsOn.length === 0));
7918
+ if (implicitWildcards.length > 0) {
7919
+ const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey} (${e.functionName})`);
7920
+ this.logger.warn('[Derivation] Derivations using custom functions without explicit dependsOn detected. ' +
7921
+ `These run on EVERY form change, which may impact performance (form has ${fieldCount} fields). ` +
7922
+ 'Consider specifying explicit dependsOn arrays for better performance.', derivationDescs);
7923
+ }
7924
+ }
7925
+ /**
7926
+ * Resolves external data signals to their current values without creating dependencies.
7927
+ *
7928
+ * Uses `untracked()` to read signals without establishing reactive dependencies,
7929
+ * which is important for derivations that shouldn't re-trigger on every external data change.
7930
+ *
7931
+ * @returns Record of resolved external data values, or undefined if no external data.
7932
+ */
7933
+ resolveExternalData() {
7934
+ const externalDataRecord = untracked(() => this.externalDataSignal?.());
7935
+ if (!externalDataRecord) {
7936
+ return undefined;
7937
+ }
7938
+ const resolved = {};
7939
+ for (const [key, signal] of Object.entries(externalDataRecord)) {
7940
+ if (isSignal(signal)) {
7941
+ resolved[key] = untracked(() => signal());
7942
+ }
7943
+ else {
7944
+ resolved[key] = signal;
7945
+ }
7946
+ }
7947
+ return resolved;
7948
+ }
7949
+ }
7950
+ /**
7951
+ * Creates a DerivationOrchestrator for a dynamic form.
7952
+ * Must be called within an injection context.
7953
+ *
7954
+ * @param config - Form-specific signals configuration
7955
+ * @returns The created DerivationOrchestrator
7956
+ *
7957
+ * @public
7958
+ */
7959
+ function createDerivationOrchestrator(config) {
7960
+ return new DerivationOrchestrator(config);
7961
+ }
7962
+ /**
7963
+ * Injection token for the DerivationOrchestrator.
7964
+ *
7965
+ * @public
7966
+ */
7967
+ const DERIVATION_ORCHESTRATOR = new InjectionToken('DERIVATION_ORCHESTRATOR');
7968
+ /**
7969
+ * Injects the DerivationOrchestrator from the current injection context.
7970
+ *
7971
+ * @returns The DerivationOrchestrator instance
7972
+ * @throws Error if called outside of an injection context or if orchestrator is not provided
7973
+ *
7974
+ * @public
7975
+ */
7976
+ function injectDerivationOrchestrator() {
7977
+ return inject(DERIVATION_ORCHESTRATOR);
7978
+ }
7979
+
7980
+ class NextPageEvent {
7981
+ type = 'next-page';
7982
+ }
7983
+
7984
+ class PageChangeEvent {
7985
+ currentPageIndex;
7986
+ totalPages;
7987
+ previousPageIndex;
7988
+ type = 'page-change';
7989
+ constructor(
7990
+ /** The current page index (0-based) */
7991
+ currentPageIndex,
7992
+ /** Total number of pages */
7993
+ totalPages,
7994
+ /** Previous page index (0-based), undefined if first navigation */
7995
+ previousPageIndex) {
7996
+ this.currentPageIndex = currentPageIndex;
7997
+ this.totalPages = totalPages;
7998
+ this.previousPageIndex = previousPageIndex;
7999
+ }
8000
+ }
8001
+
8002
+ class PreviousPageEvent {
8003
+ type = 'previous-page';
8004
+ }
8005
+
8006
+ /**
8007
+ * Event dispatched to add a new item to an array field.
8008
+ *
8009
+ * This event is used to dynamically add items to array fields at runtime.
8010
+ * The array field component listens for this event and adds a new item
8011
+ * based on the provided field template, or uses the array's own template if not provided.
8012
+ *
8013
+ * @example
8014
+ * ```typescript
8015
+ * // Add item using array's own template
8016
+ * eventBus.dispatch(AddArrayItemEvent, 'contacts');
8017
+ *
8018
+ * // Add item with explicit field template
8019
+ * const fieldTemplate = { key: 'email', type: 'input' };
8020
+ * eventBus.dispatch(AddArrayItemEvent, 'contacts', fieldTemplate);
8021
+ *
8022
+ * // Add item at specific index
8023
+ * eventBus.dispatch(AddArrayItemEvent, 'contacts', fieldTemplate, 2);
8024
+ * ```
8025
+ */
8026
+ class AddArrayItemEvent {
8027
+ arrayKey;
8028
+ field;
8029
+ index;
8030
+ type = 'add-array-item';
8031
+ constructor(
8032
+ /** The key of the array field to add an item to */
8033
+ arrayKey,
8034
+ /**
8035
+ * Optional field template definition to render for the new array item.
8036
+ * If not provided, the array field component will use its own template.
8037
+ */
8038
+ field,
8039
+ /** Optional index where to insert the item (defaults to end of array) */
8040
+ index) {
8041
+ this.arrayKey = arrayKey;
8042
+ this.field = field;
8043
+ this.index = index;
8044
+ }
8045
+ }
8046
+
8047
+ /**
8048
+ * Event dispatched to remove an item from an array field.
8049
+ *
8050
+ * This event is used to dynamically remove items from array fields at runtime.
8051
+ * The array field component listens for this event and removes the item
8052
+ * at the specified index.
8053
+ *
8054
+ * @example
8055
+ * ```typescript
8056
+ * // Remove item at index 1 from 'contacts' array
8057
+ * eventBus.dispatch(RemoveArrayItemEvent, 'contacts', 1);
8058
+ *
8059
+ * // Remove last item
8060
+ * eventBus.dispatch(RemoveArrayItemEvent, 'contacts');
8061
+ * ```
8062
+ */
8063
+ class RemoveArrayItemEvent {
8064
+ arrayKey;
8065
+ index;
8066
+ type = 'remove-array-item';
8067
+ constructor(
8068
+ /** The key of the array field to remove an item from */
8069
+ arrayKey,
8070
+ /** Index of the item to remove (defaults to last item) */
8071
+ index) {
8072
+ this.arrayKey = arrayKey;
8073
+ this.index = index;
8074
+ }
8075
+ }
8076
+
8077
+ /**
8078
+ * Event dispatched when the form should be reset to its default values.
8079
+ *
8080
+ * This event instructs the dynamic form component to restore all field values
8081
+ * to their initial default values as defined in the form configuration.
8082
+ *
8083
+ * @example
8084
+ * ```typescript
8085
+ * // Dispatch from a button component
8086
+ * eventBus.dispatch(FormResetEvent);
8087
+ * ```
8088
+ *
8089
+ * @example
8090
+ * ```typescript
8091
+ * // Listen for reset events
8092
+ * eventBus.on<FormResetEvent>('form-reset').subscribe(() => {
8093
+ * console.log('Form was reset to defaults');
8094
+ * });
8095
+ * ```
8096
+ */
8097
+ class FormResetEvent {
8098
+ type = 'form-reset';
8099
+ }
8100
+
8101
+ /**
8102
+ * Event dispatched when the form should be cleared.
8103
+ *
8104
+ * This event instructs the dynamic form component to clear all field values,
8105
+ * resetting them to empty/undefined state regardless of their default values.
8106
+ *
8107
+ * @example
8108
+ * ```typescript
8109
+ * // Dispatch from a button component
8110
+ * eventBus.dispatch(FormClearEvent);
8111
+ * ```
8112
+ *
8113
+ * @example
8114
+ * ```typescript
8115
+ * // Listen for clear events
8116
+ * eventBus.on<FormClearEvent>('form-clear').subscribe(() => {
8117
+ * console.log('Form was cleared');
5032
8118
  * });
5033
8119
  * ```
5034
8120
  */
@@ -5380,47 +8466,177 @@ class PageOrchestratorComponent {
5380
8466
  }
5381
8467
  }
5382
8468
  }
5383
- `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-Bt6blv9I.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-Bt6blv9I.mjs').then(m => m.default)]] });
5384
- }
5385
- i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.1.0", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-Bt6blv9I.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
5386
- type: Component,
5387
- args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
5388
- @for (pageField of pageFields(); track pageField.key; let i = $index) {
5389
- @if (i === state().currentPageIndex || i === state().currentPageIndex + 1 || i === state().currentPageIndex - 1) {
5390
- <!-- Current and adjacent pages (±1): render immediately for flicker-free navigation -->
5391
- @defer (on immediate) {
5392
- <section
5393
- page-field
5394
- [field]="pageField"
5395
- [key]="pageField.key"
5396
- [pageIndex]="i"
5397
- [isVisible]="i === state().currentPageIndex"
5398
- ></section>
5399
- } @placeholder {
5400
- <div class="df-page-placeholder" [attr.data-page-index]="i" [attr.data-page-key]="pageField.key"></div>
5401
- }
5402
- } @else {
5403
- <!-- Distant pages: defer until browser is idle for memory savings -->
5404
- @defer (on idle) {
5405
- <section page-field [field]="pageField" [key]="pageField.key" [pageIndex]="i" [isVisible]="false"></section>
5406
- } @placeholder {
5407
- <div class="df-page-placeholder" [attr.data-page-index]="i" [attr.data-page-key]="pageField.key"></div>
5408
- }
5409
- }
8469
+ `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs').then(m => m.default)]] });
8470
+ }
8471
+ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.1.0", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
8472
+ type: Component,
8473
+ args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
8474
+ @for (pageField of pageFields(); track pageField.key; let i = $index) {
8475
+ @if (i === state().currentPageIndex || i === state().currentPageIndex + 1 || i === state().currentPageIndex - 1) {
8476
+ <!-- Current and adjacent pages (±1): render immediately for flicker-free navigation -->
8477
+ @defer (on immediate) {
8478
+ <section
8479
+ page-field
8480
+ [field]="pageField"
8481
+ [key]="pageField.key"
8482
+ [pageIndex]="i"
8483
+ [isVisible]="i === state().currentPageIndex"
8484
+ ></section>
8485
+ } @placeholder {
8486
+ <div class="df-page-placeholder" [attr.data-page-index]="i" [attr.data-page-key]="pageField.key"></div>
8487
+ }
8488
+ } @else {
8489
+ <!-- Distant pages: defer until browser is idle for memory savings -->
8490
+ @defer (on idle) {
8491
+ <section page-field [field]="pageField" [key]="pageField.key" [pageIndex]="i" [isVisible]="false"></section>
8492
+ } @placeholder {
8493
+ <div class="df-page-placeholder" [attr.data-page-index]="i" [attr.data-page-key]="pageField.key"></div>
8494
+ }
8495
+ }
8496
+ }
8497
+ `, host: {
8498
+ class: 'df-page-orchestrator',
8499
+ '[attr.data-current-page]': 'state().currentPageIndex',
8500
+ '[attr.data-total-pages]': 'state().totalPages',
8501
+ }, providers: [
8502
+ {
8503
+ provide: FIELD_SIGNAL_CONTEXT,
8504
+ useFactory: (orchestrator) => orchestrator.extendedFieldSignalContext(),
8505
+ deps: [PageOrchestratorComponent],
8506
+ },
8507
+ ], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative;width:100%}\n"] }]
8508
+ }], ctorParameters: () => [], propDecorators: { pageFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageFields", required: true }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], fieldSignalContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldSignalContext", required: true }] }] } }) });
8509
+
8510
+ /**
8511
+ * Type guard to check if a value is a DynamicFormFeature
8512
+ */
8513
+ function isDynamicFormFeature(value) {
8514
+ return (typeof value === 'object' &&
8515
+ value !== null &&
8516
+ 'ɵkind' in value &&
8517
+ typeof value.ɵkind === 'string' &&
8518
+ 'ɵproviders' in value &&
8519
+ Array.isArray(value.ɵproviders));
8520
+ }
8521
+ /**
8522
+ * Helper to create a feature with proper typing
8523
+ */
8524
+ function createFeature(kind, providers) {
8525
+ return {
8526
+ ɵkind: kind,
8527
+ ɵproviders: providers,
8528
+ };
8529
+ }
8530
+
8531
+ /**
8532
+ * Console-based logger implementation.
8533
+ */
8534
+ class ConsoleLogger {
8535
+ prefix = '[Dynamic Forms]';
8536
+ debug(message, ...args) {
8537
+ console.debug(this.prefix, message, ...args);
8538
+ }
8539
+ info(message, ...args) {
8540
+ console.info(this.prefix, message, ...args);
8541
+ }
8542
+ warn(message, ...args) {
8543
+ console.warn(this.prefix, message, ...args);
8544
+ }
8545
+ error(message, ...args) {
8546
+ console.error(this.prefix, message, ...args);
8547
+ }
8548
+ }
8549
+
8550
+ /**
8551
+ * No-operation logger implementation.
8552
+ * All methods are no-ops - used in production by default.
8553
+ */
8554
+ class NoopLogger {
8555
+ debug() {
8556
+ // Intentionally empty
8557
+ }
8558
+ info() {
8559
+ // Intentionally empty
8560
+ }
8561
+ warn() {
8562
+ // Intentionally empty
8563
+ }
8564
+ error() {
8565
+ // Intentionally empty
8566
+ }
8567
+ }
8568
+
8569
+ /**
8570
+ * Injection token for derivation logging configuration.
8571
+ *
8572
+ * @internal
8573
+ */
8574
+ const DERIVATION_LOG_CONFIG = new InjectionToken('DERIVATION_LOG_CONFIG', {
8575
+ providedIn: 'root',
8576
+ factory: createDefaultDerivationLogConfig,
8577
+ });
8578
+ /**
8579
+ * Configure logging for dynamic forms.
8580
+ *
8581
+ * By default, general logging is enabled (ConsoleLogger) and derivation
8582
+ * logging is disabled ('none'). Use this feature to enable derivation debugging.
8583
+ *
8584
+ * @example
8585
+ * ```typescript
8586
+ * // Disable all logging
8587
+ * provideDynamicForm(
8588
+ * ...withMaterialFields(),
8589
+ * withLoggerConfig(false)
8590
+ * )
8591
+ *
8592
+ * // Enable verbose derivation logging
8593
+ * provideDynamicForm(
8594
+ * ...withMaterialFields(),
8595
+ * withLoggerConfig({ derivations: 'verbose' })
8596
+ * )
8597
+ *
8598
+ * // Disable derivation logging but keep general logging
8599
+ * provideDynamicForm(
8600
+ * ...withMaterialFields(),
8601
+ * withLoggerConfig({ derivations: 'none' })
8602
+ * )
8603
+ *
8604
+ * // Conditional logging (e.g., only in dev mode)
8605
+ * provideDynamicForm(
8606
+ * ...withMaterialFields(),
8607
+ * withLoggerConfig(() => isDevMode())
8608
+ * )
8609
+ * ```
8610
+ *
8611
+ * @param config - Boolean to enable/disable logging, a function returning boolean, or a config object
8612
+ * @returns A DynamicFormFeature that configures logging
8613
+ *
8614
+ * @public
8615
+ */
8616
+ function withLoggerConfig(config = true) {
8617
+ // Handle boolean or function
8618
+ if (typeof config === 'boolean' || typeof config === 'function') {
8619
+ const isEnabled = typeof config === 'function' ? config() : config;
8620
+ const logger = isEnabled ? new ConsoleLogger() : new NoopLogger();
8621
+ return createFeature('logger', [{ provide: DynamicFormLogger, useValue: logger }]);
8622
+ }
8623
+ // Handle config object
8624
+ const isEnabled = config.enabled !== false;
8625
+ const logger = isEnabled ? new ConsoleLogger() : new NoopLogger();
8626
+ const providers = [{ provide: DynamicFormLogger, useValue: logger }];
8627
+ // Add derivation log config if specified
8628
+ if (config.derivations !== undefined) {
8629
+ providers.push({
8630
+ provide: DERIVATION_LOG_CONFIG,
8631
+ useValue: { level: config.derivations },
8632
+ });
5410
8633
  }
5411
- `, host: {
5412
- class: 'df-page-orchestrator',
5413
- '[attr.data-current-page]': 'state().currentPageIndex',
5414
- '[attr.data-total-pages]': 'state().totalPages',
5415
- }, providers: [
5416
- {
5417
- provide: FIELD_SIGNAL_CONTEXT,
5418
- useFactory: (orchestrator) => orchestrator.extendedFieldSignalContext(),
5419
- deps: [PageOrchestratorComponent],
5420
- },
5421
- ], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative;width:100%}\n"] }]
5422
- }], ctorParameters: () => [], propDecorators: { pageFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageFields", required: true }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], fieldSignalContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldSignalContext", required: true }] }] } }) });
8634
+ return createFeature('logger', providers);
8635
+ }
5423
8636
 
8637
+ function derivationOrchestratorFactoryWrapper(dynamicForm) {
8638
+ return createDerivationOrchestrator(dynamicForm.derivationOrchestratorConfig);
8639
+ }
5424
8640
  /**
5425
8641
  * Dynamic form component that renders a complete form based on configuration.
5426
8642
  *
@@ -5452,6 +8668,7 @@ class DynamicForm {
5452
8668
  functionRegistry = inject(FunctionRegistryService);
5453
8669
  schemaRegistry = inject(SchemaRegistryService);
5454
8670
  logger = inject(DynamicFormLogger);
8671
+ derivationLogConfig = inject(DERIVATION_LOG_CONFIG);
5455
8672
  // ─────────────────────────────────────────────────────────────────────────────
5456
8673
  // Inputs
5457
8674
  // ─────────────────────────────────────────────────────────────────────────────
@@ -5542,6 +8759,15 @@ class DynamicForm {
5542
8759
  const inputOptions = this.formOptions();
5543
8760
  return { ...configOptions, ...inputOptions };
5544
8761
  }, ...(ngDevMode ? [{ debugName: "effectiveFormOptions" }] : []));
8762
+ /**
8763
+ * Computed derivation logger based on injected config.
8764
+ *
8765
+ * Uses factory pattern to return no-op logger when logging is disabled,
8766
+ * avoiding any logging overhead in production.
8767
+ */
8768
+ derivationLogger = computed(() => {
8769
+ return createDerivationLogger(this.derivationLogConfig, this.logger);
8770
+ }, ...(ngDevMode ? [{ debugName: "derivationLogger" }] : []));
5545
8771
  fieldSignalContext = computed(() => ({
5546
8772
  injector: this.injector,
5547
8773
  value: this.value,
@@ -5577,12 +8803,27 @@ class DynamicForm {
5577
8803
  form = computed(() => {
5578
8804
  return runInInjectionContext(this.injector, () => {
5579
8805
  const setup = this.formSetup();
8806
+ const config = this.config();
5580
8807
  let formInstance;
5581
8808
  untracked(() => this.rootFormRegistry.registerFormValueSignal(this.entity));
5582
- if (setup.schemaFields && setup.schemaFields.length > 0) {
8809
+ const hasFields = setup.schemaFields && setup.schemaFields.length > 0;
8810
+ const hasFormSchema = config.schema !== undefined;
8811
+ if (hasFields) {
5583
8812
  const crossFieldCollection = collectCrossFieldEntries(setup.schemaFields);
5584
- const schema = createSchemaFromFields(setup.schemaFields, setup.registry, crossFieldCollection.validators);
5585
- formInstance = untracked(() => form(this.entity, schema));
8813
+ // Create schema with both field-level and form-level validation
8814
+ // Type assertion needed due to complex generic type inference between TModel and InferFormValue
8815
+ const combinedSchema = createSchemaFromFields(setup.schemaFields, setup.registry, {
8816
+ crossFieldValidators: crossFieldCollection.validators,
8817
+ formLevelSchema: config.schema,
8818
+ });
8819
+ formInstance = untracked(() => form(this.entity, combinedSchema));
8820
+ }
8821
+ else if (hasFormSchema) {
8822
+ // Only form-level schema (no fields with validation)
8823
+ // Non-null assertion safe: hasFormSchema guarantees config.schema is defined
8824
+ // Type assertion needed due to generic type inference
8825
+ const formSchema = createFormLevelSchema(config.schema);
8826
+ formInstance = untracked(() => form(this.entity, formSchema));
5586
8827
  }
5587
8828
  else {
5588
8829
  formInstance = untracked(() => form(this.entity));
@@ -5628,6 +8869,18 @@ class DynamicForm {
5628
8869
  }, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
5629
8870
  /** Whether the form is currently submitting. */
5630
8871
  submitting = computed(() => this.form()().submitting(), ...(ngDevMode ? [{ debugName: "submitting" }] : []));
8872
+ /**
8873
+ * Configuration for the derivation orchestrator.
8874
+ * Bundles the form-specific signals needed by the orchestrator.
8875
+ *
8876
+ * @internal Used by DERIVATION_ORCHESTRATOR provider.
8877
+ */
8878
+ derivationOrchestratorConfig = {
8879
+ schemaFields: computed(() => this.formSetup().schemaFields),
8880
+ formValue: this.formValue,
8881
+ form: computed(() => this.form()),
8882
+ derivationLogger: this.derivationLogger,
8883
+ };
5631
8884
  // ─────────────────────────────────────────────────────────────────────────────
5632
8885
  // Initialization
5633
8886
  // ─────────────────────────────────────────────────────────────────────────────
@@ -5645,10 +8898,21 @@ class DynamicForm {
5645
8898
  /** Emits when form dirty state changes. */
5646
8899
  dirtyChange = outputFromObservable(toObservable(this.dirty));
5647
8900
  /**
5648
- * Emits form values when submitted (via SubmitEvent).
8901
+ * Emits form values when submitted (via SubmitEvent) and form is valid.
8902
+ *
8903
+ * **Important:** This output only emits when the form is valid. If you need to
8904
+ * handle submit events regardless of validity, use the `(events)` output and
8905
+ * filter for `'submit'` events.
8906
+ *
5649
8907
  * Note: Does not emit when `submission.action` is configured - use one or the other.
5650
8908
  */
5651
- submitted = outputFromObservable(this.eventBus.on('submit').pipe(switchMap(() => {
8909
+ submitted = outputFromObservable(this.eventBus.on('submit').pipe(filter(() => {
8910
+ if (!this.valid()) {
8911
+ this.logger.debug('Form submitted while invalid, not emitting to (submitted) output');
8912
+ return false;
8913
+ }
8914
+ return true;
8915
+ }), switchMap(() => {
5652
8916
  const submissionConfig = this.config().submission;
5653
8917
  if (submissionConfig?.action) {
5654
8918
  this.logger.warn('Both `submission.action` and `(submitted)` output are configured. ' +
@@ -5656,7 +8920,8 @@ class DynamicForm {
5656
8920
  'Use either `submission.action` OR `(submitted)`, not both.');
5657
8921
  return EMPTY;
5658
8922
  }
5659
- return of(this.value());
8923
+ // Form is valid here, cast to TModel (no longer Partial)
8924
+ return of(this.formValue());
5660
8925
  })));
5661
8926
  /** Emits when form is reset to default values. */
5662
8927
  reset = outputFromObservable(this.eventBus.on('form-reset'));
@@ -5707,6 +8972,11 @@ class DynamicForm {
5707
8972
  constructor() {
5708
8973
  this.setupEffects();
5709
8974
  this.setupEventHandlers();
8975
+ // Lazily inject the derivation orchestrator to avoid circular dependency.
8976
+ // The orchestrator's constructor sets up RxJS subscriptions that process derivations.
8977
+ afterNextRender(() => {
8978
+ this.injector.get(DERIVATION_ORCHESTRATOR);
8979
+ });
5710
8980
  }
5711
8981
  /**
5712
8982
  * Handles native form submission triggered by:
@@ -5768,7 +9038,9 @@ class DynamicForm {
5768
9038
  formSignal: this.form,
5769
9039
  })
5770
9040
  .pipe(takeUntilDestroyed())
5771
- .subscribe();
9041
+ .subscribe({
9042
+ error: (err) => this.logger.error('Submission handler error', err),
9043
+ });
5772
9044
  }
5773
9045
  registerValidatorsFromConfig({ customFnConfig, schemas }) {
5774
9046
  if (schemas) {
@@ -5784,6 +9056,10 @@ class DynamicForm {
5784
9056
  this.functionRegistry.registerCustomFunction(name, fn);
5785
9057
  });
5786
9058
  }
9059
+ // Register derivation functions (stored separately for use by derivation applicator)
9060
+ if (customFnConfig.derivations) {
9061
+ this.functionRegistry.setDerivationFunctions(customFnConfig.derivations);
9062
+ }
5787
9063
  this.functionRegistry.setValidators(customFnConfig.validators);
5788
9064
  this.functionRegistry.setAsyncValidators(customFnConfig.asyncValidators);
5789
9065
  this.functionRegistry.setHttpValidators(customFnConfig.httpValidators);
@@ -5855,6 +9131,17 @@ class DynamicForm {
5855
9131
  useFactory: (form) => form.effectiveFormOptions,
5856
9132
  deps: [DynamicForm],
5857
9133
  },
9134
+ {
9135
+ provide: EXTERNAL_DATA,
9136
+ useFactory: (form) => computed(() => form.config().externalData),
9137
+ deps: [DynamicForm],
9138
+ },
9139
+ { provide: DERIVATION_WARNING_TRACKER, useFactory: createDerivationWarningTracker },
9140
+ {
9141
+ provide: DERIVATION_ORCHESTRATOR,
9142
+ useFactory: derivationOrchestratorFactoryWrapper,
9143
+ deps: [DynamicForm],
9144
+ },
5858
9145
  ], ngImport: i0, template: `
5859
9146
  @if (formModeDetection().mode === 'paged') {
5860
9147
  <div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
@@ -5897,6 +9184,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
5897
9184
  useFactory: (form) => form.effectiveFormOptions,
5898
9185
  deps: [DynamicForm],
5899
9186
  },
9187
+ {
9188
+ provide: EXTERNAL_DATA,
9189
+ useFactory: (form) => computed(() => form.config().externalData),
9190
+ deps: [DynamicForm],
9191
+ },
9192
+ { provide: DERIVATION_WARNING_TRACKER, useFactory: createDerivationWarningTracker },
9193
+ {
9194
+ provide: DERIVATION_ORCHESTRATOR,
9195
+ useFactory: derivationOrchestratorFactoryWrapper,
9196
+ deps: [DynamicForm],
9197
+ },
5900
9198
  ], host: {
5901
9199
  class: 'df-dynamic-form df-form',
5902
9200
  novalidate: '', // Disable browser validation - Angular Signal Forms handles validation
@@ -6081,13 +9379,13 @@ const BUILT_IN_FIELDS = [
6081
9379
  },
6082
9380
  {
6083
9381
  name: 'page',
6084
- loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-Bt6blv9I.mjs'),
9382
+ loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs'),
6085
9383
  mapper: pageFieldMapper,
6086
9384
  valueHandling: 'flatten',
6087
9385
  },
6088
9386
  {
6089
9387
  name: 'text',
6090
- loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-DKCx7Z_U.mjs'),
9388
+ loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-DIVyioMX.mjs'),
6091
9389
  mapper: textFieldMapper,
6092
9390
  valueHandling: 'exclude',
6093
9391
  },
@@ -6098,46 +9396,6 @@ const BUILT_IN_FIELDS = [
6098
9396
  },
6099
9397
  ];
6100
9398
 
6101
- /**
6102
- * Type guard to check if a value is a DynamicFormFeature
6103
- */
6104
- function isDynamicFormFeature(value) {
6105
- return (typeof value === 'object' &&
6106
- value !== null &&
6107
- 'ɵkind' in value &&
6108
- typeof value.ɵkind === 'string' &&
6109
- 'ɵproviders' in value &&
6110
- Array.isArray(value.ɵproviders));
6111
- }
6112
- /**
6113
- * Helper to create a feature with proper typing
6114
- */
6115
- function createFeature(kind, providers) {
6116
- return {
6117
- ɵkind: kind,
6118
- ɵproviders: providers,
6119
- };
6120
- }
6121
-
6122
- /**
6123
- * Console-based logger implementation.
6124
- */
6125
- class ConsoleLogger {
6126
- prefix = '[Dynamic Forms]';
6127
- debug(message, ...args) {
6128
- console.debug(this.prefix, message, ...args);
6129
- }
6130
- info(message, ...args) {
6131
- console.info(this.prefix, message, ...args);
6132
- }
6133
- warn(message, ...args) {
6134
- console.warn(this.prefix, message, ...args);
6135
- }
6136
- error(message, ...args) {
6137
- console.error(this.prefix, message, ...args);
6138
- }
6139
- }
6140
-
6141
9399
  /**
6142
9400
  * Provider function to configure the dynamic form system with field types and options.
6143
9401
  *
@@ -6232,52 +9490,76 @@ function provideDynamicForm(...items) {
6232
9490
  }
6233
9491
 
6234
9492
  /**
6235
- * No-operation logger implementation.
6236
- * All methods are no-ops - used in production by default.
9493
+ * Enables automatic form value attachment to all events dispatched through the EventBus.
9494
+ *
9495
+ * When this feature is enabled, events dispatched via `eventBus.dispatch()` will include
9496
+ * the current form value in the `formValue` property. This is useful for event handlers
9497
+ * that need access to the complete form state at the time of the event.
9498
+ *
9499
+ * @example Global opt-in
9500
+ * ```typescript
9501
+ * provideDynamicForm(
9502
+ * ...withMaterialFields(),
9503
+ * withEventFormValue()
9504
+ * )
9505
+ * ```
9506
+ *
9507
+ * @example Consumer usage
9508
+ * ```typescript
9509
+ * eventBus.on<SubmitEvent>('submit').subscribe(event => {
9510
+ * if (hasFormValue(event)) {
9511
+ * console.log('Form value:', event.formValue);
9512
+ * }
9513
+ * });
9514
+ * ```
9515
+ *
9516
+ * @example Per-form disable (when globally enabled)
9517
+ * ```typescript
9518
+ * const config: FormConfig = {
9519
+ * fields: [...],
9520
+ * options: { emitFormValueOnEvents: false }
9521
+ * };
9522
+ * ```
9523
+ *
9524
+ * @remarks
9525
+ * **Precedence rules:**
9526
+ * 1. Per-form `false` - Disables for this form, regardless of global setting
9527
+ * 2. Per-form `true` - Enables for this form, regardless of global setting
9528
+ * 3. Per-form `undefined` - Uses global setting
9529
+ * 4. Global `withEventFormValue()` - Enables globally
9530
+ * 5. No global feature - Disabled globally (default)
9531
+ *
9532
+ * @returns A DynamicFormFeature that enables form value attachment to events
9533
+ *
9534
+ * @public
6237
9535
  */
6238
- class NoopLogger {
6239
- debug() {
6240
- // Intentionally empty
6241
- }
6242
- info() {
6243
- // Intentionally empty
6244
- }
6245
- warn() {
6246
- // Intentionally empty
6247
- }
6248
- error() {
6249
- // Intentionally empty
6250
- }
9536
+ function withEventFormValue() {
9537
+ return createFeature('event-form-value', [{ provide: EMIT_FORM_VALUE_ON_EVENTS, useValue: true }]);
6251
9538
  }
6252
9539
 
6253
9540
  /**
6254
- * Configure logging for dynamic forms.
9541
+ * Type guard to check if a form event has a form value attached.
6255
9542
  *
6256
- * By default, logging is enabled (ConsoleLogger). Use this feature to disable logging
6257
- * or configure it conditionally.
9543
+ * Use this to safely access the `formValue` property on events when
9544
+ * `withEventFormValue()` is enabled globally or `options.emitFormValueOnEvents`
9545
+ * is set to `true` in the form config.
6258
9546
  *
6259
9547
  * @example
6260
9548
  * ```typescript
6261
- * // Disable logging
6262
- * provideDynamicForm(
6263
- * ...withMaterialFields(),
6264
- * withLoggerConfig(false)
6265
- * )
6266
- *
6267
- * // Conditional logging (e.g., only in dev mode)
6268
- * provideDynamicForm(
6269
- * ...withMaterialFields(),
6270
- * withLoggerConfig(() => isDevMode())
6271
- * )
9549
+ * eventBus.on<SubmitEvent>('submit').subscribe(event => {
9550
+ * if (hasFormValue(event)) {
9551
+ * console.log('Form value:', event.formValue);
9552
+ * }
9553
+ * });
6272
9554
  * ```
6273
9555
  *
6274
- * @param enabled - Whether logging is enabled. Can be a boolean or a function that returns a boolean. Defaults to true.
6275
- * @returns A DynamicFormFeature that configures logging
9556
+ * @param event - The form event to check
9557
+ * @returns `true` if the event has a form value attached, narrowing the type
9558
+ *
9559
+ * @public
6276
9560
  */
6277
- function withLoggerConfig(enabled = true) {
6278
- const isEnabled = typeof enabled === 'function' ? enabled() : enabled;
6279
- const logger = isEnabled ? new ConsoleLogger() : new NoopLogger();
6280
- return createFeature('logger', [{ provide: DynamicFormLogger, useValue: logger }]);
9561
+ function hasFormValue(event) {
9562
+ return 'formValue' in event && event.formValue !== undefined;
6281
9563
  }
6282
9564
 
6283
9565
  /**
@@ -6535,6 +9817,232 @@ function resolveTokens(args, context) {
6535
9817
  });
6536
9818
  }
6537
9819
 
9820
+ /**
9821
+ * Type-safe form config builder that ensures schema matches field structure.
9822
+ *
9823
+ * This helper function provides type safety when using form-level schemas by
9824
+ * automatically inferring the form value type from fields and constraining
9825
+ * the schema type parameter accordingly.
9826
+ *
9827
+ * **When to use:**
9828
+ * - When you have a form-level `schema` and want type safety between fields and schema
9829
+ * - When you prefer function syntax over `as const satisfies FormConfig`
9830
+ *
9831
+ * **Note:** This is optional. Users who don't need schema type safety can
9832
+ * continue using `as const satisfies FormConfig` directly.
9833
+ *
9834
+ * @typeParam TFields - The field definitions array (narrowed via `as const`)
9835
+ * @typeParam TProps - Optional form-level default props type
9836
+ *
9837
+ * @param config - The form configuration object
9838
+ * @returns The same configuration with proper type inference
9839
+ *
9840
+ * @example Basic usage with Zod schema
9841
+ * ```typescript
9842
+ * import { z } from 'zod';
9843
+ * import { formConfig } from '@ng-forge/dynamic-forms';
9844
+ * import { standardSchema } from '@ng-forge/dynamic-forms/schema';
9845
+ *
9846
+ * const passwordSchema = z.object({
9847
+ * password: z.string().min(8),
9848
+ * confirmPassword: z.string(),
9849
+ * }).refine(
9850
+ * (data) => data.password === data.confirmPassword,
9851
+ * { message: 'Passwords must match', path: ['confirmPassword'] }
9852
+ * );
9853
+ *
9854
+ * const config = formConfig({
9855
+ * schema: standardSchema(passwordSchema),
9856
+ * fields: [
9857
+ * { key: 'password', type: 'input', label: 'Password', required: true, props: { type: 'password' } },
9858
+ * { key: 'confirmPassword', type: 'input', label: 'Confirm', required: true, props: { type: 'password' } },
9859
+ * { key: 'submit', type: 'submit', label: 'Register' },
9860
+ * ] as const,
9861
+ * });
9862
+ * ```
9863
+ *
9864
+ * @example Equivalent without helper
9865
+ * ```typescript
9866
+ * const config = {
9867
+ * schema: standardSchema(passwordSchema),
9868
+ * fields: [...],
9869
+ * } as const satisfies FormConfig;
9870
+ * ```
9871
+ */
9872
+ function formConfig(config) {
9873
+ return config;
9874
+ }
9875
+
9876
+ /**
9877
+ * Container field types that do NOT support labels or most logic
9878
+ */
9879
+ const CONTAINER_TYPES = ['group', 'row', 'array'];
9880
+ /**
9881
+ * Field types that support options at the field level
9882
+ */
9883
+ const OPTION_FIELD_TYPES = ['select', 'radio', 'multi-checkbox'];
9884
+ /**
9885
+ * Field type that uses minValue/maxValue instead of min/max in props
9886
+ */
9887
+ const SLIDER_TYPE = 'slider';
9888
+ /**
9889
+ * Hidden field type - no logic, no validators, no label
9890
+ */
9891
+ const HIDDEN_TYPE = 'hidden';
9892
+ /**
9893
+ * Page field type - only supports hidden logic
9894
+ */
9895
+ const PAGE_TYPE = 'page';
9896
+ /**
9897
+ * Creates a typed field configuration with helpful error messages for common mistakes.
9898
+ *
9899
+ * This helper function provides early validation and clear error messages
9900
+ * for common configuration pitfalls that would otherwise cause runtime errors.
9901
+ *
9902
+ * @example
9903
+ * ```typescript
9904
+ * // Basic usage
9905
+ * const nameField = createField('input', {
9906
+ * key: 'name',
9907
+ * label: 'Name',
9908
+ * value: ''
9909
+ * });
9910
+ *
9911
+ * // With validation
9912
+ * const emailField = createField('input', {
9913
+ * key: 'email',
9914
+ * label: 'Email',
9915
+ * required: true,
9916
+ * email: true,
9917
+ * props: { type: 'email' }
9918
+ * });
9919
+ *
9920
+ * // Select with options (at field level)
9921
+ * const countryField = createField('select', {
9922
+ * key: 'country',
9923
+ * label: 'Country',
9924
+ * options: [{ label: 'USA', value: 'us' }]
9925
+ * });
9926
+ *
9927
+ * // Slider with minValue/maxValue (at field level)
9928
+ * const ratingField = createField('slider', {
9929
+ * key: 'rating',
9930
+ * label: 'Rating',
9931
+ * minValue: 1,
9932
+ * maxValue: 10,
9933
+ * step: 1,
9934
+ * value: 5
9935
+ * });
9936
+ * ```
9937
+ *
9938
+ * @param type - The field type (e.g., 'input', 'select', 'group')
9939
+ * @param config - The field configuration (excluding the 'type' property)
9940
+ * @returns A properly typed field configuration
9941
+ * @throws DynamicFormError if the configuration contains common mistakes
9942
+ */
9943
+ function createField(type, config) {
9944
+ const configWithProps = config;
9945
+ // Validate key is present
9946
+ if (!configWithProps['key']) {
9947
+ throw new DynamicFormError(`createField('${type}'): 'key' property is required`);
9948
+ }
9949
+ // Container validation: no label allowed
9950
+ if (CONTAINER_TYPES.includes(type)) {
9951
+ if ('label' in configWithProps && configWithProps['label'] !== undefined) {
9952
+ throw new DynamicFormError(`createField('${type}'): Container fields (${CONTAINER_TYPES.join(', ')}) do NOT support 'label'. ` +
9953
+ `Labels go on the child fields inside the container.`);
9954
+ }
9955
+ }
9956
+ // Container validation: no logic allowed (except page with hidden)
9957
+ if (CONTAINER_TYPES.includes(type)) {
9958
+ if ('logic' in configWithProps && configWithProps['logic'] !== undefined) {
9959
+ throw new DynamicFormError(`createField('${type}'): Container fields (${CONTAINER_TYPES.join(', ')}) do NOT support 'logic'. ` +
9960
+ `Apply logic to child fields instead, or use 'page' containers which support 'hidden' logic only.`);
9961
+ }
9962
+ }
9963
+ // Page validation: only hidden logic allowed
9964
+ if (type === PAGE_TYPE && 'logic' in configWithProps) {
9965
+ const logic = configWithProps['logic'];
9966
+ if (Array.isArray(logic)) {
9967
+ const nonHiddenLogic = logic.filter((l) => l.type !== 'hidden');
9968
+ if (nonHiddenLogic.length > 0) {
9969
+ throw new DynamicFormError(`createField('page'): Pages only support 'hidden' logic type. ` +
9970
+ `Found unsupported logic types: ${nonHiddenLogic.map((l) => l.type).join(', ')}. ` +
9971
+ `For other logic types, apply them to child fields instead.`);
9972
+ }
9973
+ }
9974
+ }
9975
+ // Options placement validation
9976
+ if (OPTION_FIELD_TYPES.includes(type)) {
9977
+ const props = configWithProps['props'];
9978
+ if (props && 'options' in props) {
9979
+ throw new DynamicFormError(`createField('${type}'): 'options' must be at FIELD level, NOT inside 'props'. ` +
9980
+ `Move 'options' from props to the field root: { type: '${type}', key: '...', options: [...] }`);
9981
+ }
9982
+ }
9983
+ // Slider props validation
9984
+ if (type === SLIDER_TYPE) {
9985
+ const props = configWithProps['props'];
9986
+ if (props) {
9987
+ if ('min' in props || 'max' in props) {
9988
+ throw new DynamicFormError(`createField('slider'): Use 'minValue' and 'maxValue' at FIELD level, NOT 'min'/'max' in props. ` +
9989
+ `Example: { type: 'slider', key: '...', minValue: 0, maxValue: 100, step: 1 }`);
9990
+ }
9991
+ if ('step' in props) {
9992
+ throw new DynamicFormError(`createField('slider'): Use 'step' at FIELD level, NOT in props. ` +
9993
+ `Example: { type: 'slider', key: '...', minValue: 0, maxValue: 100, step: 1 }`);
9994
+ }
9995
+ }
9996
+ }
9997
+ // Hidden field validation
9998
+ if (type === HIDDEN_TYPE) {
9999
+ if ('logic' in configWithProps && configWithProps['logic'] !== undefined) {
10000
+ throw new DynamicFormError(`createField('hidden'): Hidden fields do NOT support 'logic'. ` + `Hidden fields are purely for passing values through the form.`);
10001
+ }
10002
+ if ('validators' in configWithProps && configWithProps['validators'] !== undefined) {
10003
+ throw new DynamicFormError(`createField('hidden'): Hidden fields do NOT support 'validators'. ` +
10004
+ `Hidden fields are purely for passing values through the form.`);
10005
+ }
10006
+ if ('required' in configWithProps && configWithProps['required']) {
10007
+ throw new DynamicFormError(`createField('hidden'): Hidden fields do NOT support 'required' validation. ` +
10008
+ `Hidden fields are purely for passing values through the form.`);
10009
+ }
10010
+ if ('label' in configWithProps && configWithProps['label'] !== undefined) {
10011
+ throw new DynamicFormError(`createField('hidden'): Hidden fields do NOT support 'label'. ` +
10012
+ `Hidden fields are not rendered and have no visual representation.`);
10013
+ }
10014
+ }
10015
+ // Array field validation
10016
+ if (type === 'array') {
10017
+ if ('template' in configWithProps) {
10018
+ throw new DynamicFormError(`createField('array'): Use 'fields' (NOT 'template') to define the array item structure. ` +
10019
+ `Example: { type: 'array', key: '...', fields: [{ type: 'group', key: 'item', fields: [...] }] }`);
10020
+ }
10021
+ if ('minItems' in configWithProps || 'maxItems' in configWithProps) {
10022
+ throw new DynamicFormError(`createField('array'): 'minItems' and 'maxItems' properties are NOT supported. ` +
10023
+ `Use custom validation if you need to enforce array length constraints.`);
10024
+ }
10025
+ }
10026
+ // Generic button validation
10027
+ if (type === 'button') {
10028
+ if (!('event' in configWithProps) || configWithProps['event'] === undefined) {
10029
+ throw new DynamicFormError(`createField('button'): Generic buttons REQUIRE the 'event' property with a FormEventConstructor. ` +
10030
+ `For common actions, use 'submit', 'next', or 'previous' instead which don't require event configuration. ` +
10031
+ `Example: { type: 'submit', key: 'submit', label: 'Submit' }`);
10032
+ }
10033
+ }
10034
+ return { type, ...config };
10035
+ }
10036
+ /**
10037
+ * Shorthand alias for createField
10038
+ *
10039
+ * @example
10040
+ * ```typescript
10041
+ * const nameField = field('input', { key: 'name', label: 'Name', value: '' });
10042
+ * ```
10043
+ */
10044
+ const field = createField;
10045
+
6538
10046
  /**
6539
10047
  * Converts DynamicText (string | Observable | Signal) to Observable<string>
6540
10048
  * Unifies all three types into a consistent Observable stream
@@ -7281,49 +10789,65 @@ class ArrayFieldComponent {
7281
10789
  this.resolvedItemsSignal.set([]);
7282
10790
  break;
7283
10791
  case 'initial':
7284
- this.resolveAllItems(fieldTrees, updateId);
10792
+ void this.resolveAllItems(fieldTrees, updateId);
7285
10793
  break;
7286
10794
  case 'append':
7287
- this.appendItems(fieldTrees, operation.startIndex, operation.endIndex, updateId);
10795
+ void this.appendItems(fieldTrees, operation.startIndex, operation.endIndex, updateId);
7288
10796
  break;
7289
10797
  case 'pop':
7290
10798
  this.resolvedItemsSignal.set(resolvedItems.slice(0, operation.newLength));
7291
10799
  break;
7292
10800
  case 'recreate':
7293
10801
  this.resolvedItemsSignal.set([]);
7294
- this.resolveAllItems(fieldTrees, updateId);
10802
+ void this.resolveAllItems(fieldTrees, updateId);
7295
10803
  break;
7296
10804
  case 'none':
7297
10805
  break;
7298
10806
  }
7299
10807
  }
7300
- resolveAllItems(fieldTrees, updateId) {
10808
+ async resolveAllItems(fieldTrees, updateId) {
7301
10809
  if (fieldTrees.length === 0) {
7302
10810
  this.resolvedItemsSignal.set([]);
7303
10811
  return;
7304
10812
  }
7305
- const itemObservables = fieldTrees.map((fieldTree, i) => this.createResolveItemObservable(fieldTree, i));
7306
- forkJoin(itemObservables)
7307
- .pipe(takeUntilDestroyed(this.destroyRef), map((items) => items.filter((item) => item !== undefined)))
7308
- .subscribe((newItems) => {
10813
+ // Wrap each item observable to catch individual errors
10814
+ const safeItemObservables = fieldTrees.map((fieldTree, i) => this.createResolveItemObservable(fieldTree, i).pipe(catchError((error) => {
10815
+ this.logger.error(`Failed to resolve array item at index ${i}:`, error);
10816
+ return of(undefined);
10817
+ })));
10818
+ try {
10819
+ const items = await firstValueFrom(forkJoin(safeItemObservables).pipe(map((items) => items.filter((item) => item !== undefined))));
7309
10820
  if (updateId === this.updateVersion()) {
7310
- this.resolvedItemsSignal.set(newItems);
10821
+ this.resolvedItemsSignal.set(items);
7311
10822
  emitComponentInitialized(this.eventBus, 'array', this.field().key, this.parentInjector);
7312
10823
  }
7313
- });
10824
+ }
10825
+ catch (err) {
10826
+ this.logger.error('Failed to resolve array items:', err);
10827
+ this.resolvedItemsSignal.set([]);
10828
+ }
7314
10829
  }
7315
- appendItems(fieldTrees, startIndex, endIndex, updateId) {
10830
+ async appendItems(fieldTrees, startIndex, endIndex, updateId) {
7316
10831
  const itemsToResolve = fieldTrees.slice(startIndex, endIndex);
7317
10832
  if (itemsToResolve.length === 0)
7318
10833
  return;
7319
- const itemObservables = itemsToResolve.map((fieldTree, i) => this.createResolveItemObservable(fieldTree, startIndex + i));
7320
- forkJoin(itemObservables)
7321
- .pipe(takeUntilDestroyed(this.destroyRef), map((items) => items.filter((item) => item !== undefined)))
7322
- .subscribe((newItems) => {
10834
+ // Wrap each item observable to catch individual errors
10835
+ const safeItemObservables = itemsToResolve.map((fieldTree, i) => {
10836
+ const index = startIndex + i;
10837
+ return this.createResolveItemObservable(fieldTree, index).pipe(catchError((error) => {
10838
+ this.logger.error(`Failed to resolve array item at index ${index}:`, error);
10839
+ return of(undefined);
10840
+ }));
10841
+ });
10842
+ try {
10843
+ const newItems = await firstValueFrom(forkJoin(safeItemObservables).pipe(map((items) => items.filter((item) => item !== undefined))));
7323
10844
  if (updateId === this.updateVersion()) {
7324
10845
  this.resolvedItemsSignal.update((current) => [...current, ...newItems]);
7325
10846
  }
7326
- });
10847
+ }
10848
+ catch (err) {
10849
+ this.logger.error('Failed to append array items:', err);
10850
+ }
7327
10851
  }
7328
10852
  createResolveItemObservable(fieldTree, index) {
7329
10853
  const template = this.fieldTemplate();
@@ -7355,9 +10879,11 @@ class ArrayFieldComponent {
7355
10879
  const insertIndex = index !== undefined ? Math.min(index, currentArray.length) : currentArray.length;
7356
10880
  const rawValue = getFieldDefaultValue(fieldTemplate, this.rawFieldRegistry());
7357
10881
  const valueHandling = getFieldValueHandling(fieldTemplate.type, this.rawFieldRegistry());
7358
- // For fields with 'include' value handling (bare fields like input, checkbox, etc.),
7359
- // wrap the value in an object using the field's key since arrays contain objects
7360
- const value = valueHandling === 'include' && fieldTemplate.key ? { [fieldTemplate.key]: rawValue } : rawValue;
10882
+ const templateType = fieldTemplate.type;
10883
+ // Container fields (group, row) already have their own structure, so use rawValue as-is.
10884
+ // For value fields with 'include' handling, wrap in an object using the field's key.
10885
+ const isContainer = templateType === 'group' || templateType === 'row';
10886
+ const value = isContainer ? rawValue : valueHandling === 'include' && fieldTemplate.key ? { [fieldTemplate.key]: rawValue } : rawValue;
7361
10887
  const newArray = [...currentArray];
7362
10888
  newArray.splice(insertIndex, 0, value);
7363
10889
  // Update the parent form with the new array value
@@ -7535,10 +11061,6 @@ function extractErrorParams(error) {
7535
11061
  * - Field mappers (valueFieldMapper, checkboxFieldMapper, etc.)
7536
11062
  * - Error utilities (createResolvedErrorsSignal, shouldShowErrors)
7537
11063
  *
7538
- * ## Testing API
7539
- * Import from '@ng-forge/dynamic-forms/testing' for:
7540
- * - Test utilities and harnesses
7541
- * - Form config builders for testing
7542
11064
  */
7543
11065
  // ============================================================
7544
11066
  // PUBLIC API - For end users
@@ -7549,5 +11071,5 @@ function extractErrorParams(error) {
7549
11071
  * Generated bundle index. Do not edit.
7550
11072
  */
7551
11073
 
7552
- export { applyValidators as $, AddArrayItemEvent as A, baseFieldMapper as B, ConsoleLogger as C, DynamicFormLogger as D, EventBus as E, FormClearEvent as F, buildBaseInputs as G, arrayFieldMapper as H, groupFieldMapper as I, pageFieldMapper as J, rowFieldMapper as K, textFieldMapper as L, FIELD_REGISTRY as M, NoopLogger as N, ARRAY_CONTEXT as O, PageChangeEvent as P, DEFAULT_PROPS as Q, RemoveArrayItemEvent as R, SubmitEvent as S, DEFAULT_VALIDATION_MESSAGES as T, FIELD_SIGNAL_CONTEXT as U, FORM_OPTIONS as V, dynamicTextToObservable as W, ArrayFieldComponent as X, GroupFieldComponent as Y, RowFieldComponent as Z, applyValidator as _, DynamicTextPipe as a, getArrayLength as a0, resolveTokens as a1, BUILT_IN_FIELDS as a2, omit as a3, interpolateParams as a4, applyMetaToElement as a5, RootFormRegistryService as a6, DynamicForm as b, createFieldResolutionPipe as c, derivedFromDeferred as d, emitComponentInitialized as e, isFormStateCondition as f, resolveNextButtonDisabled as g, isCheckedField as h, injectFieldRegistry as i, isValueField as j, isRowField as k, isArrayField as l, isContainerField as m, isDisplayOnlyField as n, isGroupField as o, provideDynamicForm as p, isLeafField as q, resolveSubmitButtonDisabled as r, isPageField as s, isValueBearingField as t, FormResetEvent as u, validatePageNesting as v, withLoggerConfig as w, NextPageEvent as x, PreviousPageEvent as y, DynamicFormError as z };
7553
- //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-DHxoGouT.mjs.map
11074
+ export { dynamicTextToObservable as $, AddArrayItemEvent as A, PreviousPageEvent as B, ConsoleLogger as C, DynamicFormLogger as D, EventBus as E, FormClearEvent as F, createField as G, field as H, formConfig as I, DynamicFormError as J, baseFieldMapper as K, buildBaseInputs as L, arrayFieldMapper as M, NoopLogger as N, groupFieldMapper as O, PageChangeEvent as P, pageFieldMapper as Q, RemoveArrayItemEvent as R, SubmitEvent as S, rowFieldMapper as T, textFieldMapper as U, FIELD_REGISTRY as V, ARRAY_CONTEXT as W, DEFAULT_PROPS as X, DEFAULT_VALIDATION_MESSAGES as Y, FIELD_SIGNAL_CONTEXT as Z, FORM_OPTIONS as _, DynamicTextPipe as a, ArrayFieldComponent as a0, GroupFieldComponent as a1, RowFieldComponent as a2, applyValidator as a3, applyValidators as a4, getArrayLength as a5, resolveTokens as a6, BUILT_IN_FIELDS as a7, isEqual as a8, omit as a9, interpolateParams as aa, applyMetaToElement as ab, RootFormRegistryService as ac, DynamicForm as b, createFieldResolutionPipe as c, derivedFromDeferred as d, emitComponentInitialized as e, withEventFormValue as f, isFormStateCondition as g, hasFormValue as h, injectFieldRegistry as i, resolveNextButtonDisabled as j, isCheckedField as k, isValueField as l, isRowField as m, isArrayField as n, isContainerField as o, provideDynamicForm as p, isDisplayOnlyField as q, resolveSubmitButtonDisabled as r, isGroupField as s, isLeafField as t, isPageField as u, validatePageNesting as v, withLoggerConfig as w, isValueBearingField as x, FormResetEvent as y, NextPageEvent as z };
11075
+ //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-BWFdMPyw.mjs.map