@ng-forge/dynamic-forms 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +37 -66
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-D58rojQc.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-WGww2nXx.mjs} +3572 -346
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-WGww2nXx.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-m35XgkkL.mjs → ng-forge-dynamic-forms-page-field.component-CrO9WTDw.mjs} +5 -5
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-m35XgkkL.mjs.map → ng-forge-dynamic-forms-page-field.component-CrO9WTDw.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-schema.mjs +92 -0
- package/fesm2022/ng-forge-dynamic-forms-schema.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CfDZFTyf.mjs → ng-forge-dynamic-forms-text-field.component-BcFx_kOz.mjs} +5 -5
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CfDZFTyf.mjs.map → ng-forge-dynamic-forms-text-field.component-BcFx_kOz.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +13 -12
- package/types/ng-forge-dynamic-forms-integration.d.ts +61 -32
- package/types/ng-forge-dynamic-forms-schema.d.ts +178 -0
- package/types/ng-forge-dynamic-forms.d.ts +1173 -240
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-D58rojQc.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-0iXHLraC.mjs +0 -31
- package/fesm2022/ng-forge-dynamic-forms-testing-test-button.harness-0iXHLraC.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-testing.mjs +0 -971
- package/fesm2022/ng-forge-dynamic-forms-testing.mjs.map +0 -1
- package/types/ng-forge-dynamic-forms-testing.d.ts +0 -597
|
@@ -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,
|
|
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
|
|
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));
|
|
@@ -133,9 +136,10 @@ function buildClassName(fieldDef) {
|
|
|
133
136
|
* Used by mappers to build the inputs record.
|
|
134
137
|
*
|
|
135
138
|
* @param fieldDef The field definition to extract properties from
|
|
139
|
+
* @param defaultProps Optional form-level default props to merge with field props
|
|
136
140
|
* @returns Record of input names to values
|
|
137
141
|
*/
|
|
138
|
-
function buildBaseInputs(fieldDef) {
|
|
142
|
+
function buildBaseInputs(fieldDef, defaultProps) {
|
|
139
143
|
const { key, label, className, tabIndex, props, meta } = fieldDef;
|
|
140
144
|
const inputs = {};
|
|
141
145
|
// Always include key - required by components for accessibility and identification
|
|
@@ -158,8 +162,12 @@ function buildBaseInputs(fieldDef) {
|
|
|
158
162
|
if (tabIndex !== undefined) {
|
|
159
163
|
inputs['tabIndex'] = tabIndex;
|
|
160
164
|
}
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
// Merge props: form-level defaultProps as base, field-level props override
|
|
166
|
+
// Cast props to Record since it's typed as unknown but always represents an object at runtime
|
|
167
|
+
const fieldProps = props;
|
|
168
|
+
const mergedProps = fieldProps !== undefined || defaultProps !== undefined ? { ...(defaultProps ?? {}), ...(fieldProps ?? {}) } : undefined;
|
|
169
|
+
if (mergedProps !== undefined) {
|
|
170
|
+
inputs['props'] = mergedProps;
|
|
163
171
|
}
|
|
164
172
|
if (meta !== undefined) {
|
|
165
173
|
inputs['meta'] = meta;
|
|
@@ -404,6 +412,133 @@ function createSubmissionHandler(options) {
|
|
|
404
412
|
* Native JavaScript utility functions to replace lodash-es
|
|
405
413
|
* These are simple implementations for common object manipulation patterns
|
|
406
414
|
*/
|
|
415
|
+
/**
|
|
416
|
+
* Performs a deep equality comparison between two values.
|
|
417
|
+
*
|
|
418
|
+
* Handles:
|
|
419
|
+
* - Primitives (including NaN via Object.is)
|
|
420
|
+
* - Dates (by timestamp)
|
|
421
|
+
* - Arrays (deep element comparison)
|
|
422
|
+
* - Plain objects (deep property comparison)
|
|
423
|
+
* - RegExp (by source and flags)
|
|
424
|
+
* - Map and Set (by entries)
|
|
425
|
+
* - Circular references (via WeakMap tracking)
|
|
426
|
+
*
|
|
427
|
+
* @param a - First value
|
|
428
|
+
* @param b - Second value
|
|
429
|
+
* @returns true if values are deeply equal
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* ```typescript
|
|
433
|
+
* isEqual({ a: 1 }, { a: 1 }); // true
|
|
434
|
+
* isEqual([1, 2], [1, 2]); // true
|
|
435
|
+
* isEqual({ a: 1 }, { a: 2 }); // false
|
|
436
|
+
* isEqual(new Date('2024-01-01'), new Date('2024-01-01')); // true
|
|
437
|
+
* isEqual(/abc/gi, /abc/gi); // true
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
function isEqual(a, b) {
|
|
441
|
+
return isEqualInternal(a, b, new WeakMap(), new WeakMap());
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Internal implementation with circular reference tracking.
|
|
445
|
+
* @internal
|
|
446
|
+
*/
|
|
447
|
+
function isEqualInternal(a, b, seenA, seenB) {
|
|
448
|
+
// Same reference or both null/undefined
|
|
449
|
+
if (a === b)
|
|
450
|
+
return true;
|
|
451
|
+
// Handle NaN (NaN === NaN is false, but we want it to be equal)
|
|
452
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
453
|
+
return Object.is(a, b);
|
|
454
|
+
}
|
|
455
|
+
// One is null/undefined but not both
|
|
456
|
+
if (a == null || b == null)
|
|
457
|
+
return false;
|
|
458
|
+
// Different types
|
|
459
|
+
if (typeof a !== typeof b)
|
|
460
|
+
return false;
|
|
461
|
+
// Non-object primitives (strings, booleans, symbols, bigints)
|
|
462
|
+
if (typeof a !== 'object') {
|
|
463
|
+
return Object.is(a, b);
|
|
464
|
+
}
|
|
465
|
+
// At this point, both a and b are objects (typeof a === 'object')
|
|
466
|
+
const objA = a;
|
|
467
|
+
const objB = b;
|
|
468
|
+
// Check for circular references
|
|
469
|
+
if (seenA.has(objA) || seenB.has(objB)) {
|
|
470
|
+
// If we've seen these objects before, check if they were paired together
|
|
471
|
+
return seenA.get(objA) === objB && seenB.get(objB) === objA;
|
|
472
|
+
}
|
|
473
|
+
// Mark these objects as seen (paired together)
|
|
474
|
+
seenA.set(objA, objB);
|
|
475
|
+
seenB.set(objB, objA);
|
|
476
|
+
// Different constructors means different types (e.g., Date vs Object)
|
|
477
|
+
if (objA.constructor !== objB.constructor)
|
|
478
|
+
return false;
|
|
479
|
+
// Handle dates
|
|
480
|
+
if (a instanceof Date && b instanceof Date) {
|
|
481
|
+
return a.getTime() === b.getTime();
|
|
482
|
+
}
|
|
483
|
+
// Handle RegExp
|
|
484
|
+
if (a instanceof RegExp && b instanceof RegExp) {
|
|
485
|
+
return a.source === b.source && a.flags === b.flags;
|
|
486
|
+
}
|
|
487
|
+
// Handle Map
|
|
488
|
+
if (a instanceof Map && b instanceof Map) {
|
|
489
|
+
if (a.size !== b.size)
|
|
490
|
+
return false;
|
|
491
|
+
for (const [key, value] of a) {
|
|
492
|
+
if (!b.has(key) || !isEqualInternal(value, b.get(key), seenA, seenB)) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
// Handle Set
|
|
499
|
+
if (a instanceof Set && b instanceof Set) {
|
|
500
|
+
if (a.size !== b.size)
|
|
501
|
+
return false;
|
|
502
|
+
for (const value of a) {
|
|
503
|
+
if (!b.has(value)) {
|
|
504
|
+
// For non-primitive values in Sets, we need deep comparison
|
|
505
|
+
// This is O(n^2) but Sets rarely contain objects
|
|
506
|
+
if (typeof value === 'object' && value !== null) {
|
|
507
|
+
let found = false;
|
|
508
|
+
for (const bValue of b) {
|
|
509
|
+
if (typeof bValue === 'object' && bValue !== null && isEqualInternal(value, bValue, seenA, seenB)) {
|
|
510
|
+
found = true;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (!found)
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
// Handle arrays
|
|
525
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
526
|
+
if (a.length !== b.length)
|
|
527
|
+
return false;
|
|
528
|
+
return a.every((item, index) => isEqualInternal(item, b[index], seenA, seenB));
|
|
529
|
+
}
|
|
530
|
+
// Handle plain objects
|
|
531
|
+
const recordA = a;
|
|
532
|
+
const recordB = b;
|
|
533
|
+
// Get both string and symbol keys
|
|
534
|
+
const keysA = [...Object.keys(recordA), ...Object.getOwnPropertySymbols(recordA)];
|
|
535
|
+
const keysB = [...Object.keys(recordB), ...Object.getOwnPropertySymbols(recordB)];
|
|
536
|
+
if (keysA.length !== keysB.length)
|
|
537
|
+
return false;
|
|
538
|
+
// Create a Set for O(1) lookup
|
|
539
|
+
const keySetB = new Set(keysB);
|
|
540
|
+
return keysA.every((key) => keySetB.has(key) && isEqualInternal(recordA[key], recordB[key], seenA, seenB));
|
|
541
|
+
}
|
|
407
542
|
/**
|
|
408
543
|
* Creates a new object without the specified keys
|
|
409
544
|
* Native replacement for lodash omit()
|
|
@@ -511,54 +646,6 @@ function memoize(fn, resolverOrOptions) {
|
|
|
511
646
|
return result;
|
|
512
647
|
});
|
|
513
648
|
}
|
|
514
|
-
/**
|
|
515
|
-
* Performs a deep equality comparison between two values
|
|
516
|
-
* Native replacement for lodash isEqual()
|
|
517
|
-
*
|
|
518
|
-
* @param a - First value
|
|
519
|
-
* @param b - Second value
|
|
520
|
-
* @returns true if values are deeply equal
|
|
521
|
-
*
|
|
522
|
-
* @example
|
|
523
|
-
* ```typescript
|
|
524
|
-
* isEqual({ a: 1 }, { a: 1 }); // true
|
|
525
|
-
* isEqual([1, 2], [1, 2]); // true
|
|
526
|
-
* isEqual({ a: 1 }, { a: 2 }); // false
|
|
527
|
-
* ```
|
|
528
|
-
*/
|
|
529
|
-
function isEqual(a, b) {
|
|
530
|
-
// Same reference or both null/undefined
|
|
531
|
-
if (a === b)
|
|
532
|
-
return true;
|
|
533
|
-
// One is null/undefined but not both
|
|
534
|
-
if (a == null || b == null)
|
|
535
|
-
return false;
|
|
536
|
-
// Different types
|
|
537
|
-
if (typeof a !== typeof b)
|
|
538
|
-
return false;
|
|
539
|
-
// Handle dates
|
|
540
|
-
if (a instanceof Date && b instanceof Date) {
|
|
541
|
-
return a.getTime() === b.getTime();
|
|
542
|
-
}
|
|
543
|
-
// Handle arrays
|
|
544
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
545
|
-
if (a.length !== b.length)
|
|
546
|
-
return false;
|
|
547
|
-
return a.every((item, index) => isEqual(item, b[index]));
|
|
548
|
-
}
|
|
549
|
-
// Handle objects
|
|
550
|
-
if (typeof a === 'object' && typeof b === 'object') {
|
|
551
|
-
const objA = a;
|
|
552
|
-
const objB = b;
|
|
553
|
-
const keysA = Object.keys(objA);
|
|
554
|
-
const keysB = Object.keys(objB);
|
|
555
|
-
if (keysA.length !== keysB.length)
|
|
556
|
-
return false;
|
|
557
|
-
return keysA.every((key) => keysB.includes(key) && isEqual(objA[key], objB[key]));
|
|
558
|
-
}
|
|
559
|
-
// Primitives - use Object.is to handle NaN correctly (NaN === NaN is false, but Object.is(NaN, NaN) is true)
|
|
560
|
-
return Object.is(a, b);
|
|
561
|
-
}
|
|
562
649
|
/**
|
|
563
650
|
* Normalizes a fields collection to an array.
|
|
564
651
|
* Handles both array format and object format (keyed by field key).
|
|
@@ -569,6 +656,52 @@ function isEqual(a, b) {
|
|
|
569
656
|
function normalizeFieldsArray(fields) {
|
|
570
657
|
return Array.isArray(fields) ? [...fields] : Object.values(fields);
|
|
571
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Gets the keys that differ between two objects.
|
|
661
|
+
*
|
|
662
|
+
* Compares top-level keys and returns those whose values are different.
|
|
663
|
+
* Uses deep equality comparison for nested values.
|
|
664
|
+
*
|
|
665
|
+
* @param previous - Previous object state
|
|
666
|
+
* @param current - Current object state
|
|
667
|
+
* @returns Set of keys that have different values
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
* ```typescript
|
|
671
|
+
* const prev = { a: 1, b: 2, c: 3 };
|
|
672
|
+
* const curr = { a: 1, b: 5, d: 4 };
|
|
673
|
+
* getChangedKeys(prev, curr); // Set { 'b', 'c', 'd' }
|
|
674
|
+
* ```
|
|
675
|
+
*/
|
|
676
|
+
function getChangedKeys(previous, current) {
|
|
677
|
+
const changedKeys = new Set();
|
|
678
|
+
// Handle null/undefined cases
|
|
679
|
+
if (!previous && !current)
|
|
680
|
+
return changedKeys;
|
|
681
|
+
if (!previous) {
|
|
682
|
+
return new Set(Object.keys(current ?? {}));
|
|
683
|
+
}
|
|
684
|
+
if (!current) {
|
|
685
|
+
return new Set(Object.keys(previous));
|
|
686
|
+
}
|
|
687
|
+
// Collect all keys from both objects
|
|
688
|
+
const allKeys = new Set([...Object.keys(previous), ...Object.keys(current)]);
|
|
689
|
+
// Check each key for differences
|
|
690
|
+
for (const key of allKeys) {
|
|
691
|
+
const prevValue = previous[key];
|
|
692
|
+
const currValue = current[key];
|
|
693
|
+
// Key added or removed
|
|
694
|
+
if (!(key in previous) || !(key in current)) {
|
|
695
|
+
changedKeys.add(key);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
// Value changed
|
|
699
|
+
if (!isEqual(prevValue, currValue)) {
|
|
700
|
+
changedKeys.add(key);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return changedKeys;
|
|
704
|
+
}
|
|
572
705
|
|
|
573
706
|
/**
|
|
574
707
|
* Injection token for providing field signal context to mappers and components.
|
|
@@ -628,6 +761,82 @@ const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT', {
|
|
|
628
761
|
* ```
|
|
629
762
|
*/
|
|
630
763
|
const ARRAY_CONTEXT = new InjectionToken('ARRAY_CONTEXT');
|
|
764
|
+
/**
|
|
765
|
+
* Injection token for form-level default props.
|
|
766
|
+
*
|
|
767
|
+
* Default props are form-wide property defaults that are merged with field-level props.
|
|
768
|
+
* Field props take precedence over default props.
|
|
769
|
+
*
|
|
770
|
+
* Unlike FIELD_SIGNAL_CONTEXT which is re-provided by container components (Group, Array),
|
|
771
|
+
* DEFAULT_PROPS is provided ONCE at the DynamicForm level and inherited by all children
|
|
772
|
+
* via Angular's hierarchical injector.
|
|
773
|
+
*
|
|
774
|
+
* The token provides a Signal to enable reactivity - when config changes, mappers
|
|
775
|
+
* that read the signal inside their computed() will automatically update.
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* ```typescript
|
|
779
|
+
* // In a mapper function
|
|
780
|
+
* const defaultPropsSignal = inject(DEFAULT_PROPS);
|
|
781
|
+
*
|
|
782
|
+
* return computed(() => {
|
|
783
|
+
* const defaultProps = defaultPropsSignal?.(); // Read inside computed for reactivity
|
|
784
|
+
* const baseInputs = buildBaseInputs(fieldDef, defaultProps);
|
|
785
|
+
* // ...
|
|
786
|
+
* });
|
|
787
|
+
* ```
|
|
788
|
+
*/
|
|
789
|
+
const DEFAULT_PROPS = new InjectionToken('DEFAULT_PROPS');
|
|
790
|
+
/**
|
|
791
|
+
* Injection token for form-level default validation messages.
|
|
792
|
+
*
|
|
793
|
+
* Default validation messages act as fallbacks when fields have validation
|
|
794
|
+
* errors but no field-level `validationMessages` defined.
|
|
795
|
+
*
|
|
796
|
+
* Like DEFAULT_PROPS, this token is provided ONCE at the DynamicForm level
|
|
797
|
+
* and inherited by all children via Angular's hierarchical injector.
|
|
798
|
+
*
|
|
799
|
+
* The token provides a Signal to enable reactivity - when config changes, mappers
|
|
800
|
+
* that read the signal inside their computed() will automatically update.
|
|
801
|
+
*
|
|
802
|
+
* @example
|
|
803
|
+
* ```typescript
|
|
804
|
+
* // In a mapper or component
|
|
805
|
+
* const defaultMessagesSignal = inject(DEFAULT_VALIDATION_MESSAGES);
|
|
806
|
+
*
|
|
807
|
+
* return computed(() => {
|
|
808
|
+
* const defaultMessages = defaultMessagesSignal?.(); // Read inside computed for reactivity
|
|
809
|
+
* const message = defaultMessages?.required ?? 'Field is required';
|
|
810
|
+
* // ...
|
|
811
|
+
* });
|
|
812
|
+
* ```
|
|
813
|
+
*/
|
|
814
|
+
const DEFAULT_VALIDATION_MESSAGES = new InjectionToken('DEFAULT_VALIDATION_MESSAGES');
|
|
815
|
+
/**
|
|
816
|
+
* Injection token for form-level options.
|
|
817
|
+
*
|
|
818
|
+
* Form options control form-wide behavior including button disabled states.
|
|
819
|
+
* Used by button mappers to determine default disabled behavior.
|
|
820
|
+
*
|
|
821
|
+
* Like DEFAULT_PROPS, this token is provided ONCE at the DynamicForm level
|
|
822
|
+
* and inherited by all children via Angular's hierarchical injector.
|
|
823
|
+
*
|
|
824
|
+
* The token provides a Signal to enable reactivity - when config changes, mappers
|
|
825
|
+
* that read the signal inside their computed() will automatically update.
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* ```typescript
|
|
829
|
+
* // In a button mapper
|
|
830
|
+
* const formOptionsSignal = inject(FORM_OPTIONS);
|
|
831
|
+
*
|
|
832
|
+
* return computed(() => {
|
|
833
|
+
* const formOptions = formOptionsSignal?.(); // Read inside computed for reactivity
|
|
834
|
+
* const disableWhenInvalid = formOptions?.submitButton?.disableWhenInvalid ?? true;
|
|
835
|
+
* // ...
|
|
836
|
+
* });
|
|
837
|
+
* ```
|
|
838
|
+
*/
|
|
839
|
+
const FORM_OPTIONS = new InjectionToken('FORM_OPTIONS');
|
|
631
840
|
|
|
632
841
|
/**
|
|
633
842
|
* Base error class for all Dynamic Forms errors.
|
|
@@ -1014,10 +1223,10 @@ class RootFormRegistryService {
|
|
|
1014
1223
|
this._formValueSignals.set(new Map());
|
|
1015
1224
|
this.formContexts.clear();
|
|
1016
1225
|
}
|
|
1017
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0
|
|
1018
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0
|
|
1226
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RootFormRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1227
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RootFormRegistryService });
|
|
1019
1228
|
}
|
|
1020
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0
|
|
1229
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RootFormRegistryService, decorators: [{
|
|
1021
1230
|
type: Injectable
|
|
1022
1231
|
}] });
|
|
1023
1232
|
|
|
@@ -1141,10 +1350,10 @@ class FieldContextRegistryService {
|
|
|
1141
1350
|
logger: this.logger,
|
|
1142
1351
|
};
|
|
1143
1352
|
}
|
|
1144
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0
|
|
1145
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0
|
|
1353
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1354
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: FieldContextRegistryService });
|
|
1146
1355
|
}
|
|
1147
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0
|
|
1356
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: FieldContextRegistryService, decorators: [{
|
|
1148
1357
|
type: Injectable
|
|
1149
1358
|
}] });
|
|
1150
1359
|
|
|
@@ -1205,6 +1414,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
|
|
|
1205
1414
|
class FunctionRegistryService {
|
|
1206
1415
|
customFunctions = new Map();
|
|
1207
1416
|
customFunctionScopes = new Map();
|
|
1417
|
+
derivationFunctions = new Map();
|
|
1208
1418
|
validators = new Map();
|
|
1209
1419
|
asyncValidators = new Map();
|
|
1210
1420
|
httpValidators = new Map();
|
|
@@ -1263,6 +1473,56 @@ class FunctionRegistryService {
|
|
|
1263
1473
|
this.customFunctions.clear();
|
|
1264
1474
|
this.customFunctionScopes.clear();
|
|
1265
1475
|
}
|
|
1476
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1477
|
+
// Derivation Functions
|
|
1478
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1479
|
+
/**
|
|
1480
|
+
* Register a derivation function for value derivation logic.
|
|
1481
|
+
*
|
|
1482
|
+
* Derivation functions compute derived values and are called when a
|
|
1483
|
+
* `DerivationLogicConfig` references them by `functionName`.
|
|
1484
|
+
*
|
|
1485
|
+
* @param name - Unique identifier for the function
|
|
1486
|
+
* @param fn - Function that receives EvaluationContext and returns the derived value
|
|
1487
|
+
*
|
|
1488
|
+
* @example
|
|
1489
|
+
* ```typescript
|
|
1490
|
+
* registry.registerDerivationFunction('getCurrencyForCountry', (ctx) => {
|
|
1491
|
+
* const countryToCurrency = { 'USA': 'USD', 'Germany': 'EUR', 'UK': 'GBP' };
|
|
1492
|
+
* return countryToCurrency[ctx.formValue.country] ?? 'USD';
|
|
1493
|
+
* });
|
|
1494
|
+
* ```
|
|
1495
|
+
*/
|
|
1496
|
+
registerDerivationFunction(name, fn) {
|
|
1497
|
+
this.derivationFunctions.set(name, fn);
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Get a derivation function by name
|
|
1501
|
+
*/
|
|
1502
|
+
getDerivationFunction(name) {
|
|
1503
|
+
return this.derivationFunctions.get(name);
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Get all derivation functions as an object
|
|
1507
|
+
*/
|
|
1508
|
+
getDerivationFunctions() {
|
|
1509
|
+
return Object.fromEntries(this.derivationFunctions);
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Set derivation functions from a config object.
|
|
1513
|
+
* Only updates functions if their references have changed.
|
|
1514
|
+
*
|
|
1515
|
+
* @param derivations - Object mapping function names to derivation functions
|
|
1516
|
+
*/
|
|
1517
|
+
setDerivationFunctions(derivations) {
|
|
1518
|
+
this.setRegistryIfChanged(this.derivationFunctions, derivations);
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Clear all derivation functions
|
|
1522
|
+
*/
|
|
1523
|
+
clearDerivationFunctions() {
|
|
1524
|
+
this.derivationFunctions.clear();
|
|
1525
|
+
}
|
|
1266
1526
|
/**
|
|
1267
1527
|
* Register a custom validator using Angular's public FieldContext API
|
|
1268
1528
|
*
|
|
@@ -1480,14 +1740,15 @@ class FunctionRegistryService {
|
|
|
1480
1740
|
*/
|
|
1481
1741
|
clearAll() {
|
|
1482
1742
|
this.clearCustomFunctions();
|
|
1743
|
+
this.clearDerivationFunctions();
|
|
1483
1744
|
this.clearValidators();
|
|
1484
1745
|
this.clearAsyncValidators();
|
|
1485
1746
|
this.clearHttpValidators();
|
|
1486
1747
|
}
|
|
1487
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0
|
|
1488
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0
|
|
1748
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1749
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: FunctionRegistryService });
|
|
1489
1750
|
}
|
|
1490
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0
|
|
1751
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: FunctionRegistryService, decorators: [{
|
|
1491
1752
|
type: Injectable
|
|
1492
1753
|
}] });
|
|
1493
1754
|
|
|
@@ -2323,7 +2584,7 @@ class Evaluator {
|
|
|
2323
2584
|
/**
|
|
2324
2585
|
* Maximum cache size to prevent memory issues
|
|
2325
2586
|
*/
|
|
2326
|
-
const MAX_AST_CACHE_SIZE = 1000;
|
|
2587
|
+
const MAX_AST_CACHE_SIZE$1 = 1000;
|
|
2327
2588
|
/**
|
|
2328
2589
|
* LRU Cache implementation for AST nodes
|
|
2329
2590
|
* Tracks access order to evict least-recently-used entries
|
|
@@ -2368,7 +2629,7 @@ class LRUCache {
|
|
|
2368
2629
|
/**
|
|
2369
2630
|
* Cache for parsed AST nodes to improve performance
|
|
2370
2631
|
*/
|
|
2371
|
-
const astCache = new LRUCache(MAX_AST_CACHE_SIZE);
|
|
2632
|
+
const astCache = new LRUCache(MAX_AST_CACHE_SIZE$1);
|
|
2372
2633
|
/**
|
|
2373
2634
|
* Secure expression parser that uses AST-based evaluation instead of eval() or new Function()
|
|
2374
2635
|
*
|
|
@@ -2425,7 +2686,7 @@ class ExpressionParser {
|
|
|
2425
2686
|
static getCacheStats() {
|
|
2426
2687
|
return {
|
|
2427
2688
|
size: astCache.size,
|
|
2428
|
-
maxSize: MAX_AST_CACHE_SIZE,
|
|
2689
|
+
maxSize: MAX_AST_CACHE_SIZE$1,
|
|
2429
2690
|
};
|
|
2430
2691
|
}
|
|
2431
2692
|
}
|
|
@@ -2593,6 +2854,105 @@ function createLogicFunction(expression) {
|
|
|
2593
2854
|
contextCache.set(cacheKey, fn);
|
|
2594
2855
|
return fn;
|
|
2595
2856
|
}
|
|
2857
|
+
/**
|
|
2858
|
+
* Cache for debounced logic functions.
|
|
2859
|
+
* Keyed by injection context similar to logicFunctionCache, but includes debounceMs in the key.
|
|
2860
|
+
*/
|
|
2861
|
+
const debouncedLogicFunctionCache = new WeakMap();
|
|
2862
|
+
/**
|
|
2863
|
+
* Gets or creates the cache map for debounced functions in a specific injection context.
|
|
2864
|
+
*/
|
|
2865
|
+
function getDebouncedContextCache(ctx) {
|
|
2866
|
+
let outerCache = debouncedLogicFunctionCache.get(ctx.functionRegistry);
|
|
2867
|
+
if (!outerCache) {
|
|
2868
|
+
outerCache = new WeakMap();
|
|
2869
|
+
debouncedLogicFunctionCache.set(ctx.functionRegistry, outerCache);
|
|
2870
|
+
}
|
|
2871
|
+
let innerCache = outerCache.get(ctx.fieldContextRegistry);
|
|
2872
|
+
if (!innerCache) {
|
|
2873
|
+
innerCache = new Map();
|
|
2874
|
+
outerCache.set(ctx.fieldContextRegistry, innerCache);
|
|
2875
|
+
}
|
|
2876
|
+
return innerCache;
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Store for debounced signals per field context.
|
|
2880
|
+
* Uses WeakMap with FieldContext objects as keys to automatically clean up
|
|
2881
|
+
* when the context is garbage collected.
|
|
2882
|
+
*/
|
|
2883
|
+
const debouncedSignalStore = new WeakMap();
|
|
2884
|
+
/**
|
|
2885
|
+
* Gets or creates the debounced signal store for a function registry.
|
|
2886
|
+
*/
|
|
2887
|
+
function getDebouncedSignalStore(functionRegistry) {
|
|
2888
|
+
let store = debouncedSignalStore.get(functionRegistry);
|
|
2889
|
+
if (!store) {
|
|
2890
|
+
store = new WeakMap();
|
|
2891
|
+
debouncedSignalStore.set(functionRegistry, store);
|
|
2892
|
+
}
|
|
2893
|
+
return store;
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Create a debounced logic function from a conditional expression.
|
|
2897
|
+
*
|
|
2898
|
+
* This function wraps the condition evaluation in a debounce mechanism,
|
|
2899
|
+
* so state changes only take effect after the specified delay.
|
|
2900
|
+
* Useful for avoiding UI flicker during rapid typing.
|
|
2901
|
+
*
|
|
2902
|
+
* @param expression The conditional expression to evaluate
|
|
2903
|
+
* @param debounceMs The debounce duration in milliseconds
|
|
2904
|
+
* @returns A LogicFn that evaluates the condition with debouncing
|
|
2905
|
+
*/
|
|
2906
|
+
function createDebouncedLogicFunction(expression, debounceMs) {
|
|
2907
|
+
// Inject services during factory creation, not during execution
|
|
2908
|
+
const functionRegistry = inject(FunctionRegistryService);
|
|
2909
|
+
const fieldContextRegistry = inject(FieldContextRegistryService);
|
|
2910
|
+
const injector = inject(Injector);
|
|
2911
|
+
// Get cache for this injection context
|
|
2912
|
+
const contextCache = getDebouncedContextCache({ functionRegistry, fieldContextRegistry });
|
|
2913
|
+
// Generate cache key including debounceMs
|
|
2914
|
+
const cacheKey = `${serializeExpression(expression)}:${debounceMs}`;
|
|
2915
|
+
// Check cache first
|
|
2916
|
+
const cached = contextCache.get(cacheKey);
|
|
2917
|
+
if (cached) {
|
|
2918
|
+
return cached;
|
|
2919
|
+
}
|
|
2920
|
+
// Get or create the signal store for this context
|
|
2921
|
+
const signalStore = getDebouncedSignalStore(functionRegistry);
|
|
2922
|
+
const fn = (ctx) => {
|
|
2923
|
+
// Create a unique key using the context object itself as identifier
|
|
2924
|
+
// Since FieldContext doesn't have a path property, we use a WeakMap approach
|
|
2925
|
+
// by storing the debounced signal directly on the context
|
|
2926
|
+
const contextKey = ctx;
|
|
2927
|
+
let signalPair = signalStore.get(contextKey);
|
|
2928
|
+
if (!signalPair) {
|
|
2929
|
+
// Create a signal to hold the immediate evaluation result
|
|
2930
|
+
const immediateValue = signal(false, ...(ngDevMode ? [{ debugName: "immediateValue" }] : []));
|
|
2931
|
+
// Create a debounced signal using RxJS
|
|
2932
|
+
const immediateValue$ = toObservable(immediateValue, { injector }).pipe(debounceTime(debounceMs), distinctUntilChanged(), startWith(false));
|
|
2933
|
+
const debouncedValue = toSignal(immediateValue$, { injector, initialValue: false });
|
|
2934
|
+
signalPair = { immediateValue, debouncedValue };
|
|
2935
|
+
signalStore.set(contextKey, signalPair);
|
|
2936
|
+
}
|
|
2937
|
+
// Create REACTIVE evaluation context for logic functions
|
|
2938
|
+
const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());
|
|
2939
|
+
// Evaluate the condition and update the immediate signal
|
|
2940
|
+
const result = evaluateCondition(expression, evaluationContext);
|
|
2941
|
+
// Update immediate value (this triggers the debounce chain)
|
|
2942
|
+
// signalPair is guaranteed to exist here due to the initialization above
|
|
2943
|
+
const { immediateValue, debouncedValue } = signalPair;
|
|
2944
|
+
untracked(() => {
|
|
2945
|
+
if (immediateValue() !== result) {
|
|
2946
|
+
immediateValue.set(result);
|
|
2947
|
+
}
|
|
2948
|
+
});
|
|
2949
|
+
// Return the debounced value
|
|
2950
|
+
return debouncedValue() ?? false;
|
|
2951
|
+
};
|
|
2952
|
+
// Cache the function
|
|
2953
|
+
contextCache.set(cacheKey, fn);
|
|
2954
|
+
return fn;
|
|
2955
|
+
}
|
|
2596
2956
|
|
|
2597
2957
|
/**
|
|
2598
2958
|
* Cache for memoized dynamic value functions, keyed by service instance.
|
|
@@ -2640,6 +3000,36 @@ function createDynamicValueFunction(expression) {
|
|
|
2640
3000
|
return fn;
|
|
2641
3001
|
}
|
|
2642
3002
|
|
|
3003
|
+
/**
|
|
3004
|
+
* Creates the default derivation log configuration.
|
|
3005
|
+
*
|
|
3006
|
+
* Defaults to 'none' (silent). Users can enable logging via `withLoggerConfig`.
|
|
3007
|
+
*
|
|
3008
|
+
* @returns Default DerivationLogConfig
|
|
3009
|
+
*
|
|
3010
|
+
* @public
|
|
3011
|
+
*/
|
|
3012
|
+
function createDefaultDerivationLogConfig() {
|
|
3013
|
+
return {
|
|
3014
|
+
level: 'none',
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
/**
|
|
3018
|
+
* Checks if logging should occur at the specified level.
|
|
3019
|
+
*
|
|
3020
|
+
* @param config - Current log configuration
|
|
3021
|
+
* @param minLevel - Minimum level required for logging
|
|
3022
|
+
* @returns True if logging should occur
|
|
3023
|
+
*
|
|
3024
|
+
* @public
|
|
3025
|
+
*/
|
|
3026
|
+
function shouldLog(config, minLevel) {
|
|
3027
|
+
if (config.level === 'none')
|
|
3028
|
+
return false;
|
|
3029
|
+
if (minLevel === 'summary')
|
|
3030
|
+
return config.level === 'summary' || config.level === 'verbose';
|
|
3031
|
+
return config.level === 'verbose';
|
|
3032
|
+
}
|
|
2643
3033
|
/**
|
|
2644
3034
|
* Type guard to check if a condition is a FormStateCondition.
|
|
2645
3035
|
*
|
|
@@ -2651,6 +3041,28 @@ function createDynamicValueFunction(expression) {
|
|
|
2651
3041
|
function isFormStateCondition(condition) {
|
|
2652
3042
|
return typeof condition === 'string' && ['formInvalid', 'formSubmitting', 'pageInvalid'].includes(condition);
|
|
2653
3043
|
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Type guard to check if a logic config is a StateLogicConfig.
|
|
3046
|
+
*
|
|
3047
|
+
* @param config - The logic config to check
|
|
3048
|
+
* @returns true if the config is for field state logic
|
|
3049
|
+
*
|
|
3050
|
+
* @public
|
|
3051
|
+
*/
|
|
3052
|
+
function isStateLogicConfig(config) {
|
|
3053
|
+
return config.type === 'hidden' || config.type === 'readonly' || config.type === 'disabled' || config.type === 'required';
|
|
3054
|
+
}
|
|
3055
|
+
/**
|
|
3056
|
+
* Type guard to check if a logic config is a DerivationLogicConfig.
|
|
3057
|
+
*
|
|
3058
|
+
* @param config - The logic config to check
|
|
3059
|
+
* @returns true if the config is for value derivation
|
|
3060
|
+
*
|
|
3061
|
+
* @public
|
|
3062
|
+
*/
|
|
3063
|
+
function isDerivationLogicConfig(config) {
|
|
3064
|
+
return config.type === 'derivation';
|
|
3065
|
+
}
|
|
2654
3066
|
|
|
2655
3067
|
/**
|
|
2656
3068
|
* Regular expression to detect formValue property access in expressions.
|
|
@@ -2659,10 +3071,19 @@ function isFormStateCondition(condition) {
|
|
|
2659
3071
|
const FORM_VALUE_ACCESS_PATTERN = /\bformValue\s*(?:\.|\[)/;
|
|
2660
3072
|
/**
|
|
2661
3073
|
* Regular expressions to extract field paths from formValue expressions.
|
|
3074
|
+
*
|
|
3075
|
+
* Enhanced to capture full nested paths including dot notation chains:
|
|
3076
|
+
* - formValue.fieldName → captures 'fieldName'
|
|
3077
|
+
* - formValue.parent.child.grandchild → captures 'parent.child.grandchild'
|
|
3078
|
+
* - formValue['field-name'] → captures 'field-name'
|
|
3079
|
+
* - formValue["field.with.dots"] → captures 'field.with.dots'
|
|
3080
|
+
*
|
|
3081
|
+
* Note: Computed property access (formValue[variableName]) is NOT supported
|
|
3082
|
+
* and must use explicit dependsOn configuration.
|
|
2662
3083
|
*/
|
|
2663
|
-
const FORM_VALUE_DOT_PATTERN = /\bformValue\.(\w+)/g;
|
|
2664
|
-
const FORM_VALUE_BRACKET_SINGLE_PATTERN = /\bformValue\s*\[\s*'(\w+)'\s*\]/g;
|
|
2665
|
-
const FORM_VALUE_BRACKET_DOUBLE_PATTERN = /\bformValue\s*\[\s*"(\w+)"\s*\]/g;
|
|
3084
|
+
const FORM_VALUE_DOT_PATTERN = /\bformValue\.([\w.]+)/g;
|
|
3085
|
+
const FORM_VALUE_BRACKET_SINGLE_PATTERN = /\bformValue\s*\[\s*'([\w.-]+)'\s*\]/g;
|
|
3086
|
+
const FORM_VALUE_BRACKET_DOUBLE_PATTERN = /\bformValue\s*\[\s*"([\w.-]+)"\s*\]/g;
|
|
2666
3087
|
/**
|
|
2667
3088
|
* Detects if a ConditionalExpression references other fields (cross-field).
|
|
2668
3089
|
*
|
|
@@ -2770,22 +3191,45 @@ function extractStringDependencies(expression) {
|
|
|
2770
3191
|
extractFromExpressionString(expression, deps);
|
|
2771
3192
|
return Array.from(deps);
|
|
2772
3193
|
}
|
|
2773
|
-
/**
|
|
3194
|
+
/**
|
|
3195
|
+
* Internal helper that populates a Set with dependencies from an expression string.
|
|
3196
|
+
*
|
|
3197
|
+
* Extracts both root fields and full nested paths for precise dependency tracking:
|
|
3198
|
+
* - 'formValue.parent.child' → adds 'parent' (root) and 'parent.child' (full path)
|
|
3199
|
+
* - 'formValue.simple' → adds 'simple'
|
|
3200
|
+
*/
|
|
2774
3201
|
function extractFromExpressionString(expression, deps) {
|
|
2775
|
-
// Extract from dot notation: formValue.fieldName
|
|
3202
|
+
// Extract from dot notation: formValue.fieldName or formValue.parent.child.grandchild
|
|
2776
3203
|
const dotMatches = expression.matchAll(FORM_VALUE_DOT_PATTERN);
|
|
2777
3204
|
for (const match of dotMatches) {
|
|
2778
|
-
|
|
3205
|
+
const fullPath = match[1];
|
|
3206
|
+
// Always add the root field (first segment) as the primary dependency
|
|
3207
|
+
const rootField = fullPath.split('.')[0];
|
|
3208
|
+
deps.add(rootField);
|
|
3209
|
+
// Also add the full path for more precise dependency tracking if nested
|
|
3210
|
+
if (fullPath.includes('.')) {
|
|
3211
|
+
deps.add(fullPath);
|
|
3212
|
+
}
|
|
2779
3213
|
}
|
|
2780
3214
|
// Extract from bracket notation with single quotes: formValue['fieldName']
|
|
2781
3215
|
const bracketSingleMatches = expression.matchAll(FORM_VALUE_BRACKET_SINGLE_PATTERN);
|
|
2782
3216
|
for (const match of bracketSingleMatches) {
|
|
2783
|
-
|
|
3217
|
+
const fullPath = match[1];
|
|
3218
|
+
const rootField = fullPath.split('.')[0];
|
|
3219
|
+
deps.add(rootField);
|
|
3220
|
+
if (fullPath.includes('.')) {
|
|
3221
|
+
deps.add(fullPath);
|
|
3222
|
+
}
|
|
2784
3223
|
}
|
|
2785
3224
|
// Extract from bracket notation with double quotes: formValue["fieldName"]
|
|
2786
3225
|
const bracketDoubleMatches = expression.matchAll(FORM_VALUE_BRACKET_DOUBLE_PATTERN);
|
|
2787
3226
|
for (const match of bracketDoubleMatches) {
|
|
2788
|
-
|
|
3227
|
+
const fullPath = match[1];
|
|
3228
|
+
const rootField = fullPath.split('.')[0];
|
|
3229
|
+
deps.add(rootField);
|
|
3230
|
+
if (fullPath.includes('.')) {
|
|
3231
|
+
deps.add(fullPath);
|
|
3232
|
+
}
|
|
2789
3233
|
}
|
|
2790
3234
|
}
|
|
2791
3235
|
// ============================================================================
|
|
@@ -2839,20 +3283,40 @@ function hasCrossFieldWhenCondition(config, context) {
|
|
|
2839
3283
|
// Logic Detection
|
|
2840
3284
|
// ============================================================================
|
|
2841
3285
|
/**
|
|
2842
|
-
* Detects if a
|
|
3286
|
+
* Detects if a StateLogicConfig has a cross-field condition.
|
|
2843
3287
|
*
|
|
2844
|
-
*
|
|
3288
|
+
* Note: This function only handles state logic (hidden, readonly, disabled, required).
|
|
3289
|
+
* Derivation logic is handled separately by the derivation system.
|
|
3290
|
+
*
|
|
3291
|
+
* @param config The state logic configuration to check
|
|
2845
3292
|
* @param context Optional context providing function scope lookup
|
|
2846
3293
|
* @returns true if the logic condition references other fields
|
|
2847
3294
|
*/
|
|
2848
|
-
function
|
|
3295
|
+
function isCrossFieldStateLogic(config, context) {
|
|
2849
3296
|
return isCrossFieldExpression(config.condition, context);
|
|
2850
3297
|
}
|
|
2851
|
-
// ============================================================================
|
|
2852
|
-
// Schema Detection
|
|
2853
|
-
// ============================================================================
|
|
2854
3298
|
/**
|
|
2855
|
-
* Detects if a
|
|
3299
|
+
* Detects if a LogicConfig has a cross-field condition.
|
|
3300
|
+
*
|
|
3301
|
+
* @param config The logic configuration to check
|
|
3302
|
+
* @param context Optional context providing function scope lookup
|
|
3303
|
+
* @returns true if the logic condition references other fields
|
|
3304
|
+
*
|
|
3305
|
+
* @deprecated Use `isCrossFieldStateLogic` for state logic configs.
|
|
3306
|
+
* Derivation logic is handled by the derivation system.
|
|
3307
|
+
*/
|
|
3308
|
+
function isCrossFieldLogic(config, context) {
|
|
3309
|
+
// Only check state logic configs
|
|
3310
|
+
if (!isStateLogicConfig(config)) {
|
|
3311
|
+
return false;
|
|
3312
|
+
}
|
|
3313
|
+
return isCrossFieldExpression(config.condition, context);
|
|
3314
|
+
}
|
|
3315
|
+
// ============================================================================
|
|
3316
|
+
// Schema Detection
|
|
3317
|
+
// ============================================================================
|
|
3318
|
+
/**
|
|
3319
|
+
* Detects if a SchemaApplicationConfig has a cross-field condition.
|
|
2856
3320
|
*
|
|
2857
3321
|
* @param config The schema application configuration to check
|
|
2858
3322
|
* @param context Optional context providing function scope lookup
|
|
@@ -3108,13 +3572,111 @@ function applyValidators(configs, fieldPath) {
|
|
|
3108
3572
|
configs.forEach((config) => applyValidator(config, fieldPath));
|
|
3109
3573
|
}
|
|
3110
3574
|
|
|
3575
|
+
/**
|
|
3576
|
+
* Default debounce duration in milliseconds.
|
|
3577
|
+
*/
|
|
3578
|
+
const DEFAULT_DEBOUNCE_MS = 500;
|
|
3579
|
+
/**
|
|
3580
|
+
* Creates a debounced effect that triggers a callback after a signal's value
|
|
3581
|
+
* has stabilized for the specified duration.
|
|
3582
|
+
*
|
|
3583
|
+
* Uses Angular's `toObservable` and `toSignal` with RxJS `debounceTime` for proper debouncing.
|
|
3584
|
+
* This approach is cleaner than manual setTimeout management and integrates
|
|
3585
|
+
* well with Angular's signal-based reactivity.
|
|
3586
|
+
*
|
|
3587
|
+
* @param source - The source signal to watch for changes
|
|
3588
|
+
* @param callback - Function to call with the debounced value
|
|
3589
|
+
* @param options - Configuration options
|
|
3590
|
+
* @param options.ms - Debounce duration in milliseconds (default: 500)
|
|
3591
|
+
* @param options.injector - Angular injector for the effect context
|
|
3592
|
+
*
|
|
3593
|
+
* @returns A signal that emits the debounced value (can be ignored if only side effects are needed)
|
|
3594
|
+
*
|
|
3595
|
+
* @example
|
|
3596
|
+
* ```typescript
|
|
3597
|
+
* // In a component or service
|
|
3598
|
+
* const searchInput = signal('');
|
|
3599
|
+
*
|
|
3600
|
+
* createDebouncedEffect(
|
|
3601
|
+
* searchInput,
|
|
3602
|
+
* (value) => console.log('Debounced search:', value),
|
|
3603
|
+
* { ms: 300, injector: this.injector }
|
|
3604
|
+
* );
|
|
3605
|
+
* ```
|
|
3606
|
+
*
|
|
3607
|
+
* @public
|
|
3608
|
+
*/
|
|
3609
|
+
function createDebouncedEffect(source, callback, options) {
|
|
3610
|
+
const { ms = DEFAULT_DEBOUNCE_MS, injector } = options;
|
|
3611
|
+
const source$ = toObservable(source, { injector }).pipe(debounceTime(ms), distinctUntilChanged(), tap((value) => callback(value)));
|
|
3612
|
+
return toSignal(source$, { injector });
|
|
3613
|
+
}
|
|
3614
|
+
/**
|
|
3615
|
+
* Creates a debounced signal that emits values from the source signal
|
|
3616
|
+
* only after the value has stabilized for the specified duration.
|
|
3617
|
+
*
|
|
3618
|
+
* Unlike `createDebouncedEffect`, this doesn't execute a side effect callback.
|
|
3619
|
+
* Use this when you just need a debounced version of a signal.
|
|
3620
|
+
*
|
|
3621
|
+
* @param source - The source signal to debounce
|
|
3622
|
+
* @param options - Configuration options
|
|
3623
|
+
* @param options.ms - Debounce duration in milliseconds (default: 500)
|
|
3624
|
+
* @param options.injector - Angular injector for the effect context
|
|
3625
|
+
*
|
|
3626
|
+
* @returns A signal that emits the debounced value
|
|
3627
|
+
*
|
|
3628
|
+
* @example
|
|
3629
|
+
* ```typescript
|
|
3630
|
+
* const input = signal('');
|
|
3631
|
+
* const debouncedInput = createDebouncedSignal(input, { ms: 300, injector });
|
|
3632
|
+
*
|
|
3633
|
+
* effect(() => {
|
|
3634
|
+
* console.log('Debounced value:', debouncedInput());
|
|
3635
|
+
* });
|
|
3636
|
+
* ```
|
|
3637
|
+
*
|
|
3638
|
+
* @public
|
|
3639
|
+
*/
|
|
3640
|
+
function createDebouncedSignal(source, options) {
|
|
3641
|
+
const { ms = DEFAULT_DEBOUNCE_MS, injector } = options;
|
|
3642
|
+
const source$ = toObservable(source, { injector }).pipe(debounceTime(ms), distinctUntilChanged());
|
|
3643
|
+
return toSignal(source$, { injector });
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
/**
|
|
3647
|
+
* Extracts the trigger from a StateLogicConfig, handling the discriminated union.
|
|
3648
|
+
*/
|
|
3649
|
+
function getConfigTrigger(config) {
|
|
3650
|
+
if (!isStateLogicConfig(config)) {
|
|
3651
|
+
return 'onChange';
|
|
3652
|
+
}
|
|
3653
|
+
return config.trigger ?? 'onChange';
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Extracts debounceMs from a StateLogicConfig if trigger is 'debounced'.
|
|
3657
|
+
*/
|
|
3658
|
+
function getConfigDebounceMs(config) {
|
|
3659
|
+
if (!isStateLogicConfig(config)) {
|
|
3660
|
+
return undefined;
|
|
3661
|
+
}
|
|
3662
|
+
// Type narrowing: debounceMs only exists when trigger is 'debounced'
|
|
3663
|
+
if (config.trigger === 'debounced') {
|
|
3664
|
+
return config.debounceMs;
|
|
3665
|
+
}
|
|
3666
|
+
return undefined;
|
|
3667
|
+
}
|
|
3111
3668
|
function applyLogic(config, fieldPath) {
|
|
3112
3669
|
const path = fieldPath;
|
|
3670
|
+
const trigger = getConfigTrigger(config);
|
|
3671
|
+
const debounceMs = getConfigDebounceMs(config) ?? DEFAULT_DEBOUNCE_MS;
|
|
3113
3672
|
if (typeof config.condition === 'boolean') {
|
|
3114
3673
|
applyLogicFn(config.type, path, () => config.condition);
|
|
3115
3674
|
return;
|
|
3116
3675
|
}
|
|
3117
|
-
|
|
3676
|
+
// Create appropriate logic function based on trigger
|
|
3677
|
+
const logicFn = trigger === 'debounced'
|
|
3678
|
+
? createDebouncedLogicFunction(config.condition, debounceMs)
|
|
3679
|
+
: createLogicFunction(config.condition);
|
|
3118
3680
|
applyLogicFn(config.type, path, logicFn);
|
|
3119
3681
|
}
|
|
3120
3682
|
function applyLogicFn(type, path, logicFn) {
|
|
@@ -3172,10 +3734,10 @@ class SchemaRegistryService {
|
|
|
3172
3734
|
}
|
|
3173
3735
|
return schema;
|
|
3174
3736
|
}
|
|
3175
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0
|
|
3176
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0
|
|
3737
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SchemaRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3738
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SchemaRegistryService });
|
|
3177
3739
|
}
|
|
3178
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0
|
|
3740
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SchemaRegistryService, decorators: [{
|
|
3179
3741
|
type: Injectable
|
|
3180
3742
|
}] });
|
|
3181
3743
|
|
|
@@ -3740,6 +4302,69 @@ function isDisplayOnlyField(field) {
|
|
|
3740
4302
|
return field.type === 'text' || isContainerField(field);
|
|
3741
4303
|
}
|
|
3742
4304
|
|
|
4305
|
+
/**
|
|
4306
|
+
* Applies a form-level schema validation to a schema path.
|
|
4307
|
+
* Supports both Standard Schema (Zod, Valibot, ArkType) and raw Angular schema callbacks.
|
|
4308
|
+
*
|
|
4309
|
+
* This is a helper function used internally by `createSchemaFromFields`
|
|
4310
|
+
* to apply form-level validation after field-level validation.
|
|
4311
|
+
*
|
|
4312
|
+
* @typeParam TModel - The form value type
|
|
4313
|
+
* @param path - The schema path to validate (from Angular's schema callback)
|
|
4314
|
+
* @param formLevelSchema - Form-level schema (Standard Schema marker or Angular callback)
|
|
4315
|
+
*
|
|
4316
|
+
* @remarks
|
|
4317
|
+
* Type assertions are required due to Angular's complex generic constraints:
|
|
4318
|
+
* - `validateStandardSchema` uses `IgnoreUnknownProperties<TSchema>` to accommodate Zod's strict types
|
|
4319
|
+
* - `SchemaPathTree` includes `SchemaPath` via conditional types that TypeScript can't narrow
|
|
4320
|
+
* These assertions are safe because the path originates from Angular's schema() function.
|
|
4321
|
+
*
|
|
4322
|
+
* @internal
|
|
4323
|
+
*/
|
|
4324
|
+
function applyFormLevelSchema(path, formLevelSchema) {
|
|
4325
|
+
if (isStandardSchemaMarker(formLevelSchema)) {
|
|
4326
|
+
// Standard Schema (Zod, Valibot, ArkType, etc.)
|
|
4327
|
+
// Cast via Parameters: validateStandardSchema's IgnoreUnknownProperties<TSchema> creates type incompatibility
|
|
4328
|
+
// that TypeScript cannot resolve. The cast is safe as path comes from Angular's schema() callback.
|
|
4329
|
+
validateStandardSchema(path, formLevelSchema.schema);
|
|
4330
|
+
}
|
|
4331
|
+
else if (typeof formLevelSchema === 'function') {
|
|
4332
|
+
// Angular schema callback - execute directly with the path
|
|
4333
|
+
// Cast via Function: AngularSchemaCallback expects intersection type SchemaPath & SchemaPathTree.
|
|
4334
|
+
// SchemaPathTree structurally includes SchemaPath, so this is safe at runtime.
|
|
4335
|
+
formLevelSchema(path);
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
/**
|
|
4339
|
+
* Creates a schema from only form-level schema (when no field-level schema exists).
|
|
4340
|
+
*
|
|
4341
|
+
* Use this when the form has fields without validation rules but still
|
|
4342
|
+
* needs form-level validation via a Standard Schema.
|
|
4343
|
+
*
|
|
4344
|
+
* @typeParam TModel - The form value type
|
|
4345
|
+
* @param formLevelSchema - Form-level Standard Schema
|
|
4346
|
+
* @returns Schema that applies form-level validation
|
|
4347
|
+
*
|
|
4348
|
+
* @example
|
|
4349
|
+
* ```typescript
|
|
4350
|
+
* import { z } from 'zod';
|
|
4351
|
+
* import { standardSchema } from '@ng-forge/dynamic-forms/schema';
|
|
4352
|
+
*
|
|
4353
|
+
* const FormSchema = z.object({
|
|
4354
|
+
* email: z.string().email(),
|
|
4355
|
+
* password: z.string().min(8),
|
|
4356
|
+
* });
|
|
4357
|
+
*
|
|
4358
|
+
* // Create schema from form-level schema only
|
|
4359
|
+
* const formSchema = createFormLevelSchema(standardSchema(FormSchema));
|
|
4360
|
+
* ```
|
|
4361
|
+
*/
|
|
4362
|
+
function createFormLevelSchema(formLevelSchema) {
|
|
4363
|
+
return schema((path) => {
|
|
4364
|
+
applyFormLevelSchema(path, formLevelSchema);
|
|
4365
|
+
});
|
|
4366
|
+
}
|
|
4367
|
+
|
|
3743
4368
|
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3744
4369
|
function getFieldTreeByKey(ctx, key) {
|
|
3745
4370
|
return ctx.fieldTree[key];
|
|
@@ -3756,13 +4381,18 @@ function getFieldTreeByKey(ctx, key) {
|
|
|
3756
4381
|
*
|
|
3757
4382
|
* @param fields Field definitions to create schema from
|
|
3758
4383
|
* @param registry Field type registry
|
|
3759
|
-
* @param
|
|
4384
|
+
* @param optionsOrValidators Optional configuration object or array of cross-field validators (for backwards compatibility)
|
|
3760
4385
|
*/
|
|
3761
|
-
function createSchemaFromFields(fields, registry,
|
|
4386
|
+
function createSchemaFromFields(fields, registry, optionsOrValidators) {
|
|
3762
4387
|
// Inject services for cross-field validation
|
|
3763
4388
|
// These will be available because createSchemaFromFields is called within runInInjectionContext
|
|
3764
4389
|
const functionRegistry = inject(FunctionRegistryService);
|
|
3765
4390
|
const logger = inject(DynamicFormLogger);
|
|
4391
|
+
// Normalize options - support both old array signature and new options object
|
|
4392
|
+
const options = Array.isArray(optionsOrValidators)
|
|
4393
|
+
? { crossFieldValidators: optionsOrValidators }
|
|
4394
|
+
: (optionsOrValidators ?? {});
|
|
4395
|
+
const { crossFieldValidators, formLevelSchema } = options;
|
|
3766
4396
|
return schema((path) => {
|
|
3767
4397
|
for (const fieldDef of fields) {
|
|
3768
4398
|
const valueHandling = getFieldValueHandling(fieldDef.type, registry);
|
|
@@ -3796,6 +4426,10 @@ function createSchemaFromFields(fields, registry, crossFieldValidators) {
|
|
|
3796
4426
|
if (crossFieldValidators && crossFieldValidators.length > 0) {
|
|
3797
4427
|
applyCrossFieldTreeValidator(path, crossFieldValidators, functionRegistry, logger);
|
|
3798
4428
|
}
|
|
4429
|
+
// Apply form-level Standard Schema validation
|
|
4430
|
+
if (formLevelSchema) {
|
|
4431
|
+
applyFormLevelSchema(path, formLevelSchema);
|
|
4432
|
+
}
|
|
3799
4433
|
});
|
|
3800
4434
|
}
|
|
3801
4435
|
/**
|
|
@@ -4146,10 +4780,10 @@ class EventBus {
|
|
|
4146
4780
|
}
|
|
4147
4781
|
return this.pipeline$.pipe(filter((event) => event.type === eventType));
|
|
4148
4782
|
}
|
|
4149
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0
|
|
4150
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0
|
|
4783
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4784
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: EventBus });
|
|
4151
4785
|
}
|
|
4152
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0
|
|
4786
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: EventBus, decorators: [{
|
|
4153
4787
|
type: Injectable
|
|
4154
4788
|
}] });
|
|
4155
4789
|
|
|
@@ -4536,19 +5170,19 @@ function createEmptyCollection() {
|
|
|
4536
5170
|
/** Traverses field definitions and collects cross-field entries (validators, logic, schemas). */
|
|
4537
5171
|
function collectCrossFieldEntries(fields) {
|
|
4538
5172
|
const collection = createEmptyCollection();
|
|
4539
|
-
traverseFields(fields, collection);
|
|
5173
|
+
traverseFields$1(fields, collection);
|
|
4540
5174
|
return collection;
|
|
4541
5175
|
}
|
|
4542
|
-
function traverseFields(fields, collection) {
|
|
5176
|
+
function traverseFields$1(fields, collection) {
|
|
4543
5177
|
for (const field of fields) {
|
|
4544
|
-
collectFromField(field, collection);
|
|
5178
|
+
collectFromField$1(field, collection);
|
|
4545
5179
|
// Recursively process container fields (page, row, group, array)
|
|
4546
5180
|
if (hasChildFields(field)) {
|
|
4547
|
-
traverseFields(normalizeFieldsArray(field.fields), collection);
|
|
5181
|
+
traverseFields$1(normalizeFieldsArray(field.fields), collection);
|
|
4548
5182
|
}
|
|
4549
5183
|
}
|
|
4550
5184
|
}
|
|
4551
|
-
function collectFromField(field, collection) {
|
|
5185
|
+
function collectFromField$1(field, collection) {
|
|
4552
5186
|
const fieldKey = field.key;
|
|
4553
5187
|
if (!fieldKey)
|
|
4554
5188
|
return;
|
|
@@ -4619,9 +5253,19 @@ function tryCreateValidatorEntry(fieldKey, config) {
|
|
|
4619
5253
|
}
|
|
4620
5254
|
return null;
|
|
4621
5255
|
}
|
|
4622
|
-
/**
|
|
5256
|
+
/**
|
|
5257
|
+
* Returns a logic entry if cross-field state logic, null otherwise.
|
|
5258
|
+
*
|
|
5259
|
+
* Note: Derivation logic is handled separately by the derivation system
|
|
5260
|
+
* and is not collected here.
|
|
5261
|
+
*/
|
|
4623
5262
|
function tryCreateLogicEntry(fieldKey, config) {
|
|
4624
|
-
|
|
5263
|
+
// Only process state logic (hidden, readonly, disabled, required)
|
|
5264
|
+
// Derivation logic is handled by the derivation collector
|
|
5265
|
+
if (!isStateLogicConfig(config)) {
|
|
5266
|
+
return null;
|
|
5267
|
+
}
|
|
5268
|
+
if (!isCrossFieldStateLogic(config)) {
|
|
4625
5269
|
return null;
|
|
4626
5270
|
}
|
|
4627
5271
|
const condition = config.condition;
|
|
@@ -4629,7 +5273,7 @@ function tryCreateLogicEntry(fieldKey, config) {
|
|
|
4629
5273
|
sourceFieldKey: fieldKey,
|
|
4630
5274
|
logicType: config.type,
|
|
4631
5275
|
condition,
|
|
4632
|
-
config,
|
|
5276
|
+
config: config,
|
|
4633
5277
|
dependsOn: extractExpressionDependencies(condition),
|
|
4634
5278
|
category: 'logic',
|
|
4635
5279
|
};
|
|
@@ -4810,159 +5454,2552 @@ function isValidFormConfiguration(fields) {
|
|
|
4810
5454
|
return FormModeValidator.validateFormConfiguration(fields).isValid;
|
|
4811
5455
|
}
|
|
4812
5456
|
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
5457
|
+
/**
|
|
5458
|
+
* Centralized constants for the derivation system.
|
|
5459
|
+
*
|
|
5460
|
+
* These constants configure the behavior of derivation processing,
|
|
5461
|
+
* caching, and path handling. Centralizing them enables:
|
|
5462
|
+
* - Easy configuration changes
|
|
5463
|
+
* - Consistent values across all derivation modules
|
|
5464
|
+
* - Clear documentation of magic numbers
|
|
5465
|
+
*
|
|
5466
|
+
* @module
|
|
5467
|
+
*/
|
|
5468
|
+
/**
|
|
5469
|
+
* Maximum number of derivation iterations before stopping to prevent infinite loops.
|
|
5470
|
+
*
|
|
5471
|
+
* The value of 10 is chosen based on:
|
|
5472
|
+
* - Most derivation chains are 2-3 levels deep (A→B→C)
|
|
5473
|
+
* - Complex forms with conditional cascades rarely exceed 5-6 levels
|
|
5474
|
+
* - 10 provides headroom for bidirectional sync patterns (A↔B) which may need
|
|
5475
|
+
* 2 iterations per pair to stabilize
|
|
5476
|
+
* - Higher values delay detection of actual infinite loops
|
|
5477
|
+
* - Lower values risk false positives on legitimate deep chains
|
|
5478
|
+
*
|
|
5479
|
+
* If you hit this limit legitimately, consider:
|
|
5480
|
+
* 1. Restructuring derivations to reduce chain depth
|
|
5481
|
+
* 2. Using explicit `dependsOn` to control evaluation order
|
|
5482
|
+
* 3. Breaking complex derivations into computed signals outside the form
|
|
5483
|
+
*/
|
|
5484
|
+
const MAX_DERIVATION_ITERATIONS = 10;
|
|
5485
|
+
/**
|
|
5486
|
+
* Placeholder string for array index in derivation paths.
|
|
5487
|
+
*
|
|
5488
|
+
* Used in paths like `items.$.quantity` to represent "each item in the array".
|
|
5489
|
+
* The `$` is resolved to actual indices at runtime during array derivation processing.
|
|
5490
|
+
*/
|
|
5491
|
+
const ARRAY_PLACEHOLDER = '.$.';
|
|
5492
|
+
/**
|
|
5493
|
+
* Maximum size of the expression AST cache.
|
|
5494
|
+
*
|
|
5495
|
+
* Expressions are parsed into Abstract Syntax Trees (AST) once and cached
|
|
5496
|
+
* to avoid re-parsing on each evaluation. The cache uses LRU eviction.
|
|
5497
|
+
*
|
|
5498
|
+
* 1000 entries handles:
|
|
5499
|
+
* - Large forms with many derivation expressions
|
|
5500
|
+
* - Multiple form instances with different expressions
|
|
5501
|
+
* - Memory efficiency (AST nodes are relatively small)
|
|
5502
|
+
*/
|
|
5503
|
+
const MAX_AST_CACHE_SIZE = 1000;
|
|
5504
|
+
/**
|
|
5505
|
+
* Delimiter used to create unique derivation keys from source and target field keys.
|
|
5506
|
+
*
|
|
5507
|
+
* Uses null character (\x00) which is extremely unlikely to appear in field names,
|
|
5508
|
+
* avoiding collision issues that could occur with common delimiters like ':'.
|
|
5509
|
+
*/
|
|
5510
|
+
const DERIVATION_KEY_DELIMITER = '\x00';
|
|
4816
5511
|
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
totalPages,
|
|
4827
|
-
/** Previous page index (0-based), undefined if first navigation */
|
|
4828
|
-
previousPageIndex) {
|
|
4829
|
-
this.currentPageIndex = currentPageIndex;
|
|
4830
|
-
this.totalPages = totalPages;
|
|
4831
|
-
this.previousPageIndex = previousPageIndex;
|
|
4832
|
-
}
|
|
5512
|
+
/**
|
|
5513
|
+
* Creates an empty derivation collection.
|
|
5514
|
+
*
|
|
5515
|
+
* @returns Empty collection ready for population
|
|
5516
|
+
*
|
|
5517
|
+
* @internal
|
|
5518
|
+
*/
|
|
5519
|
+
function createEmptyDerivationCollection() {
|
|
5520
|
+
return { entries: [] };
|
|
4833
5521
|
}
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
5522
|
+
/**
|
|
5523
|
+
* Creates a new derivation chain context for tracking applied derivations.
|
|
5524
|
+
*
|
|
5525
|
+
* @returns Fresh context for a new derivation cycle
|
|
5526
|
+
*
|
|
5527
|
+
* @internal
|
|
5528
|
+
*/
|
|
5529
|
+
function createDerivationChainContext() {
|
|
5530
|
+
return {
|
|
5531
|
+
appliedDerivations: new Set(),
|
|
5532
|
+
iteration: 0,
|
|
5533
|
+
};
|
|
4837
5534
|
}
|
|
4838
|
-
|
|
4839
5535
|
/**
|
|
4840
|
-
*
|
|
5536
|
+
* Creates a unique key for a derivation entry.
|
|
4841
5537
|
*
|
|
4842
|
-
*
|
|
4843
|
-
*
|
|
4844
|
-
* based on the provided field template, or uses the array's own template if not provided.
|
|
5538
|
+
* Uses a null character delimiter to avoid collision with field names
|
|
5539
|
+
* that might contain common delimiters like ':'.
|
|
4845
5540
|
*
|
|
4846
|
-
* @
|
|
4847
|
-
*
|
|
4848
|
-
*
|
|
4849
|
-
* eventBus.dispatch(AddArrayItemEvent, 'contacts');
|
|
5541
|
+
* @param sourceKey - The source field key
|
|
5542
|
+
* @param targetKey - The target field key
|
|
5543
|
+
* @returns Unique key for the derivation
|
|
4850
5544
|
*
|
|
4851
|
-
*
|
|
4852
|
-
|
|
4853
|
-
|
|
5545
|
+
* @internal
|
|
5546
|
+
*/
|
|
5547
|
+
function createDerivationKey(sourceKey, targetKey) {
|
|
5548
|
+
return `${sourceKey}${DERIVATION_KEY_DELIMITER}${targetKey}`;
|
|
5549
|
+
}
|
|
5550
|
+
/**
|
|
5551
|
+
* Parses a derivation key back into source and target field keys.
|
|
4854
5552
|
*
|
|
4855
|
-
*
|
|
4856
|
-
*
|
|
4857
|
-
*
|
|
5553
|
+
* @param key - The derivation key to parse
|
|
5554
|
+
* @returns Object containing source and target field keys
|
|
5555
|
+
*
|
|
5556
|
+
* @internal
|
|
4858
5557
|
*/
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
/** The key of the array field to add an item to */
|
|
4866
|
-
arrayKey,
|
|
4867
|
-
/**
|
|
4868
|
-
* Optional field template definition to render for the new array item.
|
|
4869
|
-
* If not provided, the array field component will use its own template.
|
|
4870
|
-
*/
|
|
4871
|
-
field,
|
|
4872
|
-
/** Optional index where to insert the item (defaults to end of array) */
|
|
4873
|
-
index) {
|
|
4874
|
-
this.arrayKey = arrayKey;
|
|
4875
|
-
this.field = field;
|
|
4876
|
-
this.index = index;
|
|
4877
|
-
}
|
|
5558
|
+
function parseDerivationKey(key) {
|
|
5559
|
+
const delimiterIndex = key.indexOf(DERIVATION_KEY_DELIMITER);
|
|
5560
|
+
return {
|
|
5561
|
+
sourceKey: key.substring(0, delimiterIndex),
|
|
5562
|
+
targetKey: key.substring(delimiterIndex + DERIVATION_KEY_DELIMITER.length),
|
|
5563
|
+
};
|
|
4878
5564
|
}
|
|
4879
5565
|
|
|
4880
5566
|
/**
|
|
4881
|
-
*
|
|
5567
|
+
* Injection token for the collected derivation entries.
|
|
4882
5568
|
*
|
|
4883
|
-
*
|
|
4884
|
-
*
|
|
4885
|
-
*
|
|
5569
|
+
* Provided at form level, contains the topologically sorted entries
|
|
5570
|
+
* after validation. This is the "core data" that other derivation
|
|
5571
|
+
* components consume.
|
|
4886
5572
|
*
|
|
4887
5573
|
* @example
|
|
4888
5574
|
* ```typescript
|
|
4889
|
-
* //
|
|
4890
|
-
*
|
|
5575
|
+
* // In a component or service
|
|
5576
|
+
* private readonly entries = inject(DERIVATION_ENTRIES);
|
|
4891
5577
|
*
|
|
4892
|
-
* //
|
|
4893
|
-
*
|
|
5578
|
+
* // Access the current entries
|
|
5579
|
+
* const currentEntries = this.entries();
|
|
4894
5580
|
* ```
|
|
4895
5581
|
*/
|
|
4896
|
-
|
|
4897
|
-
arrayKey;
|
|
4898
|
-
index;
|
|
4899
|
-
type = 'remove-array-item';
|
|
4900
|
-
constructor(
|
|
4901
|
-
/** The key of the array field to remove an item from */
|
|
4902
|
-
arrayKey,
|
|
4903
|
-
/** Index of the item to remove (defaults to last item) */
|
|
4904
|
-
index) {
|
|
4905
|
-
this.arrayKey = arrayKey;
|
|
4906
|
-
this.index = index;
|
|
4907
|
-
}
|
|
4908
|
-
}
|
|
5582
|
+
const DERIVATION_ENTRIES = new InjectionToken('DerivationEntries');
|
|
4909
5583
|
|
|
4910
5584
|
/**
|
|
4911
|
-
*
|
|
5585
|
+
* Utilities for parsing and manipulating field paths in dynamic forms.
|
|
4912
5586
|
*
|
|
4913
|
-
*
|
|
4914
|
-
*
|
|
5587
|
+
* Field paths follow these formats:
|
|
5588
|
+
* - Simple: "fieldName"
|
|
5589
|
+
* - Nested: "parent.child.grandchild"
|
|
5590
|
+
* - Array indexed: "items.0.quantity" or "items[0].quantity"
|
|
5591
|
+
* - Array placeholder: "items.$.quantity" ($ = any index)
|
|
4915
5592
|
*
|
|
4916
|
-
* @
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
*
|
|
4920
|
-
*
|
|
5593
|
+
* @module path-utils
|
|
5594
|
+
*/
|
|
5595
|
+
/**
|
|
5596
|
+
* Parses an array derivation path into its components.
|
|
5597
|
+
*
|
|
5598
|
+
* Array paths use the `$` placeholder to represent "any index".
|
|
5599
|
+
* Format: "arrayPath.$.relativePath"
|
|
5600
|
+
*
|
|
5601
|
+
* @param path - The path to parse (e.g., "items.$.quantity")
|
|
5602
|
+
* @returns Parsed path info with arrayPath and relativePath
|
|
4921
5603
|
*
|
|
4922
5604
|
* @example
|
|
4923
5605
|
* ```typescript
|
|
4924
|
-
*
|
|
4925
|
-
*
|
|
4926
|
-
*
|
|
4927
|
-
*
|
|
5606
|
+
* parseArrayPath('items.$.quantity')
|
|
5607
|
+
* // { arrayPath: 'items', relativePath: 'quantity', isArrayPath: true }
|
|
5608
|
+
*
|
|
5609
|
+
* parseArrayPath('orders.lineItems.$.total')
|
|
5610
|
+
* // { arrayPath: 'orders.lineItems', relativePath: 'total', isArrayPath: true }
|
|
5611
|
+
*
|
|
5612
|
+
* parseArrayPath('simpleField')
|
|
5613
|
+
* // { arrayPath: '', relativePath: '', isArrayPath: false }
|
|
4928
5614
|
* ```
|
|
5615
|
+
*
|
|
5616
|
+
* @public
|
|
4929
5617
|
*/
|
|
4930
|
-
|
|
4931
|
-
|
|
5618
|
+
function parseArrayPath(path) {
|
|
5619
|
+
const ARRAY_PLACEHOLDER = '.$.';
|
|
5620
|
+
const placeholderIndex = path.indexOf(ARRAY_PLACEHOLDER);
|
|
5621
|
+
if (placeholderIndex === -1) {
|
|
5622
|
+
return {
|
|
5623
|
+
arrayPath: '',
|
|
5624
|
+
relativePath: '',
|
|
5625
|
+
isArrayPath: false,
|
|
5626
|
+
};
|
|
5627
|
+
}
|
|
5628
|
+
return {
|
|
5629
|
+
arrayPath: path.substring(0, placeholderIndex),
|
|
5630
|
+
relativePath: path.substring(placeholderIndex + ARRAY_PLACEHOLDER.length),
|
|
5631
|
+
isArrayPath: true,
|
|
5632
|
+
};
|
|
4932
5633
|
}
|
|
4933
|
-
|
|
4934
5634
|
/**
|
|
4935
|
-
*
|
|
5635
|
+
* Resolves an array placeholder path to a concrete indexed path.
|
|
4936
5636
|
*
|
|
4937
|
-
*
|
|
4938
|
-
*
|
|
5637
|
+
* @param path - The path with $ placeholder (e.g., "items.$.quantity")
|
|
5638
|
+
* @param index - The array index to substitute
|
|
5639
|
+
* @returns Resolved path with index (e.g., "items.0.quantity")
|
|
4939
5640
|
*
|
|
4940
5641
|
* @example
|
|
4941
5642
|
* ```typescript
|
|
4942
|
-
*
|
|
4943
|
-
*
|
|
4944
|
-
* ```
|
|
5643
|
+
* resolveArrayPath('items.$.quantity', 2)
|
|
5644
|
+
* // 'items.2.quantity'
|
|
4945
5645
|
*
|
|
4946
|
-
*
|
|
4947
|
-
*
|
|
4948
|
-
* // Listen for clear events
|
|
4949
|
-
* eventBus.on<FormClearEvent>('form-clear').subscribe(() => {
|
|
4950
|
-
* console.log('Form was cleared');
|
|
4951
|
-
* });
|
|
5646
|
+
* resolveArrayPath('orders.lineItems.$.total', 0)
|
|
5647
|
+
* // 'orders.lineItems.0.total'
|
|
4952
5648
|
* ```
|
|
5649
|
+
*
|
|
5650
|
+
* @public
|
|
4953
5651
|
*/
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
class PageNavigationStateChangeEvent {
|
|
4959
|
-
state;
|
|
4960
|
-
type = 'page-navigation-state-change';
|
|
4961
|
-
constructor(state) {
|
|
4962
|
-
this.state = state;
|
|
5652
|
+
function resolveArrayPath(path, index) {
|
|
5653
|
+
const info = parseArrayPath(path);
|
|
5654
|
+
if (!info.isArrayPath) {
|
|
5655
|
+
return path;
|
|
4963
5656
|
}
|
|
5657
|
+
return `${info.arrayPath}.${index}.${info.relativePath}`;
|
|
4964
5658
|
}
|
|
4965
|
-
|
|
5659
|
+
/**
|
|
5660
|
+
* Checks if a path contains an array placeholder.
|
|
5661
|
+
*
|
|
5662
|
+
* @param path - The path to check
|
|
5663
|
+
* @returns True if the path contains .$.
|
|
5664
|
+
*
|
|
5665
|
+
* @public
|
|
5666
|
+
*/
|
|
5667
|
+
function isArrayPlaceholderPath(path) {
|
|
5668
|
+
return path.includes('.$.');
|
|
5669
|
+
}
|
|
5670
|
+
/**
|
|
5671
|
+
* Extracts the array path from a path that may contain an array placeholder.
|
|
5672
|
+
*
|
|
5673
|
+
* @param path - The path to extract from
|
|
5674
|
+
* @returns The array path, or empty string if not an array path
|
|
5675
|
+
*
|
|
5676
|
+
* @example
|
|
5677
|
+
* ```typescript
|
|
5678
|
+
* extractArrayPath('items.$.quantity')
|
|
5679
|
+
* // 'items'
|
|
5680
|
+
*
|
|
5681
|
+
* extractArrayPath('orders.lineItems.$.total')
|
|
5682
|
+
* // 'orders.lineItems'
|
|
5683
|
+
*
|
|
5684
|
+
* extractArrayPath('simpleField')
|
|
5685
|
+
* // ''
|
|
5686
|
+
* ```
|
|
5687
|
+
*
|
|
5688
|
+
* @public
|
|
5689
|
+
*/
|
|
5690
|
+
function extractArrayPath(path) {
|
|
5691
|
+
return parseArrayPath(path).arrayPath;
|
|
5692
|
+
}
|
|
5693
|
+
/**
|
|
5694
|
+
* Splits a dot-separated path into segments.
|
|
5695
|
+
*
|
|
5696
|
+
* Handles both dot notation and bracket notation for array indices.
|
|
5697
|
+
*
|
|
5698
|
+
* @param path - The path to split
|
|
5699
|
+
* @returns Array of path segments
|
|
5700
|
+
*
|
|
5701
|
+
* @example
|
|
5702
|
+
* ```typescript
|
|
5703
|
+
* splitPath('parent.child.grandchild')
|
|
5704
|
+
* // ['parent', 'child', 'grandchild']
|
|
5705
|
+
*
|
|
5706
|
+
* splitPath('items.0.quantity')
|
|
5707
|
+
* // ['items', '0', 'quantity']
|
|
5708
|
+
* ```
|
|
5709
|
+
*
|
|
5710
|
+
* @public
|
|
5711
|
+
*/
|
|
5712
|
+
function splitPath(path) {
|
|
5713
|
+
if (!path)
|
|
5714
|
+
return [];
|
|
5715
|
+
return path.split('.');
|
|
5716
|
+
}
|
|
5717
|
+
/**
|
|
5718
|
+
* Joins path segments into a dot-separated path.
|
|
5719
|
+
*
|
|
5720
|
+
* @param segments - The path segments to join
|
|
5721
|
+
* @returns Joined path string
|
|
5722
|
+
*
|
|
5723
|
+
* @public
|
|
5724
|
+
*/
|
|
5725
|
+
function joinPath(segments) {
|
|
5726
|
+
return segments.join('.');
|
|
5727
|
+
}
|
|
5728
|
+
/**
|
|
5729
|
+
* Gets the parent path from a nested path.
|
|
5730
|
+
*
|
|
5731
|
+
* @param path - The path to get parent from
|
|
5732
|
+
* @returns The parent path, or empty string if no parent
|
|
5733
|
+
*
|
|
5734
|
+
* @example
|
|
5735
|
+
* ```typescript
|
|
5736
|
+
* getParentPath('parent.child.grandchild')
|
|
5737
|
+
* // 'parent.child'
|
|
5738
|
+
*
|
|
5739
|
+
* getParentPath('topLevel')
|
|
5740
|
+
* // ''
|
|
5741
|
+
* ```
|
|
5742
|
+
*
|
|
5743
|
+
* @public
|
|
5744
|
+
*/
|
|
5745
|
+
function getParentPath(path) {
|
|
5746
|
+
const segments = splitPath(path);
|
|
5747
|
+
if (segments.length <= 1)
|
|
5748
|
+
return '';
|
|
5749
|
+
return joinPath(segments.slice(0, -1));
|
|
5750
|
+
}
|
|
5751
|
+
/**
|
|
5752
|
+
* Gets the last segment (leaf) from a path.
|
|
5753
|
+
*
|
|
5754
|
+
* @param path - The path to get leaf from
|
|
5755
|
+
* @returns The last segment
|
|
5756
|
+
*
|
|
5757
|
+
* @example
|
|
5758
|
+
* ```typescript
|
|
5759
|
+
* getLeafPath('parent.child.grandchild')
|
|
5760
|
+
* // 'grandchild'
|
|
5761
|
+
*
|
|
5762
|
+
* getLeafPath('topLevel')
|
|
5763
|
+
* // 'topLevel'
|
|
5764
|
+
* ```
|
|
5765
|
+
*
|
|
5766
|
+
* @public
|
|
5767
|
+
*/
|
|
5768
|
+
function getLeafPath(path) {
|
|
5769
|
+
const segments = splitPath(path);
|
|
5770
|
+
return segments[segments.length - 1] ?? '';
|
|
5771
|
+
}
|
|
5772
|
+
/**
|
|
5773
|
+
* Parses a path that may contain multiple array placeholders.
|
|
5774
|
+
*
|
|
5775
|
+
* This supports deeply nested array structures like `orders.$.items.$.quantity`
|
|
5776
|
+
* where multiple levels of arrays need to be traversed.
|
|
5777
|
+
*
|
|
5778
|
+
* @param path - Path with zero or more $ placeholders
|
|
5779
|
+
* @returns Parsed path information
|
|
5780
|
+
*
|
|
5781
|
+
* @example
|
|
5782
|
+
* ```typescript
|
|
5783
|
+
* parseMultiArrayPath('orders.$.items.$.quantity')
|
|
5784
|
+
* // {
|
|
5785
|
+
* // isArrayPath: true,
|
|
5786
|
+
* // placeholderCount: 2,
|
|
5787
|
+
* // segments: ['orders', 'items', 'quantity'],
|
|
5788
|
+
* // placeholderPositions: [1, 3]
|
|
5789
|
+
* // }
|
|
5790
|
+
*
|
|
5791
|
+
* parseMultiArrayPath('items.$.name')
|
|
5792
|
+
* // {
|
|
5793
|
+
* // isArrayPath: true,
|
|
5794
|
+
* // placeholderCount: 1,
|
|
5795
|
+
* // segments: ['items', 'name'],
|
|
5796
|
+
* // placeholderPositions: [1]
|
|
5797
|
+
* // }
|
|
5798
|
+
*
|
|
5799
|
+
* parseMultiArrayPath('simpleField')
|
|
5800
|
+
* // {
|
|
5801
|
+
* // isArrayPath: false,
|
|
5802
|
+
* // placeholderCount: 0,
|
|
5803
|
+
* // segments: ['simpleField'],
|
|
5804
|
+
* // placeholderPositions: []
|
|
5805
|
+
* // }
|
|
5806
|
+
* ```
|
|
5807
|
+
*
|
|
5808
|
+
* @public
|
|
5809
|
+
*/
|
|
5810
|
+
function parseMultiArrayPath(path) {
|
|
5811
|
+
const parts = path.split('.');
|
|
5812
|
+
const segments = [];
|
|
5813
|
+
const placeholderPositions = [];
|
|
5814
|
+
for (let i = 0; i < parts.length; i++) {
|
|
5815
|
+
if (parts[i] === '$') {
|
|
5816
|
+
placeholderPositions.push(i);
|
|
5817
|
+
}
|
|
5818
|
+
else {
|
|
5819
|
+
segments.push(parts[i]);
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5822
|
+
return {
|
|
5823
|
+
isArrayPath: placeholderPositions.length > 0,
|
|
5824
|
+
placeholderCount: placeholderPositions.length,
|
|
5825
|
+
segments,
|
|
5826
|
+
placeholderPositions,
|
|
5827
|
+
};
|
|
5828
|
+
}
|
|
5829
|
+
/**
|
|
5830
|
+
* Resolves a path with multiple array placeholders using provided indices.
|
|
5831
|
+
*
|
|
5832
|
+
* Each `$` placeholder is replaced with the corresponding index from the
|
|
5833
|
+
* indices array, in order.
|
|
5834
|
+
*
|
|
5835
|
+
* @param path - Path with $ placeholders (e.g., 'orders.$.items.$.quantity')
|
|
5836
|
+
* @param indices - Array of indices to substitute (e.g., [0, 2])
|
|
5837
|
+
* @returns Resolved path (e.g., 'orders.0.items.2.quantity')
|
|
5838
|
+
*
|
|
5839
|
+
* @throws Error if number of indices doesn't match number of placeholders
|
|
5840
|
+
*
|
|
5841
|
+
* @example
|
|
5842
|
+
* ```typescript
|
|
5843
|
+
* resolveMultiArrayPath('orders.$.items.$.quantity', [0, 2])
|
|
5844
|
+
* // 'orders.0.items.2.quantity'
|
|
5845
|
+
*
|
|
5846
|
+
* resolveMultiArrayPath('items.$.name', [5])
|
|
5847
|
+
* // 'items.5.name'
|
|
5848
|
+
* ```
|
|
5849
|
+
*
|
|
5850
|
+
* @public
|
|
5851
|
+
*/
|
|
5852
|
+
function resolveMultiArrayPath(path, indices) {
|
|
5853
|
+
const parts = path.split('.');
|
|
5854
|
+
let indexPointer = 0;
|
|
5855
|
+
const resolved = parts.map((part) => {
|
|
5856
|
+
if (part === '$') {
|
|
5857
|
+
if (indexPointer >= indices.length) {
|
|
5858
|
+
throw new Error(`Not enough indices provided for path '${path}'. ` +
|
|
5859
|
+
`Expected ${parts.filter((p) => p === '$').length} indices, got ${indices.length}.`);
|
|
5860
|
+
}
|
|
5861
|
+
return String(indices[indexPointer++]);
|
|
5862
|
+
}
|
|
5863
|
+
return part;
|
|
5864
|
+
});
|
|
5865
|
+
if (indexPointer < indices.length) {
|
|
5866
|
+
throw new Error(`Too many indices provided for path '${path}'. ` + `Expected ${indexPointer} indices, got ${indices.length}.`);
|
|
5867
|
+
}
|
|
5868
|
+
return resolved.join('.');
|
|
5869
|
+
}
|
|
5870
|
+
/**
|
|
5871
|
+
* Counts the number of array placeholders in a path.
|
|
5872
|
+
*
|
|
5873
|
+
* @param path - The path to analyze
|
|
5874
|
+
* @returns Number of $ placeholders in the path
|
|
5875
|
+
*
|
|
5876
|
+
* @example
|
|
5877
|
+
* ```typescript
|
|
5878
|
+
* countArrayPlaceholders('orders.$.items.$.quantity')
|
|
5879
|
+
* // 2
|
|
5880
|
+
*
|
|
5881
|
+
* countArrayPlaceholders('items.$.name')
|
|
5882
|
+
* // 1
|
|
5883
|
+
*
|
|
5884
|
+
* countArrayPlaceholders('simpleField')
|
|
5885
|
+
* // 0
|
|
5886
|
+
* ```
|
|
5887
|
+
*
|
|
5888
|
+
* @public
|
|
5889
|
+
*/
|
|
5890
|
+
function countArrayPlaceholders(path) {
|
|
5891
|
+
return path.split('.').filter((part) => part === '$').length;
|
|
5892
|
+
}
|
|
5893
|
+
|
|
5894
|
+
/**
|
|
5895
|
+
* Provides lazy-computed lookup maps for derivation entries.
|
|
5896
|
+
*
|
|
5897
|
+
* Instead of pre-computing all lookup maps at collection time (which caused
|
|
5898
|
+
* hidden mutation and bloated the DerivationCollection god object), this class
|
|
5899
|
+
* computes maps lazily on first access and caches them.
|
|
5900
|
+
*
|
|
5901
|
+
* @example
|
|
5902
|
+
* ```typescript
|
|
5903
|
+
* const lookup = inject(DerivationLookup);
|
|
5904
|
+
*
|
|
5905
|
+
* // Lazy-computed on first access
|
|
5906
|
+
* const entriesForQuantity = lookup.byDependency.get('quantity');
|
|
5907
|
+
* const onChangeEntries = lookup.getOnChangeEntries();
|
|
5908
|
+
* ```
|
|
5909
|
+
*/
|
|
5910
|
+
class DerivationLookup {
|
|
5911
|
+
entriesSignal = inject(DERIVATION_ENTRIES);
|
|
5912
|
+
// Track the entries array reference to auto-invalidate on change
|
|
5913
|
+
_cachedEntriesRef;
|
|
5914
|
+
// Cached lookup maps (built lazily on first access)
|
|
5915
|
+
_byTarget;
|
|
5916
|
+
_bySource;
|
|
5917
|
+
_byDependency;
|
|
5918
|
+
_byArrayPath;
|
|
5919
|
+
_wildcardEntries;
|
|
5920
|
+
_onChangeEntries;
|
|
5921
|
+
_debouncedEntriesByMs;
|
|
5922
|
+
/**
|
|
5923
|
+
* Current derivation entries (from signal).
|
|
5924
|
+
* Auto-invalidates cache if entries reference changed.
|
|
5925
|
+
*/
|
|
5926
|
+
get entries() {
|
|
5927
|
+
const currentEntries = this.entriesSignal();
|
|
5928
|
+
// Auto-invalidate if entries reference changed (schema was updated)
|
|
5929
|
+
if (this._cachedEntriesRef !== currentEntries) {
|
|
5930
|
+
this.invalidateCache();
|
|
5931
|
+
this._cachedEntriesRef = currentEntries;
|
|
5932
|
+
}
|
|
5933
|
+
return currentEntries;
|
|
5934
|
+
}
|
|
5935
|
+
/**
|
|
5936
|
+
* Map of target field key to entries that modify it.
|
|
5937
|
+
* Used for quick lookup when processing derivations.
|
|
5938
|
+
*/
|
|
5939
|
+
get byTarget() {
|
|
5940
|
+
return (this._byTarget ??= this.buildByTargetMap());
|
|
5941
|
+
}
|
|
5942
|
+
/**
|
|
5943
|
+
* Map of source field key to entries defined on it.
|
|
5944
|
+
* Used for finding derivations when a field's value changes.
|
|
5945
|
+
*/
|
|
5946
|
+
get bySource() {
|
|
5947
|
+
return (this._bySource ??= this.buildBySourceMap());
|
|
5948
|
+
}
|
|
5949
|
+
/**
|
|
5950
|
+
* Map of dependency field key to entries that depend on it.
|
|
5951
|
+
* Used for efficient O(1) lookup when filtering by changed fields.
|
|
5952
|
+
*/
|
|
5953
|
+
get byDependency() {
|
|
5954
|
+
return (this._byDependency ??= this.buildByDependencyMap());
|
|
5955
|
+
}
|
|
5956
|
+
/**
|
|
5957
|
+
* Map of array path to entries that derive array item values.
|
|
5958
|
+
* Used for O(1) lookup when an array field changes.
|
|
5959
|
+
*/
|
|
5960
|
+
get byArrayPath() {
|
|
5961
|
+
return (this._byArrayPath ??= this.buildByArrayPathMap());
|
|
5962
|
+
}
|
|
5963
|
+
/**
|
|
5964
|
+
* Entries that have wildcard (*) dependency.
|
|
5965
|
+
* These entries depend on all form values and must always be considered.
|
|
5966
|
+
*/
|
|
5967
|
+
get wildcardEntries() {
|
|
5968
|
+
return (this._wildcardEntries ??= this.buildWildcardEntries());
|
|
5969
|
+
}
|
|
5970
|
+
/**
|
|
5971
|
+
* Gets entries that should be processed based on changed fields.
|
|
5972
|
+
*
|
|
5973
|
+
* Handles nested path matching: if 'address' changed, entries depending on
|
|
5974
|
+
* 'address.city' will also be included (parent change implies child changed).
|
|
5975
|
+
*
|
|
5976
|
+
* Uses O(k) lookup via indexed maps for exact matches, plus O(d) scan for
|
|
5977
|
+
* nested dependencies, where k = changed fields, d = unique dependencies.
|
|
5978
|
+
*/
|
|
5979
|
+
getEntriesForChangedFields(changedFields) {
|
|
5980
|
+
const entrySet = new Set();
|
|
5981
|
+
// Add entries triggered by each changed field via byDependency map
|
|
5982
|
+
for (const fieldKey of changedFields) {
|
|
5983
|
+
const entries = this.byDependency.get(fieldKey);
|
|
5984
|
+
if (entries) {
|
|
5985
|
+
for (const entry of entries) {
|
|
5986
|
+
entrySet.add(entry);
|
|
5987
|
+
}
|
|
5988
|
+
}
|
|
5989
|
+
// Check if this changed field is an array path - O(1) lookup
|
|
5990
|
+
const arrayEntries = this.byArrayPath.get(fieldKey);
|
|
5991
|
+
if (arrayEntries) {
|
|
5992
|
+
for (const entry of arrayEntries) {
|
|
5993
|
+
entrySet.add(entry);
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
// Handle nested dependencies: if 'address' changed, include entries
|
|
5997
|
+
// that depend on 'address.city', 'address.street', etc.
|
|
5998
|
+
// This is O(d) where d = number of unique dependency keys in the map.
|
|
5999
|
+
const fieldKeyPrefix = fieldKey + '.';
|
|
6000
|
+
for (const [depKey, depEntries] of this.byDependency) {
|
|
6001
|
+
if (depKey.startsWith(fieldKeyPrefix)) {
|
|
6002
|
+
for (const entry of depEntries) {
|
|
6003
|
+
entrySet.add(entry);
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
// Add all wildcard entries - O(w) where w = number of wildcard entries
|
|
6009
|
+
for (const entry of this.wildcardEntries) {
|
|
6010
|
+
entrySet.add(entry);
|
|
6011
|
+
}
|
|
6012
|
+
return Array.from(entrySet);
|
|
6013
|
+
}
|
|
6014
|
+
/**
|
|
6015
|
+
* Gets entries with trigger 'onChange' (or no trigger, which defaults to onChange).
|
|
6016
|
+
*/
|
|
6017
|
+
getOnChangeEntries() {
|
|
6018
|
+
return (this._onChangeEntries ??= this.entries.filter((e) => e.trigger !== 'debounced'));
|
|
6019
|
+
}
|
|
6020
|
+
/**
|
|
6021
|
+
* Gets entries with trigger 'debounced' for a specific debounce period.
|
|
6022
|
+
*/
|
|
6023
|
+
getDebouncedEntries(debounceMs) {
|
|
6024
|
+
if (!this._debouncedEntriesByMs) {
|
|
6025
|
+
this._debouncedEntriesByMs = this.buildDebouncedEntriesMap();
|
|
6026
|
+
}
|
|
6027
|
+
return this._debouncedEntriesByMs.get(debounceMs) ?? [];
|
|
6028
|
+
}
|
|
6029
|
+
/**
|
|
6030
|
+
* Gets all unique debounce periods that have entries.
|
|
6031
|
+
*/
|
|
6032
|
+
getDebouncePeriods() {
|
|
6033
|
+
if (!this._debouncedEntriesByMs) {
|
|
6034
|
+
this._debouncedEntriesByMs = this.buildDebouncedEntriesMap();
|
|
6035
|
+
}
|
|
6036
|
+
return Array.from(this._debouncedEntriesByMs.keys());
|
|
6037
|
+
}
|
|
6038
|
+
/**
|
|
6039
|
+
* Invalidates cached maps. Call this when entries change.
|
|
6040
|
+
*/
|
|
6041
|
+
invalidateCache() {
|
|
6042
|
+
this._byTarget = undefined;
|
|
6043
|
+
this._bySource = undefined;
|
|
6044
|
+
this._byDependency = undefined;
|
|
6045
|
+
this._byArrayPath = undefined;
|
|
6046
|
+
this._wildcardEntries = undefined;
|
|
6047
|
+
this._onChangeEntries = undefined;
|
|
6048
|
+
this._debouncedEntriesByMs = undefined;
|
|
6049
|
+
}
|
|
6050
|
+
// ─────────────────────────────────────────────────────────────────
|
|
6051
|
+
// Private builders
|
|
6052
|
+
// ─────────────────────────────────────────────────────────────────
|
|
6053
|
+
buildByTargetMap() {
|
|
6054
|
+
const map = new Map();
|
|
6055
|
+
for (const entry of this.entries) {
|
|
6056
|
+
const targetEntries = map.get(entry.targetFieldKey) ?? [];
|
|
6057
|
+
targetEntries.push(entry);
|
|
6058
|
+
map.set(entry.targetFieldKey, targetEntries);
|
|
6059
|
+
}
|
|
6060
|
+
return map;
|
|
6061
|
+
}
|
|
6062
|
+
buildBySourceMap() {
|
|
6063
|
+
const map = new Map();
|
|
6064
|
+
for (const entry of this.entries) {
|
|
6065
|
+
const sourceEntries = map.get(entry.sourceFieldKey) ?? [];
|
|
6066
|
+
sourceEntries.push(entry);
|
|
6067
|
+
map.set(entry.sourceFieldKey, sourceEntries);
|
|
6068
|
+
}
|
|
6069
|
+
return map;
|
|
6070
|
+
}
|
|
6071
|
+
buildByDependencyMap() {
|
|
6072
|
+
const map = new Map();
|
|
6073
|
+
for (const entry of this.entries) {
|
|
6074
|
+
for (const dep of entry.dependsOn) {
|
|
6075
|
+
if (dep !== '*') {
|
|
6076
|
+
const depEntries = map.get(dep) ?? [];
|
|
6077
|
+
depEntries.push(entry);
|
|
6078
|
+
map.set(dep, depEntries);
|
|
6079
|
+
}
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
6082
|
+
return map;
|
|
6083
|
+
}
|
|
6084
|
+
buildByArrayPathMap() {
|
|
6085
|
+
const map = new Map();
|
|
6086
|
+
for (const entry of this.entries) {
|
|
6087
|
+
if (isArrayPlaceholderPath(entry.targetFieldKey)) {
|
|
6088
|
+
const arrayPath = extractArrayPath(entry.targetFieldKey);
|
|
6089
|
+
if (arrayPath) {
|
|
6090
|
+
const arrayEntries = map.get(arrayPath) ?? [];
|
|
6091
|
+
arrayEntries.push(entry);
|
|
6092
|
+
map.set(arrayPath, arrayEntries);
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
6096
|
+
return map;
|
|
6097
|
+
}
|
|
6098
|
+
buildWildcardEntries() {
|
|
6099
|
+
return this.entries.filter((entry) => entry.dependsOn.includes('*'));
|
|
6100
|
+
}
|
|
6101
|
+
buildDebouncedEntriesMap() {
|
|
6102
|
+
const map = new Map();
|
|
6103
|
+
for (const entry of this.entries) {
|
|
6104
|
+
if (entry.trigger === 'debounced') {
|
|
6105
|
+
const ms = entry.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
6106
|
+
const group = map.get(ms) ?? [];
|
|
6107
|
+
group.push(entry);
|
|
6108
|
+
map.set(ms, group);
|
|
6109
|
+
}
|
|
6110
|
+
}
|
|
6111
|
+
return map;
|
|
6112
|
+
}
|
|
6113
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DerivationLookup, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
6114
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DerivationLookup });
|
|
6115
|
+
}
|
|
6116
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DerivationLookup, decorators: [{
|
|
6117
|
+
type: Injectable
|
|
6118
|
+
}] });
|
|
6119
|
+
|
|
6120
|
+
/**
|
|
6121
|
+
* Sorts derivation entries in topological order based on their dependencies.
|
|
6122
|
+
*
|
|
6123
|
+
* This ensures that derivations are processed in the correct order:
|
|
6124
|
+
* if derivation B depends on field A, and derivation A->A' modifies A,
|
|
6125
|
+
* then A->A' is processed before B.
|
|
6126
|
+
*
|
|
6127
|
+
* Uses Kahn's algorithm for topological sorting, which also provides
|
|
6128
|
+
* natural handling of entries with no dependencies (they come first).
|
|
6129
|
+
*
|
|
6130
|
+
* @param entries - The derivation entries to sort
|
|
6131
|
+
* @returns A new array of entries in topological order
|
|
6132
|
+
*
|
|
6133
|
+
* @example
|
|
6134
|
+
* ```typescript
|
|
6135
|
+
* // Given derivations:
|
|
6136
|
+
* // 1. quantity -> lineTotal (depends on quantity, unitPrice)
|
|
6137
|
+
* // 2. unitPrice -> lineTotal (depends on quantity, unitPrice)
|
|
6138
|
+
* // 3. lineTotal -> grandTotal (depends on lineTotal)
|
|
6139
|
+
* //
|
|
6140
|
+
* // Topological sort ensures lineTotal is computed before grandTotal
|
|
6141
|
+
* const sorted = topologicalSort(entries);
|
|
6142
|
+
* // Result: [1, 2, 3] - derivations that produce lineTotal come before
|
|
6143
|
+
* // those that consume it
|
|
6144
|
+
* ```
|
|
6145
|
+
*
|
|
6146
|
+
* @public
|
|
6147
|
+
*/
|
|
6148
|
+
function topologicalSort(entries) {
|
|
6149
|
+
if (entries.length <= 1) {
|
|
6150
|
+
return [...entries];
|
|
6151
|
+
}
|
|
6152
|
+
// Build adjacency list and in-degree count
|
|
6153
|
+
// An edge A -> B exists if B depends on the target of A
|
|
6154
|
+
const graph = new Map();
|
|
6155
|
+
const inDegree = new Map();
|
|
6156
|
+
// Initialize
|
|
6157
|
+
for (const entry of entries) {
|
|
6158
|
+
graph.set(entry, new Set());
|
|
6159
|
+
inDegree.set(entry, 0);
|
|
6160
|
+
}
|
|
6161
|
+
// Pre-build index: target field -> entries that produce it
|
|
6162
|
+
// This enables O(1) lookup instead of O(n) scanning
|
|
6163
|
+
const producersByTarget = new Map();
|
|
6164
|
+
for (const entry of entries) {
|
|
6165
|
+
// Index by exact target field
|
|
6166
|
+
const producers = producersByTarget.get(entry.targetFieldKey) ?? [];
|
|
6167
|
+
producers.push(entry);
|
|
6168
|
+
producersByTarget.set(entry.targetFieldKey, producers);
|
|
6169
|
+
// Also index array field relative paths for array derivations
|
|
6170
|
+
// If entry targets 'items.$.lineTotal', index under 'lineTotal' too
|
|
6171
|
+
if (entry.targetFieldKey.includes('.$.')) {
|
|
6172
|
+
const relativePath = entry.targetFieldKey.split('.$.')[1];
|
|
6173
|
+
if (relativePath) {
|
|
6174
|
+
const relativeProducers = producersByTarget.get(relativePath) ?? [];
|
|
6175
|
+
relativeProducers.push(entry);
|
|
6176
|
+
producersByTarget.set(relativePath, relativeProducers);
|
|
6177
|
+
}
|
|
6178
|
+
}
|
|
6179
|
+
}
|
|
6180
|
+
// Collect entries with wildcard dependencies (need special handling)
|
|
6181
|
+
const wildcardDependents = [];
|
|
6182
|
+
for (const entry of entries) {
|
|
6183
|
+
if (entry.dependsOn.includes('*')) {
|
|
6184
|
+
wildcardDependents.push(entry);
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
6187
|
+
// Build edges using pre-computed index
|
|
6188
|
+
// For each entry B, find all entries A whose target is in B's dependencies
|
|
6189
|
+
for (const entryB of entries) {
|
|
6190
|
+
for (const dep of entryB.dependsOn) {
|
|
6191
|
+
// Skip wildcards here - handled separately below
|
|
6192
|
+
if (dep === '*')
|
|
6193
|
+
continue;
|
|
6194
|
+
// O(1) lookup: find all producers of this dependency
|
|
6195
|
+
const producers = producersByTarget.get(dep);
|
|
6196
|
+
if (producers) {
|
|
6197
|
+
for (const entryA of producers) {
|
|
6198
|
+
const entryAEdges = graph.get(entryA);
|
|
6199
|
+
if (entryA !== entryB && entryAEdges && !entryAEdges.has(entryB)) {
|
|
6200
|
+
entryAEdges.add(entryB);
|
|
6201
|
+
inDegree.set(entryB, (inDegree.get(entryB) ?? 0) + 1);
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
}
|
|
6207
|
+
// Handle wildcard dependencies: they depend on ALL producers
|
|
6208
|
+
for (const entryB of wildcardDependents) {
|
|
6209
|
+
for (const entryA of entries) {
|
|
6210
|
+
const entryAEdges = graph.get(entryA);
|
|
6211
|
+
if (entryA !== entryB && entryAEdges && !entryAEdges.has(entryB)) {
|
|
6212
|
+
entryAEdges.add(entryB);
|
|
6213
|
+
inDegree.set(entryB, (inDegree.get(entryB) ?? 0) + 1);
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
// Kahn's algorithm: process nodes with in-degree 0
|
|
6218
|
+
const queue = [];
|
|
6219
|
+
const sorted = [];
|
|
6220
|
+
// Start with entries that have no dependencies on other derivation outputs
|
|
6221
|
+
for (const entry of entries) {
|
|
6222
|
+
if (inDegree.get(entry) === 0) {
|
|
6223
|
+
queue.push(entry);
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
while (queue.length > 0) {
|
|
6227
|
+
const current = queue.shift();
|
|
6228
|
+
if (!current)
|
|
6229
|
+
break; // TypeScript guard (shouldn't happen due to while condition)
|
|
6230
|
+
sorted.push(current);
|
|
6231
|
+
// Reduce in-degree for all neighbors
|
|
6232
|
+
const neighbors = graph.get(current) ?? new Set();
|
|
6233
|
+
for (const neighbor of neighbors) {
|
|
6234
|
+
const newDegree = (inDegree.get(neighbor) ?? 0) - 1;
|
|
6235
|
+
inDegree.set(neighbor, newDegree);
|
|
6236
|
+
if (newDegree === 0) {
|
|
6237
|
+
queue.push(neighbor);
|
|
6238
|
+
}
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
// If we couldn't sort all entries, there's a cycle
|
|
6242
|
+
// (but cycles should be caught by cycle-detector, so this is defensive)
|
|
6243
|
+
if (sorted.length !== entries.length) {
|
|
6244
|
+
// Return original order for entries in cycles
|
|
6245
|
+
const sortedSet = new Set(sorted);
|
|
6246
|
+
for (const entry of entries) {
|
|
6247
|
+
if (!sortedSet.has(entry)) {
|
|
6248
|
+
sorted.push(entry);
|
|
6249
|
+
}
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
return sorted;
|
|
6253
|
+
}
|
|
6254
|
+
|
|
6255
|
+
/**
|
|
6256
|
+
* Collects all derivation entries from field definitions.
|
|
6257
|
+
*
|
|
6258
|
+
* This function traverses the field definition tree and extracts:
|
|
6259
|
+
* - Shorthand `derivation` properties on fields
|
|
6260
|
+
* - Full `logic` array entries with `type: 'derivation'`
|
|
6261
|
+
*
|
|
6262
|
+
* The collected entries include dependency information for cycle detection
|
|
6263
|
+
* and reactive evaluation. Entries are sorted topologically so derivations
|
|
6264
|
+
* are processed in dependency order.
|
|
6265
|
+
*
|
|
6266
|
+
* Lookup maps (byTarget, byDependency, etc.) are NOT built here - they are
|
|
6267
|
+
* computed lazily via {@link DerivationLookup} to avoid hidden mutation
|
|
6268
|
+
* and reduce the size of the returned collection.
|
|
6269
|
+
*
|
|
6270
|
+
* @param fields - Array of field definitions to traverse
|
|
6271
|
+
* @returns Collection containing sorted derivation entries
|
|
6272
|
+
*
|
|
6273
|
+
* @example
|
|
6274
|
+
* ```typescript
|
|
6275
|
+
* const fields = [
|
|
6276
|
+
* { key: 'quantity', type: 'number' },
|
|
6277
|
+
* { key: 'unitPrice', type: 'number' },
|
|
6278
|
+
* {
|
|
6279
|
+
* key: 'total',
|
|
6280
|
+
* type: 'number',
|
|
6281
|
+
* derivation: 'formValue.quantity * formValue.unitPrice',
|
|
6282
|
+
* },
|
|
6283
|
+
* ];
|
|
6284
|
+
*
|
|
6285
|
+
* const collection = collectDerivations(fields);
|
|
6286
|
+
* // collection.entries has 1 entry for the 'total' field derivation
|
|
6287
|
+
* ```
|
|
6288
|
+
*
|
|
6289
|
+
* @public
|
|
6290
|
+
*/
|
|
6291
|
+
function collectDerivations(fields) {
|
|
6292
|
+
const entries = [];
|
|
6293
|
+
const context = {};
|
|
6294
|
+
traverseFields(fields, entries, context);
|
|
6295
|
+
// Sort entries in topological order for efficient processing.
|
|
6296
|
+
// This ensures derivations are processed in dependency order,
|
|
6297
|
+
// reducing the number of iterations needed in the applicator.
|
|
6298
|
+
const sortedEntries = topologicalSort(entries);
|
|
6299
|
+
return { entries: sortedEntries };
|
|
6300
|
+
}
|
|
6301
|
+
/**
|
|
6302
|
+
* Recursively traverses field definitions to collect derivations.
|
|
6303
|
+
*
|
|
6304
|
+
* @internal
|
|
6305
|
+
*/
|
|
6306
|
+
function traverseFields(fields, entries, context) {
|
|
6307
|
+
for (const field of fields) {
|
|
6308
|
+
collectFromField(field, entries, context);
|
|
6309
|
+
// Recursively process container fields (page, row, group, array)
|
|
6310
|
+
if (hasChildFields(field)) {
|
|
6311
|
+
const childContext = { ...context };
|
|
6312
|
+
// Update array path context if this is an array field
|
|
6313
|
+
if (field.type === 'array') {
|
|
6314
|
+
childContext.arrayPath = field.key;
|
|
6315
|
+
}
|
|
6316
|
+
traverseFields(normalizeFieldsArray(field.fields), entries, childContext);
|
|
6317
|
+
}
|
|
6318
|
+
}
|
|
6319
|
+
}
|
|
6320
|
+
/**
|
|
6321
|
+
* Collects derivation entries from a single field.
|
|
6322
|
+
*
|
|
6323
|
+
* @internal
|
|
6324
|
+
*/
|
|
6325
|
+
function collectFromField(field, entries, context) {
|
|
6326
|
+
const fieldKey = field.key;
|
|
6327
|
+
if (!fieldKey)
|
|
6328
|
+
return;
|
|
6329
|
+
const validationField = field;
|
|
6330
|
+
// Collect shorthand derivation property
|
|
6331
|
+
if (validationField.derivation) {
|
|
6332
|
+
const entry = createShorthandEntry(fieldKey, validationField.derivation);
|
|
6333
|
+
entries.push(entry);
|
|
6334
|
+
}
|
|
6335
|
+
// Collect logic array derivations
|
|
6336
|
+
if (validationField.logic) {
|
|
6337
|
+
for (const logicConfig of validationField.logic) {
|
|
6338
|
+
if (isDerivationLogicConfig(logicConfig)) {
|
|
6339
|
+
const entry = createLogicEntry(fieldKey, logicConfig, context);
|
|
6340
|
+
entries.push(entry);
|
|
6341
|
+
}
|
|
6342
|
+
}
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
/**
|
|
6346
|
+
* Creates a derivation entry from the shorthand `derivation` property.
|
|
6347
|
+
*
|
|
6348
|
+
* Shorthand derivations:
|
|
6349
|
+
* - Always target the same field they're defined on
|
|
6350
|
+
* - Always apply (condition defaults to true)
|
|
6351
|
+
* - Always trigger on change
|
|
6352
|
+
*
|
|
6353
|
+
* @internal
|
|
6354
|
+
*/
|
|
6355
|
+
function createShorthandEntry(fieldKey, expression) {
|
|
6356
|
+
return {
|
|
6357
|
+
sourceFieldKey: fieldKey,
|
|
6358
|
+
targetFieldKey: fieldKey,
|
|
6359
|
+
dependsOn: extractStringDependencies(expression),
|
|
6360
|
+
condition: true,
|
|
6361
|
+
expression,
|
|
6362
|
+
trigger: 'onChange',
|
|
6363
|
+
isShorthand: true,
|
|
6364
|
+
};
|
|
6365
|
+
}
|
|
6366
|
+
/**
|
|
6367
|
+
* Creates a derivation entry from a full `DerivationLogicConfig`.
|
|
6368
|
+
*
|
|
6369
|
+
* Handles:
|
|
6370
|
+
* - Condition extraction and dependency analysis
|
|
6371
|
+
* - Relative path resolution for array fields
|
|
6372
|
+
* - Multiple value source types (static, expression, function)
|
|
6373
|
+
* - Trigger and debounce configuration
|
|
6374
|
+
*
|
|
6375
|
+
* @internal
|
|
6376
|
+
*/
|
|
6377
|
+
function createLogicEntry(fieldKey, config, context) {
|
|
6378
|
+
const targetFieldKey = resolveTargetFieldKey(config.targetField, fieldKey, context);
|
|
6379
|
+
const dependsOn = extractDependencies(config);
|
|
6380
|
+
const condition = config.condition ?? true;
|
|
6381
|
+
const trigger = config.trigger ?? 'onChange';
|
|
6382
|
+
// Extract debounceMs from debounced configs
|
|
6383
|
+
// The type system ensures debounceMs is only present when trigger is 'debounced'
|
|
6384
|
+
const debounceMs = trigger === 'debounced' ? config.debounceMs : undefined;
|
|
6385
|
+
return {
|
|
6386
|
+
sourceFieldKey: fieldKey,
|
|
6387
|
+
targetFieldKey,
|
|
6388
|
+
dependsOn,
|
|
6389
|
+
condition,
|
|
6390
|
+
value: config.value,
|
|
6391
|
+
expression: config.expression,
|
|
6392
|
+
functionName: config.functionName,
|
|
6393
|
+
trigger,
|
|
6394
|
+
debounceMs,
|
|
6395
|
+
isShorthand: false,
|
|
6396
|
+
originalConfig: config,
|
|
6397
|
+
debugName: config.debugName,
|
|
6398
|
+
};
|
|
6399
|
+
}
|
|
6400
|
+
/**
|
|
6401
|
+
* Resolves a target field key, handling relative paths for array fields.
|
|
6402
|
+
*
|
|
6403
|
+
* Relative paths start with '$.' and reference siblings at the same array index.
|
|
6404
|
+
*
|
|
6405
|
+
* @param targetField - The target field specification from config
|
|
6406
|
+
* @param sourceFieldKey - The source field key
|
|
6407
|
+
* @param context - Current collection context with array path info
|
|
6408
|
+
* @returns Resolved target field key
|
|
6409
|
+
*
|
|
6410
|
+
* @example
|
|
6411
|
+
* ```typescript
|
|
6412
|
+
* // Absolute path - returned as-is
|
|
6413
|
+
* resolveTargetFieldKey('phonePrefix', 'country', {})
|
|
6414
|
+
* // Returns: 'phonePrefix'
|
|
6415
|
+
*
|
|
6416
|
+
* // Relative path in array context
|
|
6417
|
+
* resolveTargetFieldKey('$.lineTotal', 'items[0].quantity', { arrayPath: 'items' })
|
|
6418
|
+
* // Returns: 'items.$.lineTotal'
|
|
6419
|
+
* ```
|
|
6420
|
+
*
|
|
6421
|
+
* @internal
|
|
6422
|
+
*/
|
|
6423
|
+
function resolveTargetFieldKey(targetField, _sourceFieldKey, context) {
|
|
6424
|
+
// Check for relative path indicator
|
|
6425
|
+
if (targetField.startsWith('$.')) {
|
|
6426
|
+
// Extract the relative field name (after '$.')
|
|
6427
|
+
const relativePath = targetField.substring(2);
|
|
6428
|
+
if (context.arrayPath) {
|
|
6429
|
+
// Replace '$' with the array path for pattern matching
|
|
6430
|
+
return `${context.arrayPath}.$.${relativePath}`;
|
|
6431
|
+
}
|
|
6432
|
+
// Not in array context - keep relative path as-is.
|
|
6433
|
+
// This indicates a configuration error (relative path used outside array).
|
|
6434
|
+
// The runtime applicator will handle this gracefully.
|
|
6435
|
+
return targetField;
|
|
6436
|
+
}
|
|
6437
|
+
// Absolute path - return as-is
|
|
6438
|
+
return targetField;
|
|
6439
|
+
}
|
|
6440
|
+
/**
|
|
6441
|
+
* Extracts all field dependencies from a derivation config.
|
|
6442
|
+
*
|
|
6443
|
+
* Combines dependencies from:
|
|
6444
|
+
* - Explicit `dependsOn` array (if provided, takes precedence)
|
|
6445
|
+
* - Condition expression
|
|
6446
|
+
* - Value expression
|
|
6447
|
+
* - Function name (defaults to '*' if no explicit dependsOn)
|
|
6448
|
+
*
|
|
6449
|
+
* @internal
|
|
6450
|
+
*/
|
|
6451
|
+
function extractDependencies(config) {
|
|
6452
|
+
const deps = new Set();
|
|
6453
|
+
// If explicit dependsOn is provided, use it as the primary source
|
|
6454
|
+
// This allows users to override automatic detection and control
|
|
6455
|
+
// when derivations are triggered
|
|
6456
|
+
if (config.dependsOn && config.dependsOn.length > 0) {
|
|
6457
|
+
config.dependsOn.forEach((dep) => deps.add(dep));
|
|
6458
|
+
}
|
|
6459
|
+
else {
|
|
6460
|
+
// Extract from expression (automatic dependency detection)
|
|
6461
|
+
if (config.expression) {
|
|
6462
|
+
const exprDeps = extractStringDependencies(config.expression);
|
|
6463
|
+
exprDeps.forEach((dep) => deps.add(dep));
|
|
6464
|
+
}
|
|
6465
|
+
// Custom functions assume full form access if no explicit dependsOn
|
|
6466
|
+
if (config.functionName) {
|
|
6467
|
+
deps.add('*');
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
// Always extract from condition (these are additional runtime guards)
|
|
6471
|
+
if (config.condition && config.condition !== true) {
|
|
6472
|
+
const conditionDeps = extractExpressionDependencies(config.condition);
|
|
6473
|
+
conditionDeps.forEach((dep) => deps.add(dep));
|
|
6474
|
+
}
|
|
6475
|
+
return Array.from(deps);
|
|
6476
|
+
}
|
|
6477
|
+
|
|
6478
|
+
/**
|
|
6479
|
+
* Detects cycles in the derivation dependency graph.
|
|
6480
|
+
*
|
|
6481
|
+
* Uses Kahn's algorithm variant with DFS to detect cycles.
|
|
6482
|
+
* Bidirectional sync patterns (A→B→A) are allowed because they stabilize
|
|
6483
|
+
* via the equality check at runtime.
|
|
6484
|
+
*
|
|
6485
|
+
* @param collection - The collected derivation entries
|
|
6486
|
+
* @returns Result indicating whether a cycle exists and details if found
|
|
6487
|
+
*
|
|
6488
|
+
* @example
|
|
6489
|
+
* ```typescript
|
|
6490
|
+
* const collection = collectDerivations(fields);
|
|
6491
|
+
* const result = detectCycles(collection);
|
|
6492
|
+
*
|
|
6493
|
+
* if (result.hasCycle) {
|
|
6494
|
+
* console.error(`Cycle detected: ${result.cyclePath?.join(' -> ')}`);
|
|
6495
|
+
* throw new Error(result.errorMessage);
|
|
6496
|
+
* }
|
|
6497
|
+
* ```
|
|
6498
|
+
*
|
|
6499
|
+
* @public
|
|
6500
|
+
*/
|
|
6501
|
+
function detectCycles(collection) {
|
|
6502
|
+
// Build directed graph from derivation entries
|
|
6503
|
+
const graph = buildDependencyGraph(collection);
|
|
6504
|
+
// No entries means no cycles
|
|
6505
|
+
if (graph.size === 0) {
|
|
6506
|
+
return { hasCycle: false };
|
|
6507
|
+
}
|
|
6508
|
+
// Build bidirectional pairs set for cycle exemption
|
|
6509
|
+
const bidirectionalPairs = detectBidirectionalPairs(collection);
|
|
6510
|
+
// Run DFS to detect cycles
|
|
6511
|
+
const result = detectCyclesWithDFS(graph, bidirectionalPairs);
|
|
6512
|
+
// Include bidirectional pairs in result for warnings
|
|
6513
|
+
if (bidirectionalPairs.size > 0) {
|
|
6514
|
+
result.bidirectionalPairs = Array.from(bidirectionalPairs);
|
|
6515
|
+
}
|
|
6516
|
+
return result;
|
|
6517
|
+
}
|
|
6518
|
+
/**
|
|
6519
|
+
* Detects bidirectional derivation pairs (A→B and B→A).
|
|
6520
|
+
*
|
|
6521
|
+
* These patterns are allowed because they stabilize via equality checks.
|
|
6522
|
+
* Example: USD/EUR conversion where both fields derive from each other.
|
|
6523
|
+
*
|
|
6524
|
+
* ## Floating-Point Precision Note
|
|
6525
|
+
*
|
|
6526
|
+
* Bidirectional derivations stabilize via equality checks using exact IEEE 754
|
|
6527
|
+
* comparison with no tolerance. This means:
|
|
6528
|
+
*
|
|
6529
|
+
* - **Integer math**: Safe (e.g., `A = B * 2`, `B = A / 2` where A is even)
|
|
6530
|
+
* - **Floating-point math**: May oscillate due to rounding errors
|
|
6531
|
+
*
|
|
6532
|
+
* For currency conversions or other floating-point operations, consider:
|
|
6533
|
+
* 1. Rounding values in your expression (e.g., `Math.round(value * 100) / 100`)
|
|
6534
|
+
* 2. Using integer cents instead of decimal dollars
|
|
6535
|
+
* 3. Using one-way derivation instead of bidirectional
|
|
6536
|
+
*
|
|
6537
|
+
* @example
|
|
6538
|
+
* ```typescript
|
|
6539
|
+
* // Safe - uses rounding to avoid oscillation
|
|
6540
|
+
* logic: [
|
|
6541
|
+
* {
|
|
6542
|
+
* type: 'derivation',
|
|
6543
|
+
* targetField: 'eur',
|
|
6544
|
+
* expression: 'Math.round(formValue.usd * 0.92 * 100) / 100'
|
|
6545
|
+
* }
|
|
6546
|
+
* ]
|
|
6547
|
+
* ```
|
|
6548
|
+
*
|
|
6549
|
+
* @internal
|
|
6550
|
+
*/
|
|
6551
|
+
function detectBidirectionalPairs(collection) {
|
|
6552
|
+
const pairs = new Set();
|
|
6553
|
+
const edgeSet = new Set();
|
|
6554
|
+
// Build set of all source→target edges
|
|
6555
|
+
for (const entry of collection.entries) {
|
|
6556
|
+
if (entry.sourceFieldKey !== entry.targetFieldKey) {
|
|
6557
|
+
edgeSet.add(`${entry.sourceFieldKey}→${entry.targetFieldKey}`);
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
// Find bidirectional pairs (A→B exists AND B→A exists)
|
|
6561
|
+
for (const entry of collection.entries) {
|
|
6562
|
+
const reverseEdge = `${entry.targetFieldKey}→${entry.sourceFieldKey}`;
|
|
6563
|
+
if (edgeSet.has(reverseEdge)) {
|
|
6564
|
+
// Normalize pair key (alphabetically sorted)
|
|
6565
|
+
const pairKey = entry.sourceFieldKey < entry.targetFieldKey
|
|
6566
|
+
? `${entry.sourceFieldKey}↔${entry.targetFieldKey}`
|
|
6567
|
+
: `${entry.targetFieldKey}↔${entry.sourceFieldKey}`;
|
|
6568
|
+
pairs.add(pairKey);
|
|
6569
|
+
}
|
|
6570
|
+
}
|
|
6571
|
+
return pairs;
|
|
6572
|
+
}
|
|
6573
|
+
/**
|
|
6574
|
+
* Checks if a cycle path represents a bidirectional pair.
|
|
6575
|
+
*
|
|
6576
|
+
* @internal
|
|
6577
|
+
*/
|
|
6578
|
+
function isBidirectionalCycle(cyclePath, bidirectionalPairs) {
|
|
6579
|
+
// Bidirectional cycles are exactly: [A, B, A] (length 3 with first=last)
|
|
6580
|
+
if (cyclePath.length !== 3) {
|
|
6581
|
+
return false;
|
|
6582
|
+
}
|
|
6583
|
+
const [first, second, third] = cyclePath;
|
|
6584
|
+
if (first !== third) {
|
|
6585
|
+
return false;
|
|
6586
|
+
}
|
|
6587
|
+
// Check if this pair is in our bidirectional set
|
|
6588
|
+
const pairKey = first < second ? `${first}↔${second}` : `${second}↔${first}`;
|
|
6589
|
+
return bidirectionalPairs.has(pairKey);
|
|
6590
|
+
}
|
|
6591
|
+
/**
|
|
6592
|
+
* Builds a dependency graph from derivation entries.
|
|
6593
|
+
*
|
|
6594
|
+
* Creates nodes for each field involved in derivations and
|
|
6595
|
+
* edges representing the derivation dependencies.
|
|
6596
|
+
*
|
|
6597
|
+
* Edge direction: sourceField -> targetField
|
|
6598
|
+
* (source triggers derivation that modifies target)
|
|
6599
|
+
*
|
|
6600
|
+
* @internal
|
|
6601
|
+
*/
|
|
6602
|
+
function buildDependencyGraph(collection) {
|
|
6603
|
+
const graph = new Map();
|
|
6604
|
+
// Helper to ensure a node exists
|
|
6605
|
+
const ensureNode = (fieldKey) => {
|
|
6606
|
+
let node = graph.get(fieldKey);
|
|
6607
|
+
if (!node) {
|
|
6608
|
+
node = {
|
|
6609
|
+
fieldKey,
|
|
6610
|
+
dependsOn: new Set(),
|
|
6611
|
+
dependedOnBy: new Set(),
|
|
6612
|
+
};
|
|
6613
|
+
graph.set(fieldKey, node);
|
|
6614
|
+
}
|
|
6615
|
+
return node;
|
|
6616
|
+
};
|
|
6617
|
+
for (const entry of collection.entries) {
|
|
6618
|
+
const sourceNode = ensureNode(entry.sourceFieldKey);
|
|
6619
|
+
const targetNode = ensureNode(entry.targetFieldKey);
|
|
6620
|
+
// For cycle detection, we care about the data flow:
|
|
6621
|
+
// When sourceField changes -> targetField gets updated
|
|
6622
|
+
// So the edge is: sourceField -> targetField
|
|
6623
|
+
//
|
|
6624
|
+
// A cycle exists if: A -> B -> A
|
|
6625
|
+
// (A triggers B, B triggers A)
|
|
6626
|
+
// For self-referential derivations (where source === target), don't add source->target edge.
|
|
6627
|
+
// This includes:
|
|
6628
|
+
// 1. Shorthand derivations (field.derivation = 'expression')
|
|
6629
|
+
// 2. Self-transform derivations (logic: [{ targetField: 'self', ... }])
|
|
6630
|
+
// These are legitimate patterns for computed fields and value transformations,
|
|
6631
|
+
// not actual cycles. The real dependencies come from the expression (tracked in dependsOn).
|
|
6632
|
+
const isSelfReferential = entry.sourceFieldKey === entry.targetFieldKey;
|
|
6633
|
+
if (!isSelfReferential) {
|
|
6634
|
+
// The target depends on the source for its value
|
|
6635
|
+
targetNode.dependsOn.add(entry.sourceFieldKey);
|
|
6636
|
+
sourceNode.dependedOnBy.add(entry.targetFieldKey);
|
|
6637
|
+
}
|
|
6638
|
+
// Also track dependencies from the derivation expression/condition
|
|
6639
|
+
for (const dep of entry.dependsOn) {
|
|
6640
|
+
if (dep !== '*' && dep !== entry.targetFieldKey) {
|
|
6641
|
+
const depNode = ensureNode(dep);
|
|
6642
|
+
targetNode.dependsOn.add(dep);
|
|
6643
|
+
depNode.dependedOnBy.add(entry.targetFieldKey);
|
|
6644
|
+
}
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
return graph;
|
|
6648
|
+
}
|
|
6649
|
+
/**
|
|
6650
|
+
* Detects cycles using depth-first search.
|
|
6651
|
+
*
|
|
6652
|
+
* Uses three-color marking:
|
|
6653
|
+
* - Unvisited (white): Not yet processed
|
|
6654
|
+
* - InProgress (gray): Currently in the DFS stack
|
|
6655
|
+
* - Completed (black): Fully processed
|
|
6656
|
+
*
|
|
6657
|
+
* A cycle is detected when we visit a node that's InProgress,
|
|
6658
|
+
* unless it's a bidirectional sync pattern (allowed).
|
|
6659
|
+
*
|
|
6660
|
+
* @internal
|
|
6661
|
+
*/
|
|
6662
|
+
function detectCyclesWithDFS(graph, bidirectionalPairs) {
|
|
6663
|
+
const visitState = new Map();
|
|
6664
|
+
const parent = new Map();
|
|
6665
|
+
// Initialize all nodes as unvisited
|
|
6666
|
+
for (const fieldKey of graph.keys()) {
|
|
6667
|
+
visitState.set(fieldKey, 0 /* VisitState.Unvisited */);
|
|
6668
|
+
}
|
|
6669
|
+
// Process each unvisited node
|
|
6670
|
+
for (const fieldKey of graph.keys()) {
|
|
6671
|
+
if (visitState.get(fieldKey) === 0 /* VisitState.Unvisited */) {
|
|
6672
|
+
const cycleResult = dfsVisit(fieldKey, graph, visitState, parent, [], bidirectionalPairs);
|
|
6673
|
+
if (cycleResult) {
|
|
6674
|
+
return cycleResult;
|
|
6675
|
+
}
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
6678
|
+
return { hasCycle: false };
|
|
6679
|
+
}
|
|
6680
|
+
/**
|
|
6681
|
+
* DFS visit function that detects back edges (cycles).
|
|
6682
|
+
*
|
|
6683
|
+
* @internal
|
|
6684
|
+
*/
|
|
6685
|
+
function dfsVisit(fieldKey, graph, visitState, parent, path, bidirectionalPairs) {
|
|
6686
|
+
visitState.set(fieldKey, 1 /* VisitState.InProgress */);
|
|
6687
|
+
path.push(fieldKey);
|
|
6688
|
+
const node = graph.get(fieldKey);
|
|
6689
|
+
if (!node) {
|
|
6690
|
+
visitState.set(fieldKey, 2 /* VisitState.Completed */);
|
|
6691
|
+
path.pop();
|
|
6692
|
+
return null;
|
|
6693
|
+
}
|
|
6694
|
+
// Check all nodes that this field's changes would trigger
|
|
6695
|
+
for (const targetFieldKey of node.dependedOnBy) {
|
|
6696
|
+
const targetState = visitState.get(targetFieldKey);
|
|
6697
|
+
if (targetState === 1 /* VisitState.InProgress */) {
|
|
6698
|
+
// Back edge found - potential cycle
|
|
6699
|
+
const cyclePath = extractCyclePath(path, targetFieldKey);
|
|
6700
|
+
// Allow bidirectional sync patterns (A→B→A)
|
|
6701
|
+
// These stabilize via equality checks at runtime
|
|
6702
|
+
if (isBidirectionalCycle(cyclePath, bidirectionalPairs)) {
|
|
6703
|
+
// Continue DFS without reporting this as a cycle
|
|
6704
|
+
continue;
|
|
6705
|
+
}
|
|
6706
|
+
return {
|
|
6707
|
+
hasCycle: true,
|
|
6708
|
+
cyclePath,
|
|
6709
|
+
errorMessage: formatCycleError(cyclePath),
|
|
6710
|
+
};
|
|
6711
|
+
}
|
|
6712
|
+
if (targetState === 0 /* VisitState.Unvisited */) {
|
|
6713
|
+
parent.set(targetFieldKey, fieldKey);
|
|
6714
|
+
const result = dfsVisit(targetFieldKey, graph, visitState, parent, path, bidirectionalPairs);
|
|
6715
|
+
if (result) {
|
|
6716
|
+
return result;
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
visitState.set(fieldKey, 2 /* VisitState.Completed */);
|
|
6721
|
+
path.pop();
|
|
6722
|
+
return null;
|
|
6723
|
+
}
|
|
6724
|
+
/**
|
|
6725
|
+
* Extracts the cycle path from the DFS path.
|
|
6726
|
+
*
|
|
6727
|
+
* @internal
|
|
6728
|
+
*/
|
|
6729
|
+
function extractCyclePath(path, cycleStart) {
|
|
6730
|
+
const cycleStartIndex = path.indexOf(cycleStart);
|
|
6731
|
+
if (cycleStartIndex === -1) {
|
|
6732
|
+
// Shouldn't happen, but handle gracefully
|
|
6733
|
+
return [...path, cycleStart];
|
|
6734
|
+
}
|
|
6735
|
+
// Extract from cycle start to current position, then back to start
|
|
6736
|
+
const cyclePath = path.slice(cycleStartIndex);
|
|
6737
|
+
cyclePath.push(cycleStart); // Close the cycle
|
|
6738
|
+
return cyclePath;
|
|
6739
|
+
}
|
|
6740
|
+
/**
|
|
6741
|
+
* Formats a human-readable error message for a detected cycle.
|
|
6742
|
+
*
|
|
6743
|
+
* @internal
|
|
6744
|
+
*/
|
|
6745
|
+
function formatCycleError(cyclePath) {
|
|
6746
|
+
const pathStr = cyclePath.join(' -> ');
|
|
6747
|
+
return (`Derivation cycle detected: ${pathStr}\n` +
|
|
6748
|
+
`This would cause an infinite loop at runtime. ` +
|
|
6749
|
+
`Remove one of the derivations to break the cycle.`);
|
|
6750
|
+
}
|
|
6751
|
+
/**
|
|
6752
|
+
* Validates a derivation collection and throws if cycles are detected.
|
|
6753
|
+
*
|
|
6754
|
+
* This is the main entry point for cycle validation during form initialization.
|
|
6755
|
+
* Should be called after collecting derivations and before setting up effects.
|
|
6756
|
+
*
|
|
6757
|
+
* In dev mode, logs a warning when bidirectional derivation pairs are detected.
|
|
6758
|
+
* These patterns are allowed but may oscillate with floating-point values.
|
|
6759
|
+
*
|
|
6760
|
+
* @param collection - The collected derivation entries to validate
|
|
6761
|
+
* @param logger - Optional logger for dev-mode warnings
|
|
6762
|
+
* @throws Error if a cycle is detected, with details about the cycle
|
|
6763
|
+
*
|
|
6764
|
+
* @example
|
|
6765
|
+
* ```typescript
|
|
6766
|
+
* const collection = collectDerivations(fields);
|
|
6767
|
+
*
|
|
6768
|
+
* // This will throw if cycles exist
|
|
6769
|
+
* validateNoCycles(collection, logger);
|
|
6770
|
+
*
|
|
6771
|
+
* // Safe to set up derivation effects now
|
|
6772
|
+
* setupDerivationEffects(collection);
|
|
6773
|
+
* ```
|
|
6774
|
+
*
|
|
6775
|
+
* @public
|
|
6776
|
+
*/
|
|
6777
|
+
function validateNoCycles(collection, logger) {
|
|
6778
|
+
const result = detectCycles(collection);
|
|
6779
|
+
if (result.hasCycle) {
|
|
6780
|
+
throw new Error(result.errorMessage);
|
|
6781
|
+
}
|
|
6782
|
+
// Warn about bidirectional patterns in dev mode
|
|
6783
|
+
if (isDevMode() && result.bidirectionalPairs && result.bidirectionalPairs.length > 0 && logger) {
|
|
6784
|
+
logger.warn('[Derivation] Bidirectional derivation patterns detected. ' +
|
|
6785
|
+
'These patterns stabilize via equality checks, but may oscillate with floating-point values ' +
|
|
6786
|
+
'(e.g., currency conversions with rounding). ' +
|
|
6787
|
+
'Consider adding tolerance-based comparisons for numeric values.', result.bidirectionalPairs);
|
|
6788
|
+
}
|
|
6789
|
+
}
|
|
6790
|
+
|
|
6791
|
+
/**
|
|
6792
|
+
* Applies all pending derivations from a collection.
|
|
6793
|
+
*
|
|
6794
|
+
* This function processes derivations in order, evaluating conditions
|
|
6795
|
+
* and computing values. It handles loop prevention through:
|
|
6796
|
+
* 1. Chain tracking (prevents same derivation from running twice in one cycle)
|
|
6797
|
+
* 2. Value equality checks (skips if target already has computed value)
|
|
6798
|
+
* 3. Max iteration limit (safety fallback)
|
|
6799
|
+
*
|
|
6800
|
+
* @param collection - The collected derivation entries
|
|
6801
|
+
* @param context - Context for applying derivations
|
|
6802
|
+
* @param changedFields - Set of field keys that changed (for filtering)
|
|
6803
|
+
* @returns Result of the derivation processing
|
|
6804
|
+
*
|
|
6805
|
+
* @example
|
|
6806
|
+
* ```typescript
|
|
6807
|
+
* const result = applyDerivations(collection, {
|
|
6808
|
+
* formValue: formValueSignal,
|
|
6809
|
+
* rootForm: form(),
|
|
6810
|
+
* derivationFunctions: customFnConfig?.derivations,
|
|
6811
|
+
* logger: inject(DynamicFormLogger),
|
|
6812
|
+
* });
|
|
6813
|
+
*
|
|
6814
|
+
* if (result.maxIterationsReached) {
|
|
6815
|
+
* console.warn('Possible derivation loop detected');
|
|
6816
|
+
* }
|
|
6817
|
+
* ```
|
|
6818
|
+
*
|
|
6819
|
+
* @public
|
|
6820
|
+
*/
|
|
6821
|
+
function applyDerivations(collection, context, changedFields) {
|
|
6822
|
+
const chainContext = createDerivationChainContext();
|
|
6823
|
+
const { derivationLogger } = context;
|
|
6824
|
+
const maxIterations = context.maxIterations ?? MAX_DERIVATION_ITERATIONS;
|
|
6825
|
+
let appliedCount = 0;
|
|
6826
|
+
let skippedCount = 0;
|
|
6827
|
+
let errorCount = 0;
|
|
6828
|
+
let warnCount = 0;
|
|
6829
|
+
// Filter entries based on changed fields
|
|
6830
|
+
const entriesToProcess = changedFields ? getEntriesForChangedFields(collection.entries, changedFields) : collection.entries;
|
|
6831
|
+
// Log cycle start in verbose mode
|
|
6832
|
+
derivationLogger.cycleStart('onChange', entriesToProcess.length);
|
|
6833
|
+
// Process derivations iteratively until no more changes
|
|
6834
|
+
let hasChanges = true;
|
|
6835
|
+
while (hasChanges && chainContext.iteration < maxIterations) {
|
|
6836
|
+
chainContext.iteration++;
|
|
6837
|
+
hasChanges = false;
|
|
6838
|
+
// Log iteration in verbose mode
|
|
6839
|
+
derivationLogger.iteration(chainContext.iteration);
|
|
6840
|
+
for (const entry of entriesToProcess) {
|
|
6841
|
+
const result = tryApplyDerivation(entry, context, chainContext);
|
|
6842
|
+
if (result.applied) {
|
|
6843
|
+
appliedCount++;
|
|
6844
|
+
hasChanges = true;
|
|
6845
|
+
}
|
|
6846
|
+
else if (result.error) {
|
|
6847
|
+
errorCount++;
|
|
6848
|
+
}
|
|
6849
|
+
else if (result.warning) {
|
|
6850
|
+
warnCount++;
|
|
6851
|
+
skippedCount++;
|
|
6852
|
+
}
|
|
6853
|
+
else {
|
|
6854
|
+
skippedCount++;
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
}
|
|
6858
|
+
// Build result
|
|
6859
|
+
const processingResult = {
|
|
6860
|
+
appliedCount,
|
|
6861
|
+
skippedCount,
|
|
6862
|
+
errorCount,
|
|
6863
|
+
warnCount,
|
|
6864
|
+
iterations: chainContext.iteration,
|
|
6865
|
+
maxIterationsReached: chainContext.iteration >= maxIterations,
|
|
6866
|
+
};
|
|
6867
|
+
if (processingResult.maxIterationsReached) {
|
|
6868
|
+
derivationLogger.maxIterationsReached(processingResult, 'onChange');
|
|
6869
|
+
}
|
|
6870
|
+
// Log summary
|
|
6871
|
+
derivationLogger.summary(processingResult, 'onChange');
|
|
6872
|
+
return processingResult;
|
|
6873
|
+
}
|
|
6874
|
+
/**
|
|
6875
|
+
* Gets entries that should be processed based on changed fields.
|
|
6876
|
+
*
|
|
6877
|
+
* Filters entries by checking if any of their dependencies are in the changed fields set.
|
|
6878
|
+
* Also includes all wildcard (*) entries since they depend on any form change.
|
|
6879
|
+
*
|
|
6880
|
+
* Note: For optimal O(k) performance where k = changed fields count, use
|
|
6881
|
+
* DerivationLookup.getEntriesForChangedFields() which uses pre-built indexes.
|
|
6882
|
+
*
|
|
6883
|
+
* @internal
|
|
6884
|
+
*/
|
|
6885
|
+
function getEntriesForChangedFields(entries, changedFields) {
|
|
6886
|
+
return entries.filter((entry) => {
|
|
6887
|
+
// Wildcard entries are always included
|
|
6888
|
+
if (entry.dependsOn.includes('*')) {
|
|
6889
|
+
return true;
|
|
6890
|
+
}
|
|
6891
|
+
// Check if any dependency is in changed fields
|
|
6892
|
+
return entry.dependsOn.some((dep) => changedFields.has(dep));
|
|
6893
|
+
});
|
|
6894
|
+
}
|
|
6895
|
+
/**
|
|
6896
|
+
* Attempts to apply a single derivation.
|
|
6897
|
+
*
|
|
6898
|
+
* **Important:** When a derivation's condition evaluates to `false`, the previously
|
|
6899
|
+
* derived value is NOT automatically cleared. If you need to reset a field when a
|
|
6900
|
+
* condition becomes false, use a separate derivation with an inverted condition
|
|
6901
|
+
* that sets the desired fallback value.
|
|
6902
|
+
*
|
|
6903
|
+
* @example
|
|
6904
|
+
* ```typescript
|
|
6905
|
+
* // Two derivations for conditional value with cleanup:
|
|
6906
|
+
* // When country is USA, set phonePrefix to '+1'
|
|
6907
|
+
* { condition: { field: 'country', operator: '==', value: 'USA' }, targetField: 'phonePrefix', value: '+1' }
|
|
6908
|
+
* // When country is NOT USA, clear phonePrefix
|
|
6909
|
+
* { condition: { field: 'country', operator: '!=', value: 'USA' }, targetField: 'phonePrefix', value: '' }
|
|
6910
|
+
* ```
|
|
6911
|
+
*
|
|
6912
|
+
* @internal
|
|
6913
|
+
*/
|
|
6914
|
+
function tryApplyDerivation(entry, context, chainContext) {
|
|
6915
|
+
const { derivationLogger } = context;
|
|
6916
|
+
// Check if this is an array field derivation (has '$' placeholder)
|
|
6917
|
+
if (isArrayPlaceholderPath(entry.targetFieldKey)) {
|
|
6918
|
+
return tryApplyArrayDerivation(entry, context, chainContext);
|
|
6919
|
+
}
|
|
6920
|
+
const derivationKey = createDerivationKey(entry.sourceFieldKey, entry.targetFieldKey);
|
|
6921
|
+
// Check if already applied in this cycle
|
|
6922
|
+
if (chainContext.appliedDerivations.has(derivationKey)) {
|
|
6923
|
+
derivationLogger.evaluation({
|
|
6924
|
+
debugName: entry.debugName,
|
|
6925
|
+
sourceFieldKey: entry.sourceFieldKey,
|
|
6926
|
+
targetFieldKey: entry.targetFieldKey,
|
|
6927
|
+
result: 'skipped',
|
|
6928
|
+
skipReason: 'already-applied',
|
|
6929
|
+
});
|
|
6930
|
+
return { applied: false, targetFieldKey: entry.targetFieldKey };
|
|
6931
|
+
}
|
|
6932
|
+
// Create evaluation context
|
|
6933
|
+
const formValue = untracked(() => context.formValue());
|
|
6934
|
+
const evalContext = createEvaluationContext(entry, formValue, context);
|
|
6935
|
+
// Evaluate condition
|
|
6936
|
+
if (!evaluateDerivationCondition(entry.condition, evalContext)) {
|
|
6937
|
+
derivationLogger.evaluation({
|
|
6938
|
+
debugName: entry.debugName,
|
|
6939
|
+
sourceFieldKey: entry.sourceFieldKey,
|
|
6940
|
+
targetFieldKey: entry.targetFieldKey,
|
|
6941
|
+
result: 'skipped',
|
|
6942
|
+
skipReason: 'condition-false',
|
|
6943
|
+
});
|
|
6944
|
+
return { applied: false, targetFieldKey: entry.targetFieldKey };
|
|
6945
|
+
}
|
|
6946
|
+
// Compute derived value
|
|
6947
|
+
let newValue;
|
|
6948
|
+
try {
|
|
6949
|
+
newValue = computeDerivedValue(entry, evalContext, context);
|
|
6950
|
+
}
|
|
6951
|
+
catch (error) {
|
|
6952
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6953
|
+
context.logger.error(formatDerivationError(entry, 'compute', errorMessage));
|
|
6954
|
+
derivationLogger.evaluation({
|
|
6955
|
+
debugName: entry.debugName,
|
|
6956
|
+
sourceFieldKey: entry.sourceFieldKey,
|
|
6957
|
+
targetFieldKey: entry.targetFieldKey,
|
|
6958
|
+
result: 'error',
|
|
6959
|
+
error: errorMessage,
|
|
6960
|
+
});
|
|
6961
|
+
return {
|
|
6962
|
+
applied: false,
|
|
6963
|
+
targetFieldKey: entry.targetFieldKey,
|
|
6964
|
+
error: errorMessage,
|
|
6965
|
+
};
|
|
6966
|
+
}
|
|
6967
|
+
// Check if value actually changed using exact equality.
|
|
6968
|
+
// Note: This uses isEqual which performs deep comparison with exact IEEE 754
|
|
6969
|
+
// equality for numbers. Bidirectional derivations with floating-point math
|
|
6970
|
+
// may oscillate due to rounding errors. Use explicit rounding in expressions
|
|
6971
|
+
// or integer arithmetic to avoid this issue.
|
|
6972
|
+
const currentValue = getNestedValue(formValue, entry.targetFieldKey);
|
|
6973
|
+
if (isEqual(currentValue, newValue)) {
|
|
6974
|
+
derivationLogger.evaluation({
|
|
6975
|
+
debugName: entry.debugName,
|
|
6976
|
+
sourceFieldKey: entry.sourceFieldKey,
|
|
6977
|
+
targetFieldKey: entry.targetFieldKey,
|
|
6978
|
+
result: 'skipped',
|
|
6979
|
+
skipReason: 'value-unchanged',
|
|
6980
|
+
});
|
|
6981
|
+
return { applied: false, targetFieldKey: entry.targetFieldKey };
|
|
6982
|
+
}
|
|
6983
|
+
// Apply the value
|
|
6984
|
+
try {
|
|
6985
|
+
const wasApplied = applyValueToForm(entry.targetFieldKey, newValue, context.rootForm, context.logger, context.warningTracker);
|
|
6986
|
+
if (wasApplied) {
|
|
6987
|
+
chainContext.appliedDerivations.add(derivationKey);
|
|
6988
|
+
derivationLogger.evaluation({
|
|
6989
|
+
debugName: entry.debugName,
|
|
6990
|
+
sourceFieldKey: entry.sourceFieldKey,
|
|
6991
|
+
targetFieldKey: entry.targetFieldKey,
|
|
6992
|
+
result: 'applied',
|
|
6993
|
+
previousValue: currentValue,
|
|
6994
|
+
newValue,
|
|
6995
|
+
});
|
|
6996
|
+
return { applied: true, targetFieldKey: entry.targetFieldKey, newValue };
|
|
6997
|
+
}
|
|
6998
|
+
else {
|
|
6999
|
+
// Field not found - this is a warning, not an error
|
|
7000
|
+
// The warning was already logged by warnMissingField
|
|
7001
|
+
derivationLogger.evaluation({
|
|
7002
|
+
debugName: entry.debugName,
|
|
7003
|
+
sourceFieldKey: entry.sourceFieldKey,
|
|
7004
|
+
targetFieldKey: entry.targetFieldKey,
|
|
7005
|
+
result: 'skipped',
|
|
7006
|
+
skipReason: 'target-not-found',
|
|
7007
|
+
});
|
|
7008
|
+
return {
|
|
7009
|
+
applied: false,
|
|
7010
|
+
targetFieldKey: entry.targetFieldKey,
|
|
7011
|
+
warning: `Target field '${entry.targetFieldKey}' not found`,
|
|
7012
|
+
};
|
|
7013
|
+
}
|
|
7014
|
+
}
|
|
7015
|
+
catch (error) {
|
|
7016
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
7017
|
+
context.logger.error(formatDerivationError(entry, 'apply', errorMessage));
|
|
7018
|
+
derivationLogger.evaluation({
|
|
7019
|
+
debugName: entry.debugName,
|
|
7020
|
+
sourceFieldKey: entry.sourceFieldKey,
|
|
7021
|
+
targetFieldKey: entry.targetFieldKey,
|
|
7022
|
+
result: 'error',
|
|
7023
|
+
error: errorMessage,
|
|
7024
|
+
});
|
|
7025
|
+
return {
|
|
7026
|
+
applied: false,
|
|
7027
|
+
targetFieldKey: entry.targetFieldKey,
|
|
7028
|
+
error: errorMessage,
|
|
7029
|
+
};
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
/**
|
|
7033
|
+
* Handles array field derivations with '$' placeholder.
|
|
7034
|
+
*
|
|
7035
|
+
* Iterates over all array items and applies the derivation for each,
|
|
7036
|
+
* resolving '$' to the actual index and creating scoped evaluation contexts.
|
|
7037
|
+
*
|
|
7038
|
+
* @internal
|
|
7039
|
+
*/
|
|
7040
|
+
function tryApplyArrayDerivation(entry, context, chainContext) {
|
|
7041
|
+
const formValue = untracked(() => context.formValue());
|
|
7042
|
+
// Parse the target path using path utilities
|
|
7043
|
+
const pathInfo = parseArrayPath(entry.targetFieldKey);
|
|
7044
|
+
if (!pathInfo.isArrayPath) {
|
|
7045
|
+
return { applied: false, targetFieldKey: entry.targetFieldKey, error: 'Invalid array derivation path' };
|
|
7046
|
+
}
|
|
7047
|
+
const { arrayPath } = pathInfo;
|
|
7048
|
+
// Get the array from form values
|
|
7049
|
+
const arrayValue = getNestedValue(formValue, arrayPath);
|
|
7050
|
+
if (!Array.isArray(arrayValue)) {
|
|
7051
|
+
return { applied: false, targetFieldKey: entry.targetFieldKey };
|
|
7052
|
+
}
|
|
7053
|
+
let appliedAny = false;
|
|
7054
|
+
// Process each array item
|
|
7055
|
+
for (let i = 0; i < arrayValue.length; i++) {
|
|
7056
|
+
const resolvedTargetPath = resolveArrayPath(entry.targetFieldKey, i);
|
|
7057
|
+
const derivationKey = createDerivationKey(entry.sourceFieldKey, resolvedTargetPath);
|
|
7058
|
+
// Skip if already applied in this cycle
|
|
7059
|
+
if (chainContext.appliedDerivations.has(derivationKey)) {
|
|
7060
|
+
continue;
|
|
7061
|
+
}
|
|
7062
|
+
// Create evaluation context scoped to this array item
|
|
7063
|
+
const arrayItem = arrayValue[i];
|
|
7064
|
+
const evalContext = createArrayItemEvaluationContext(entry, arrayItem, formValue, i, arrayPath, context);
|
|
7065
|
+
// Evaluate condition
|
|
7066
|
+
if (!evaluateDerivationCondition(entry.condition, evalContext)) {
|
|
7067
|
+
continue;
|
|
7068
|
+
}
|
|
7069
|
+
// Compute derived value
|
|
7070
|
+
let newValue;
|
|
7071
|
+
try {
|
|
7072
|
+
newValue = computeDerivedValue(entry, evalContext, context);
|
|
7073
|
+
}
|
|
7074
|
+
catch (error) {
|
|
7075
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
7076
|
+
context.logger.error(formatDerivationError(entry, 'compute', `[index ${i}] ${errorMessage}`));
|
|
7077
|
+
continue;
|
|
7078
|
+
}
|
|
7079
|
+
// Check if value actually changed
|
|
7080
|
+
const currentValue = getNestedValue(formValue, resolvedTargetPath);
|
|
7081
|
+
if (isEqual(currentValue, newValue)) {
|
|
7082
|
+
continue;
|
|
7083
|
+
}
|
|
7084
|
+
// Apply the value
|
|
7085
|
+
try {
|
|
7086
|
+
applyValueToForm(resolvedTargetPath, newValue, context.rootForm, context.logger, context.warningTracker);
|
|
7087
|
+
chainContext.appliedDerivations.add(derivationKey);
|
|
7088
|
+
appliedAny = true;
|
|
7089
|
+
}
|
|
7090
|
+
catch (error) {
|
|
7091
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
7092
|
+
context.logger.error(formatDerivationError(entry, 'apply', `[index ${i}] ${errorMessage}`));
|
|
7093
|
+
}
|
|
7094
|
+
}
|
|
7095
|
+
return { applied: appliedAny, targetFieldKey: entry.targetFieldKey };
|
|
7096
|
+
}
|
|
7097
|
+
/**
|
|
7098
|
+
* Creates an evaluation context for derivation processing.
|
|
7099
|
+
*
|
|
7100
|
+
* @internal
|
|
7101
|
+
*/
|
|
7102
|
+
function createEvaluationContext(entry, formValue, context) {
|
|
7103
|
+
const fieldValue = getNestedValue(formValue, entry.sourceFieldKey);
|
|
7104
|
+
return {
|
|
7105
|
+
fieldValue,
|
|
7106
|
+
formValue,
|
|
7107
|
+
fieldPath: entry.sourceFieldKey,
|
|
7108
|
+
customFunctions: context.customFunctions,
|
|
7109
|
+
logger: context.logger,
|
|
7110
|
+
};
|
|
7111
|
+
}
|
|
7112
|
+
/**
|
|
7113
|
+
* Creates an evaluation context scoped to a specific array item.
|
|
7114
|
+
*
|
|
7115
|
+
* For array derivations, `formValue` in the expression should reference
|
|
7116
|
+
* the current array item's values, not the root form values.
|
|
7117
|
+
*
|
|
7118
|
+
* @internal
|
|
7119
|
+
*/
|
|
7120
|
+
function createArrayItemEvaluationContext(entry, arrayItem, rootFormValue, itemIndex, arrayPath, context) {
|
|
7121
|
+
// For array item expressions, formValue should be the array item
|
|
7122
|
+
// This allows expressions like 'formValue.quantity * formValue.unitPrice'
|
|
7123
|
+
// to work within the context of each array item
|
|
7124
|
+
return {
|
|
7125
|
+
fieldValue: arrayItem,
|
|
7126
|
+
formValue: arrayItem,
|
|
7127
|
+
fieldPath: `${arrayPath}.${itemIndex}`,
|
|
7128
|
+
customFunctions: context.customFunctions,
|
|
7129
|
+
logger: context.logger,
|
|
7130
|
+
// Provide access to root form value for cross-scope references
|
|
7131
|
+
rootFormValue,
|
|
7132
|
+
arrayIndex: itemIndex,
|
|
7133
|
+
arrayPath,
|
|
7134
|
+
};
|
|
7135
|
+
}
|
|
7136
|
+
/**
|
|
7137
|
+
* Evaluates the derivation condition.
|
|
7138
|
+
*
|
|
7139
|
+
* @internal
|
|
7140
|
+
*/
|
|
7141
|
+
function evaluateDerivationCondition(condition, context) {
|
|
7142
|
+
if (typeof condition === 'boolean') {
|
|
7143
|
+
return condition;
|
|
7144
|
+
}
|
|
7145
|
+
return evaluateCondition(condition, context);
|
|
7146
|
+
}
|
|
7147
|
+
/**
|
|
7148
|
+
* Computes the derived value based on the entry configuration.
|
|
7149
|
+
*
|
|
7150
|
+
* @internal
|
|
7151
|
+
*/
|
|
7152
|
+
function computeDerivedValue(entry, evalContext, applicatorContext) {
|
|
7153
|
+
// Static value
|
|
7154
|
+
if (entry.value !== undefined) {
|
|
7155
|
+
return entry.value;
|
|
7156
|
+
}
|
|
7157
|
+
// Expression
|
|
7158
|
+
if (entry.expression) {
|
|
7159
|
+
return ExpressionParser.evaluate(entry.expression, evalContext);
|
|
7160
|
+
}
|
|
7161
|
+
// Custom function
|
|
7162
|
+
if (entry.functionName) {
|
|
7163
|
+
const fn = applicatorContext.derivationFunctions?.[entry.functionName];
|
|
7164
|
+
if (!fn) {
|
|
7165
|
+
throw new Error(`Derivation function '${entry.functionName}' not found in customFnConfig.derivations`);
|
|
7166
|
+
}
|
|
7167
|
+
return fn(evalContext);
|
|
7168
|
+
}
|
|
7169
|
+
// No value source specified
|
|
7170
|
+
throw new Error(`Derivation for ${entry.targetFieldKey} has no value source. ` + `Specify 'value', 'expression', or 'functionName'.`);
|
|
7171
|
+
}
|
|
7172
|
+
/**
|
|
7173
|
+
* Applies a value to the form at the specified path.
|
|
7174
|
+
*
|
|
7175
|
+
* Uses bracket notation to access child FieldTrees, following the same pattern
|
|
7176
|
+
* as group-field and array-field components. Fields are accessed as signals
|
|
7177
|
+
* and their value is set via the `.value.set()` method.
|
|
7178
|
+
*
|
|
7179
|
+
* Handles nested paths and array paths with '$' placeholder.
|
|
7180
|
+
*
|
|
7181
|
+
* @internal
|
|
7182
|
+
*/
|
|
7183
|
+
function applyValueToForm(targetPath, value, rootForm, logger, warningTracker) {
|
|
7184
|
+
// Handle simple top-level fields
|
|
7185
|
+
if (!targetPath.includes('.')) {
|
|
7186
|
+
return setFieldValue(rootForm, targetPath, value, logger, warningTracker);
|
|
7187
|
+
}
|
|
7188
|
+
// Handle nested paths (e.g., 'address.city' or 'items.$.quantity')
|
|
7189
|
+
const parts = targetPath.split('.');
|
|
7190
|
+
let current = rootForm;
|
|
7191
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
7192
|
+
const part = parts[i];
|
|
7193
|
+
// Handle array index placeholder '$'
|
|
7194
|
+
if (part === '$') {
|
|
7195
|
+
// This case should be resolved at collection time
|
|
7196
|
+
// If we still have '$', we need to skip for now
|
|
7197
|
+
return false;
|
|
7198
|
+
}
|
|
7199
|
+
// Navigate to the next level using bracket notation
|
|
7200
|
+
const next = current[part];
|
|
7201
|
+
if (next === undefined || next === null) {
|
|
7202
|
+
warnMissingField(targetPath, logger, warningTracker);
|
|
7203
|
+
return false; // Path doesn't exist
|
|
7204
|
+
}
|
|
7205
|
+
current = next;
|
|
7206
|
+
}
|
|
7207
|
+
// Set the final value
|
|
7208
|
+
const finalPart = parts[parts.length - 1];
|
|
7209
|
+
return setFieldValue(current, finalPart, value, logger, warningTracker);
|
|
7210
|
+
}
|
|
7211
|
+
/**
|
|
7212
|
+
* Error message prefix for derivation-related errors.
|
|
7213
|
+
* @internal
|
|
7214
|
+
*/
|
|
7215
|
+
const ERROR_PREFIX = '[Derivation]';
|
|
7216
|
+
/**
|
|
7217
|
+
* Creates a standardized error message for derivation errors.
|
|
7218
|
+
* @internal
|
|
7219
|
+
*/
|
|
7220
|
+
function formatDerivationError(entry, phase, message) {
|
|
7221
|
+
const source = entry.expression
|
|
7222
|
+
? `expression: "${entry.expression.substring(0, 50)}${entry.expression.length > 50 ? '...' : ''}"`
|
|
7223
|
+
: entry.functionName
|
|
7224
|
+
? `function: "${entry.functionName}"`
|
|
7225
|
+
: entry.value !== undefined
|
|
7226
|
+
? 'static value'
|
|
7227
|
+
: 'unknown source';
|
|
7228
|
+
return `${ERROR_PREFIX} Failed to ${phase} derivation (${entry.sourceFieldKey} → ${entry.targetFieldKey}, ${source}): ${message}`;
|
|
7229
|
+
}
|
|
7230
|
+
/**
|
|
7231
|
+
* Sets a field value using the Angular Signal Forms pattern.
|
|
7232
|
+
*
|
|
7233
|
+
* Accesses the child field via bracket notation, drills down to find
|
|
7234
|
+
* the WritableSignal, and uses isWritableSignal for type-safe signal detection.
|
|
7235
|
+
*
|
|
7236
|
+
* Angular Signal Forms structure:
|
|
7237
|
+
* - form[key] is a callable function (field accessor)
|
|
7238
|
+
* - form[key]() returns the field instance with { value: WritableSignal<T> }
|
|
7239
|
+
* - form[key]().value.set(newValue) sets the value
|
|
7240
|
+
*
|
|
7241
|
+
* @returns true if the value was successfully set, false if field not found
|
|
7242
|
+
*
|
|
7243
|
+
* @internal
|
|
7244
|
+
*/
|
|
7245
|
+
function setFieldValue(parent, fieldKey, value, logger, warningTracker) {
|
|
7246
|
+
// Access child field via bracket notation (same pattern as group-field/array-field)
|
|
7247
|
+
const fieldAccessor = parent[fieldKey];
|
|
7248
|
+
if (fieldAccessor === undefined || fieldAccessor === null) {
|
|
7249
|
+
warnMissingField(fieldKey, logger, warningTracker);
|
|
7250
|
+
return false;
|
|
7251
|
+
}
|
|
7252
|
+
// Angular Signal Forms: field accessor is a callable function
|
|
7253
|
+
if (typeof fieldAccessor !== 'function') {
|
|
7254
|
+
warnMissingField(fieldKey, logger, warningTracker);
|
|
7255
|
+
return false;
|
|
7256
|
+
}
|
|
7257
|
+
// Call the accessor to get the field instance
|
|
7258
|
+
const fieldInstance = fieldAccessor();
|
|
7259
|
+
if (!fieldInstance || typeof fieldInstance !== 'object' || !('value' in fieldInstance)) {
|
|
7260
|
+
warnMissingField(fieldKey, logger, warningTracker);
|
|
7261
|
+
return false;
|
|
7262
|
+
}
|
|
7263
|
+
// Get the value signal and verify it's a WritableSignal
|
|
7264
|
+
const valueSignal = fieldInstance.value;
|
|
7265
|
+
if (isWritableSignal(valueSignal)) {
|
|
7266
|
+
valueSignal.set(value);
|
|
7267
|
+
return true;
|
|
7268
|
+
}
|
|
7269
|
+
else {
|
|
7270
|
+
warnMissingField(fieldKey, logger, warningTracker);
|
|
7271
|
+
return false;
|
|
7272
|
+
}
|
|
7273
|
+
}
|
|
7274
|
+
/**
|
|
7275
|
+
* Logs a warning for a missing target field (once per field per form instance).
|
|
7276
|
+
*
|
|
7277
|
+
* Uses the provided warning tracker to avoid log spam. If no tracker is provided,
|
|
7278
|
+
* the warning will be logged every time.
|
|
7279
|
+
*
|
|
7280
|
+
* @internal
|
|
7281
|
+
*/
|
|
7282
|
+
function warnMissingField(fieldKey, logger, warningTracker) {
|
|
7283
|
+
// If tracker is provided, check if we've already warned about this field
|
|
7284
|
+
if (warningTracker) {
|
|
7285
|
+
if (warningTracker.warnedFields.has(fieldKey)) {
|
|
7286
|
+
return;
|
|
7287
|
+
}
|
|
7288
|
+
warningTracker.warnedFields.add(fieldKey);
|
|
7289
|
+
}
|
|
7290
|
+
logger?.warn(`${ERROR_PREFIX} Target field '${fieldKey}' not found in form. ` +
|
|
7291
|
+
`Ensure the field is defined in your form configuration. ` +
|
|
7292
|
+
`This warning is shown once per field.`);
|
|
7293
|
+
}
|
|
7294
|
+
/**
|
|
7295
|
+
* Processes derivations for a specific trigger type.
|
|
7296
|
+
*
|
|
7297
|
+
* Filters entries by trigger type and applies them.
|
|
7298
|
+
*
|
|
7299
|
+
* @param collection - The collected derivation entries
|
|
7300
|
+
* @param trigger - The trigger type to filter by
|
|
7301
|
+
* @param context - Context for applying derivations
|
|
7302
|
+
* @param changedFields - Set of field keys that changed (for filtering)
|
|
7303
|
+
* @returns Result of the derivation processing
|
|
7304
|
+
*
|
|
7305
|
+
* @public
|
|
7306
|
+
*/
|
|
7307
|
+
function applyDerivationsForTrigger(collection, trigger, context, changedFields) {
|
|
7308
|
+
// Filter entries by trigger type
|
|
7309
|
+
const filteredEntries = collection.entries.filter((entry) => {
|
|
7310
|
+
if (trigger === 'onChange') {
|
|
7311
|
+
return !entry.trigger || entry.trigger === 'onChange';
|
|
7312
|
+
}
|
|
7313
|
+
return entry.trigger === trigger;
|
|
7314
|
+
});
|
|
7315
|
+
// Create a minimal collection with filtered entries
|
|
7316
|
+
const filteredCollection = {
|
|
7317
|
+
entries: filteredEntries,
|
|
7318
|
+
};
|
|
7319
|
+
return applyDerivations(filteredCollection, context, changedFields);
|
|
7320
|
+
}
|
|
7321
|
+
/**
|
|
7322
|
+
* Gets all debounced derivation entries from a collection.
|
|
7323
|
+
*
|
|
7324
|
+
* Use this to extract debounced entries for separate processing with debounce timers.
|
|
7325
|
+
*
|
|
7326
|
+
* @param collection - The collected derivation entries
|
|
7327
|
+
* @returns Array of debounced derivation entries
|
|
7328
|
+
*
|
|
7329
|
+
* @public
|
|
7330
|
+
*/
|
|
7331
|
+
function getDebouncedDerivationEntries(collection) {
|
|
7332
|
+
return collection.entries.filter((entry) => entry.trigger === 'debounced');
|
|
7333
|
+
}
|
|
7334
|
+
|
|
7335
|
+
/**
|
|
7336
|
+
* Injection token for the derivation warning tracker.
|
|
7337
|
+
* Provided at form component level for instance-scoped tracking.
|
|
7338
|
+
*
|
|
7339
|
+
* @public
|
|
7340
|
+
*/
|
|
7341
|
+
const DERIVATION_WARNING_TRACKER = new InjectionToken('DerivationWarningTracker', {
|
|
7342
|
+
providedIn: null,
|
|
7343
|
+
factory: () => ({ warnedFields: new Set() }),
|
|
7344
|
+
});
|
|
7345
|
+
/**
|
|
7346
|
+
* Creates a fresh warning tracker instance.
|
|
7347
|
+
*
|
|
7348
|
+
* @returns A new DerivationWarningTracker with an empty warnedFields Set
|
|
7349
|
+
*
|
|
7350
|
+
* @public
|
|
7351
|
+
*/
|
|
7352
|
+
function createDerivationWarningTracker() {
|
|
7353
|
+
return { warnedFields: new Set() };
|
|
7354
|
+
}
|
|
7355
|
+
|
|
7356
|
+
/**
|
|
7357
|
+
* Prefix for derivation debug log messages.
|
|
7358
|
+
* @internal
|
|
7359
|
+
*/
|
|
7360
|
+
const LOG_PREFIX = '[Derivation]';
|
|
7361
|
+
/**
|
|
7362
|
+
* Logs a summary of derivation processing results.
|
|
7363
|
+
*
|
|
7364
|
+
* Only logs when:
|
|
7365
|
+
* - Config level is 'summary' or 'verbose'
|
|
7366
|
+
* - At least one derivation was applied or an error occurred
|
|
7367
|
+
*
|
|
7368
|
+
* @param result - The derivation processing result
|
|
7369
|
+
* @param trigger - The trigger type ('onChange' or 'debounced')
|
|
7370
|
+
* @param logger - Logger instance
|
|
7371
|
+
* @param config - Debug logging configuration
|
|
7372
|
+
*
|
|
7373
|
+
* @public
|
|
7374
|
+
*/
|
|
7375
|
+
function logDerivationSummary(result, trigger, logger, config) {
|
|
7376
|
+
if (!shouldLog(config, 'summary'))
|
|
7377
|
+
return;
|
|
7378
|
+
// Only log if something interesting happened
|
|
7379
|
+
if (result.appliedCount === 0 && result.errorCount === 0)
|
|
7380
|
+
return;
|
|
7381
|
+
logger.debug(`${LOG_PREFIX} Cycle complete (${trigger})`, {
|
|
7382
|
+
applied: result.appliedCount,
|
|
7383
|
+
skipped: result.skippedCount,
|
|
7384
|
+
errors: result.errorCount,
|
|
7385
|
+
iterations: result.iterations,
|
|
7386
|
+
});
|
|
7387
|
+
}
|
|
7388
|
+
/**
|
|
7389
|
+
* Logs when max iterations are reached (always logged as warning).
|
|
7390
|
+
*
|
|
7391
|
+
* @param result - The derivation processing result
|
|
7392
|
+
* @param trigger - The trigger type
|
|
7393
|
+
* @param logger - Logger instance
|
|
7394
|
+
*
|
|
7395
|
+
* @public
|
|
7396
|
+
*/
|
|
7397
|
+
function logMaxIterationsReached(result, trigger, logger) {
|
|
7398
|
+
logger.warn(`${LOG_PREFIX} Processing reached max iterations (${trigger}). ` +
|
|
7399
|
+
`This may indicate a loop in derivation logic. ` +
|
|
7400
|
+
`Applied: ${result.appliedCount}, Skipped: ${result.skippedCount}, Errors: ${result.errorCount}`);
|
|
7401
|
+
}
|
|
7402
|
+
/**
|
|
7403
|
+
* Logs a verbose entry for a single derivation evaluation.
|
|
7404
|
+
*
|
|
7405
|
+
* Only logs when config level is 'verbose'.
|
|
7406
|
+
*
|
|
7407
|
+
* @param entry - The derivation log entry with evaluation details
|
|
7408
|
+
* @param logger - Logger instance
|
|
7409
|
+
* @param config - Debug logging configuration
|
|
7410
|
+
*
|
|
7411
|
+
* @internal
|
|
7412
|
+
*/
|
|
7413
|
+
function logDerivationEvaluation(entry, logger, config) {
|
|
7414
|
+
if (!shouldLog(config, 'verbose'))
|
|
7415
|
+
return;
|
|
7416
|
+
const name = entry.debugName ? `"${entry.debugName}"` : `${entry.sourceFieldKey} -> ${entry.targetFieldKey}`;
|
|
7417
|
+
switch (entry.result) {
|
|
7418
|
+
case 'applied':
|
|
7419
|
+
logger.debug(`${LOG_PREFIX} -> Applied: ${name}`, {
|
|
7420
|
+
source: entry.sourceFieldKey,
|
|
7421
|
+
target: entry.targetFieldKey,
|
|
7422
|
+
previousValue: entry.previousValue,
|
|
7423
|
+
newValue: entry.newValue,
|
|
7424
|
+
...(entry.durationMs !== undefined && { durationMs: entry.durationMs }),
|
|
7425
|
+
});
|
|
7426
|
+
break;
|
|
7427
|
+
case 'skipped':
|
|
7428
|
+
logger.debug(`${LOG_PREFIX} -> Skipped: ${name} (${formatSkipReason(entry.skipReason)})`, {
|
|
7429
|
+
source: entry.sourceFieldKey,
|
|
7430
|
+
target: entry.targetFieldKey,
|
|
7431
|
+
reason: entry.skipReason,
|
|
7432
|
+
});
|
|
7433
|
+
break;
|
|
7434
|
+
case 'error':
|
|
7435
|
+
logger.debug(`${LOG_PREFIX} -> Error: ${name}`, {
|
|
7436
|
+
source: entry.sourceFieldKey,
|
|
7437
|
+
target: entry.targetFieldKey,
|
|
7438
|
+
error: entry.error,
|
|
7439
|
+
});
|
|
7440
|
+
break;
|
|
7441
|
+
}
|
|
7442
|
+
}
|
|
7443
|
+
/**
|
|
7444
|
+
* Formats the skip reason for human-readable output.
|
|
7445
|
+
*
|
|
7446
|
+
* @internal
|
|
7447
|
+
*/
|
|
7448
|
+
function formatSkipReason(reason) {
|
|
7449
|
+
switch (reason) {
|
|
7450
|
+
case 'condition-false':
|
|
7451
|
+
return 'condition not met';
|
|
7452
|
+
case 'value-unchanged':
|
|
7453
|
+
return 'value unchanged';
|
|
7454
|
+
case 'already-applied':
|
|
7455
|
+
return 'already applied this cycle';
|
|
7456
|
+
default:
|
|
7457
|
+
return 'unknown';
|
|
7458
|
+
}
|
|
7459
|
+
}
|
|
7460
|
+
/**
|
|
7461
|
+
* Logs the start of a derivation processing cycle.
|
|
7462
|
+
*
|
|
7463
|
+
* Only logs when config level is 'verbose'.
|
|
7464
|
+
*
|
|
7465
|
+
* @param trigger - The trigger type ('onChange' or 'debounced')
|
|
7466
|
+
* @param entryCount - Number of derivations to process in this cycle
|
|
7467
|
+
* @param logger - Logger instance
|
|
7468
|
+
* @param config - Debug logging configuration
|
|
7469
|
+
*
|
|
7470
|
+
* @internal
|
|
7471
|
+
*/
|
|
7472
|
+
function logDerivationCycleStart(trigger, entryCount, logger, config) {
|
|
7473
|
+
if (!shouldLog(config, 'verbose'))
|
|
7474
|
+
return;
|
|
7475
|
+
logger.debug(`${LOG_PREFIX} Starting cycle (${trigger}) with ${entryCount} derivation(s)`);
|
|
7476
|
+
}
|
|
7477
|
+
/**
|
|
7478
|
+
* Logs the start of a derivation iteration within a cycle.
|
|
7479
|
+
*
|
|
7480
|
+
* Only logs when config level is 'verbose'.
|
|
7481
|
+
*
|
|
7482
|
+
* @param iteration - Current iteration number (1-based)
|
|
7483
|
+
* @param logger - Logger instance
|
|
7484
|
+
* @param config - Debug logging configuration
|
|
7485
|
+
*
|
|
7486
|
+
* @internal
|
|
7487
|
+
*/
|
|
7488
|
+
function logDerivationIteration(iteration, logger, config) {
|
|
7489
|
+
if (!shouldLog(config, 'verbose'))
|
|
7490
|
+
return;
|
|
7491
|
+
logger.debug(`${LOG_PREFIX} Iteration ${iteration}`);
|
|
7492
|
+
}
|
|
7493
|
+
|
|
7494
|
+
/**
|
|
7495
|
+
* Active implementation of DerivationLogger that performs actual logging.
|
|
7496
|
+
*
|
|
7497
|
+
* @internal
|
|
7498
|
+
*/
|
|
7499
|
+
class ActiveDerivationLogger {
|
|
7500
|
+
logger;
|
|
7501
|
+
config = createDefaultDerivationLogConfig();
|
|
7502
|
+
/**
|
|
7503
|
+
* Updates the log configuration.
|
|
7504
|
+
*/
|
|
7505
|
+
setConfig(config) {
|
|
7506
|
+
this.config = config;
|
|
7507
|
+
}
|
|
7508
|
+
cycleStart(trigger, entryCount) {
|
|
7509
|
+
if (!shouldLog(this.config, 'verbose'))
|
|
7510
|
+
return;
|
|
7511
|
+
this.logger.debug(`Derivation - Starting cycle (${trigger}) with ${entryCount} derivation(s)`);
|
|
7512
|
+
}
|
|
7513
|
+
iteration(iterationNumber) {
|
|
7514
|
+
if (!shouldLog(this.config, 'verbose'))
|
|
7515
|
+
return;
|
|
7516
|
+
this.logger.debug(`Derivation - Iteration ${iterationNumber}`);
|
|
7517
|
+
}
|
|
7518
|
+
evaluation(entry) {
|
|
7519
|
+
if (!shouldLog(this.config, 'verbose'))
|
|
7520
|
+
return;
|
|
7521
|
+
const name = entry.debugName ? `"${entry.debugName}"` : `${entry.sourceFieldKey} -> ${entry.targetFieldKey}`;
|
|
7522
|
+
switch (entry.result) {
|
|
7523
|
+
case 'applied':
|
|
7524
|
+
this.logger.debug(`Derivation - Applied ${name}`, {
|
|
7525
|
+
source: entry.sourceFieldKey,
|
|
7526
|
+
target: entry.targetFieldKey,
|
|
7527
|
+
previousValue: entry.previousValue,
|
|
7528
|
+
newValue: entry.newValue,
|
|
7529
|
+
...(entry.durationMs !== undefined && { durationMs: entry.durationMs }),
|
|
7530
|
+
});
|
|
7531
|
+
break;
|
|
7532
|
+
case 'skipped':
|
|
7533
|
+
this.logger.debug(`Derivation - Skipped ${name} (${this.formatSkipReason(entry.skipReason)})`, {
|
|
7534
|
+
source: entry.sourceFieldKey,
|
|
7535
|
+
target: entry.targetFieldKey,
|
|
7536
|
+
reason: entry.skipReason,
|
|
7537
|
+
});
|
|
7538
|
+
break;
|
|
7539
|
+
case 'error':
|
|
7540
|
+
this.logger.debug(`Derivation - Error ${name}`, {
|
|
7541
|
+
source: entry.sourceFieldKey,
|
|
7542
|
+
target: entry.targetFieldKey,
|
|
7543
|
+
error: entry.error,
|
|
7544
|
+
});
|
|
7545
|
+
break;
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7548
|
+
summary(result, trigger) {
|
|
7549
|
+
if (!shouldLog(this.config, 'summary'))
|
|
7550
|
+
return;
|
|
7551
|
+
// Only log if something interesting happened
|
|
7552
|
+
if (result.appliedCount === 0 && result.errorCount === 0)
|
|
7553
|
+
return;
|
|
7554
|
+
this.logger.debug(`Derivation - Cycle complete (${trigger})`, {
|
|
7555
|
+
applied: result.appliedCount,
|
|
7556
|
+
skipped: result.skippedCount,
|
|
7557
|
+
errors: result.errorCount,
|
|
7558
|
+
iterations: result.iterations,
|
|
7559
|
+
});
|
|
7560
|
+
}
|
|
7561
|
+
maxIterationsReached(result, trigger) {
|
|
7562
|
+
this.logger.warn(`Derivation - Max iterations reached (${trigger}). ` +
|
|
7563
|
+
`This may indicate a loop in derivation logic. ` +
|
|
7564
|
+
`Applied: ${result.appliedCount}, Skipped: ${result.skippedCount}, Errors: ${result.errorCount}`);
|
|
7565
|
+
}
|
|
7566
|
+
formatSkipReason(reason) {
|
|
7567
|
+
switch (reason) {
|
|
7568
|
+
case 'condition-false':
|
|
7569
|
+
return 'condition not met';
|
|
7570
|
+
case 'value-unchanged':
|
|
7571
|
+
return 'value unchanged';
|
|
7572
|
+
case 'already-applied':
|
|
7573
|
+
return 'already applied this cycle';
|
|
7574
|
+
default:
|
|
7575
|
+
return 'unknown';
|
|
7576
|
+
}
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
/**
|
|
7580
|
+
* No-op implementation of DerivationLogger.
|
|
7581
|
+
* All methods are empty - no logging overhead when disabled.
|
|
7582
|
+
*
|
|
7583
|
+
* @internal
|
|
7584
|
+
*/
|
|
7585
|
+
class NoopDerivationLogger {
|
|
7586
|
+
cycleStart() {
|
|
7587
|
+
/* no-op */
|
|
7588
|
+
}
|
|
7589
|
+
iteration() {
|
|
7590
|
+
/* no-op */
|
|
7591
|
+
}
|
|
7592
|
+
evaluation() {
|
|
7593
|
+
/* no-op */
|
|
7594
|
+
}
|
|
7595
|
+
summary() {
|
|
7596
|
+
/* no-op */
|
|
7597
|
+
}
|
|
7598
|
+
maxIterationsReached() {
|
|
7599
|
+
/* no-op */
|
|
7600
|
+
}
|
|
7601
|
+
}
|
|
7602
|
+
/** Singleton no-op instance to avoid creating new objects */
|
|
7603
|
+
const NOOP_INSTANCE = new NoopDerivationLogger();
|
|
7604
|
+
/**
|
|
7605
|
+
* Injection token for the derivation logger service.
|
|
7606
|
+
*
|
|
7607
|
+
* @public
|
|
7608
|
+
*/
|
|
7609
|
+
const DERIVATION_LOGGER = new InjectionToken('DerivationLogger');
|
|
7610
|
+
/**
|
|
7611
|
+
* Factory function to create a DerivationLogger.
|
|
7612
|
+
*
|
|
7613
|
+
* Returns a no-op implementation if config level is 'none',
|
|
7614
|
+
* otherwise returns an active logger with the given config.
|
|
7615
|
+
*
|
|
7616
|
+
* @param config - The derivation log configuration
|
|
7617
|
+
* @param logger - The underlying logger instance
|
|
7618
|
+
* @returns A DerivationLogger instance
|
|
7619
|
+
*
|
|
7620
|
+
* @public
|
|
7621
|
+
*/
|
|
7622
|
+
function createDerivationLogger(config, logger) {
|
|
7623
|
+
const effectiveConfig = config ?? createDefaultDerivationLogConfig();
|
|
7624
|
+
// Return no-op if logging is completely disabled
|
|
7625
|
+
if (effectiveConfig.level === 'none') {
|
|
7626
|
+
return NOOP_INSTANCE;
|
|
7627
|
+
}
|
|
7628
|
+
// Create active logger with config
|
|
7629
|
+
const service = new ActiveDerivationLogger();
|
|
7630
|
+
service.logger = logger;
|
|
7631
|
+
service.setConfig(effectiveConfig);
|
|
7632
|
+
return service;
|
|
7633
|
+
}
|
|
7634
|
+
|
|
7635
|
+
/**
|
|
7636
|
+
* Orchestrates derivation processing for a dynamic form.
|
|
7637
|
+
*
|
|
7638
|
+
* Uses RxJS streams for reactive derivation processing:
|
|
7639
|
+
* - `exhaustMap` prevents re-entry during onChange derivation application
|
|
7640
|
+
* - `pairwise` tracks value changes without mutable state
|
|
7641
|
+
* - `takeUntilDestroyed` handles automatic cleanup
|
|
7642
|
+
*
|
|
7643
|
+
* Injects common services directly, requiring only form-specific signals in config.
|
|
7644
|
+
*
|
|
7645
|
+
* @public
|
|
7646
|
+
*/
|
|
7647
|
+
class DerivationOrchestrator {
|
|
7648
|
+
config;
|
|
7649
|
+
injector = inject(Injector);
|
|
7650
|
+
destroyRef = inject(DestroyRef);
|
|
7651
|
+
logger = inject(DynamicFormLogger);
|
|
7652
|
+
warningTracker = inject(DERIVATION_WARNING_TRACKER);
|
|
7653
|
+
functionRegistry = inject(FunctionRegistryService);
|
|
7654
|
+
formOptions = inject(FORM_OPTIONS);
|
|
7655
|
+
/**
|
|
7656
|
+
* Computed signal containing the collected and validated derivations.
|
|
7657
|
+
* Returns null if no derivations are defined.
|
|
7658
|
+
*/
|
|
7659
|
+
derivationCollection;
|
|
7660
|
+
constructor(config) {
|
|
7661
|
+
this.config = config;
|
|
7662
|
+
this.derivationCollection = computed(() => {
|
|
7663
|
+
const fields = config.schemaFields();
|
|
7664
|
+
if (!fields || fields.length === 0) {
|
|
7665
|
+
return null;
|
|
7666
|
+
}
|
|
7667
|
+
const collection = collectDerivations(fields);
|
|
7668
|
+
if (collection.entries.length === 0) {
|
|
7669
|
+
return null;
|
|
7670
|
+
}
|
|
7671
|
+
validateNoCycles(collection, this.logger);
|
|
7672
|
+
this.warnAboutWildcardDependencies(collection.entries, fields.length);
|
|
7673
|
+
return collection;
|
|
7674
|
+
}, ...(ngDevMode ? [{ debugName: "derivationCollection" }] : []));
|
|
7675
|
+
this.setupOnChangeStream();
|
|
7676
|
+
this.setupDebouncedStream();
|
|
7677
|
+
}
|
|
7678
|
+
setupOnChangeStream() {
|
|
7679
|
+
const collection$ = toObservable(this.derivationCollection, { injector: this.injector });
|
|
7680
|
+
const formValue$ = toObservable(this.config.formValue, { injector: this.injector });
|
|
7681
|
+
const form$ = toObservable(this.config.form, { injector: this.injector });
|
|
7682
|
+
collection$
|
|
7683
|
+
.pipe(filter((collection) => collection !== null), combineLatestWith(formValue$, form$),
|
|
7684
|
+
// auditTime(0): Batch synchronous emissions from Angular's change detection.
|
|
7685
|
+
// When a single user action triggers multiple signal updates, this ensures
|
|
7686
|
+
// we only process derivations once after all updates complete (microtask timing).
|
|
7687
|
+
auditTime(0),
|
|
7688
|
+
// exhaustMap: Prevents re-entry while processing derivations.
|
|
7689
|
+
// If form value changes DURING derivation processing (from our own setValue calls),
|
|
7690
|
+
// we ignore those emissions and complete the current cycle first.
|
|
7691
|
+
// switchMap would cancel mid-processing, causing incomplete derivation chains.
|
|
7692
|
+
exhaustMap(([collection, , formAccessor]) => {
|
|
7693
|
+
this.applyOnChangeDerivations(collection, formAccessor);
|
|
7694
|
+
// scheduled with queueScheduler: Ensures the observable completes
|
|
7695
|
+
// in the next microtask, allowing exhaustMap to accept new emissions.
|
|
7696
|
+
// Without this, exhaustMap would block indefinitely.
|
|
7697
|
+
return scheduled([null], queueScheduler);
|
|
7698
|
+
}), takeUntilDestroyed(this.destroyRef))
|
|
7699
|
+
.subscribe();
|
|
7700
|
+
}
|
|
7701
|
+
setupDebouncedStream() {
|
|
7702
|
+
toObservable(this.config.formValue, { injector: this.injector })
|
|
7703
|
+
.pipe(
|
|
7704
|
+
// debounceTime: Wait for value to stabilize before detecting changes.
|
|
7705
|
+
// Uses DEFAULT_DEBOUNCE_MS as the minimum debounce period.
|
|
7706
|
+
debounceTime(DEFAULT_DEBOUNCE_MS),
|
|
7707
|
+
// startWith + pairwise: Track previous and current values to detect changes.
|
|
7708
|
+
// startWith(null) ensures pairwise has an initial value to pair with.
|
|
7709
|
+
startWith(null), pairwise(), filter((pair) => pair[1] !== null), map(([previous, current]) => ({
|
|
7710
|
+
current,
|
|
7711
|
+
changedFields: getChangedKeys(previous, current),
|
|
7712
|
+
})), filter(({ changedFields }) => changedFields.size > 0),
|
|
7713
|
+
// switchMap: For debounced derivations, it's OK to cancel pending work
|
|
7714
|
+
// if new changes come in - we want the latest debounced values.
|
|
7715
|
+
// (Unlike onChange which uses exhaustMap to prevent cancellation)
|
|
7716
|
+
switchMap(({ changedFields }) => {
|
|
7717
|
+
const collection = untracked(() => this.derivationCollection());
|
|
7718
|
+
const formAccessor = untracked(() => this.config.form());
|
|
7719
|
+
if (!collection || !formAccessor)
|
|
7720
|
+
return of(null);
|
|
7721
|
+
// Get unique debounce periods from entries with trigger 'debounced'
|
|
7722
|
+
const debouncePeriods = this.getDebouncePeriods(collection.entries);
|
|
7723
|
+
if (debouncePeriods.length === 0)
|
|
7724
|
+
return of(null);
|
|
7725
|
+
// merge: Process multiple debounce periods concurrently.
|
|
7726
|
+
// Each period stream handles its own timing independently.
|
|
7727
|
+
const periodStreams = debouncePeriods.map((debounceMs) => this.createPeriodStream(debounceMs, collection, formAccessor, changedFields));
|
|
7728
|
+
return merge(...periodStreams);
|
|
7729
|
+
}), takeUntilDestroyed(this.destroyRef))
|
|
7730
|
+
.subscribe();
|
|
7731
|
+
}
|
|
7732
|
+
createPeriodStream(debounceMs, collection, formAccessor, changedFields) {
|
|
7733
|
+
const additionalWait = Math.max(0, debounceMs - DEFAULT_DEBOUNCE_MS);
|
|
7734
|
+
if (additionalWait === 0) {
|
|
7735
|
+
return of(null).pipe(map(() => {
|
|
7736
|
+
this.applyDebouncedEntriesForPeriod(debounceMs, collection, formAccessor, changedFields);
|
|
7737
|
+
}));
|
|
7738
|
+
}
|
|
7739
|
+
return timer(additionalWait).pipe(map(() => {
|
|
7740
|
+
const currentCollection = untracked(() => this.derivationCollection());
|
|
7741
|
+
const currentFormAccessor = untracked(() => this.config.form());
|
|
7742
|
+
if (currentCollection && currentFormAccessor) {
|
|
7743
|
+
this.applyDebouncedEntriesForPeriod(debounceMs, currentCollection, currentFormAccessor, changedFields);
|
|
7744
|
+
}
|
|
7745
|
+
}));
|
|
7746
|
+
}
|
|
7747
|
+
applyOnChangeDerivations(collection, formAccessor) {
|
|
7748
|
+
const applicatorContext = {
|
|
7749
|
+
formValue: this.config.formValue,
|
|
7750
|
+
rootForm: formAccessor,
|
|
7751
|
+
derivationFunctions: this.functionRegistry.getDerivationFunctions(),
|
|
7752
|
+
customFunctions: this.functionRegistry.getCustomFunctions(),
|
|
7753
|
+
logger: this.logger,
|
|
7754
|
+
warningTracker: this.warningTracker,
|
|
7755
|
+
derivationLogger: untracked(() => this.config.derivationLogger()),
|
|
7756
|
+
maxIterations: untracked(() => this.formOptions()?.maxDerivationIterations),
|
|
7757
|
+
};
|
|
7758
|
+
const result = applyDerivationsForTrigger(collection, 'onChange', applicatorContext);
|
|
7759
|
+
if (result.maxIterationsReached) {
|
|
7760
|
+
this.logger.warn(`Derivation processing reached max iterations. ` +
|
|
7761
|
+
`This may indicate a loop in derivation logic that wasn't caught at build time. ` +
|
|
7762
|
+
`Applied: ${result.appliedCount}, Skipped: ${result.skippedCount}, Errors: ${result.errorCount}`);
|
|
7763
|
+
}
|
|
7764
|
+
}
|
|
7765
|
+
applyDebouncedEntriesForPeriod(debounceMs, collection, formAccessor, changedFields) {
|
|
7766
|
+
// Filter entries to just those with the specific debounce period
|
|
7767
|
+
const debouncedEntries = collection.entries.filter((entry) => entry.trigger === 'debounced' && (entry.debounceMs ?? DEFAULT_DEBOUNCE_MS) === debounceMs);
|
|
7768
|
+
if (debouncedEntries.length === 0) {
|
|
7769
|
+
return;
|
|
7770
|
+
}
|
|
7771
|
+
// Create a minimal collection with filtered entries
|
|
7772
|
+
const filteredCollection = {
|
|
7773
|
+
entries: debouncedEntries,
|
|
7774
|
+
};
|
|
7775
|
+
const applicatorContext = {
|
|
7776
|
+
formValue: this.config.formValue,
|
|
7777
|
+
rootForm: formAccessor,
|
|
7778
|
+
derivationFunctions: this.functionRegistry.getDerivationFunctions(),
|
|
7779
|
+
customFunctions: this.functionRegistry.getCustomFunctions(),
|
|
7780
|
+
logger: this.logger,
|
|
7781
|
+
warningTracker: this.warningTracker,
|
|
7782
|
+
derivationLogger: untracked(() => this.config.derivationLogger()),
|
|
7783
|
+
maxIterations: untracked(() => this.formOptions()?.maxDerivationIterations),
|
|
7784
|
+
};
|
|
7785
|
+
const result = applyDerivationsForTrigger(filteredCollection, 'debounced', applicatorContext, changedFields);
|
|
7786
|
+
if (result.maxIterationsReached) {
|
|
7787
|
+
this.logger.warn(`Debounced derivation processing reached max iterations (${debounceMs}ms). ` +
|
|
7788
|
+
`Applied: ${result.appliedCount}, Skipped: ${result.skippedCount}, Errors: ${result.errorCount}`);
|
|
7789
|
+
}
|
|
7790
|
+
}
|
|
7791
|
+
/**
|
|
7792
|
+
* Gets all unique debounce periods from entries with trigger 'debounced'.
|
|
7793
|
+
*/
|
|
7794
|
+
getDebouncePeriods(entries) {
|
|
7795
|
+
const periods = new Set();
|
|
7796
|
+
for (const entry of entries) {
|
|
7797
|
+
if (entry.trigger === 'debounced') {
|
|
7798
|
+
periods.add(entry.debounceMs ?? DEFAULT_DEBOUNCE_MS);
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
return Array.from(periods);
|
|
7802
|
+
}
|
|
7803
|
+
warnAboutWildcardDependencies(entries, fieldCount) {
|
|
7804
|
+
if (!isDevMode())
|
|
7805
|
+
return;
|
|
7806
|
+
// Find entries with wildcard dependency
|
|
7807
|
+
const wildcardEntries = entries.filter((entry) => entry.dependsOn.includes('*'));
|
|
7808
|
+
if (wildcardEntries.length === 0)
|
|
7809
|
+
return;
|
|
7810
|
+
// Find implicit wildcards (custom functions without explicit dependsOn)
|
|
7811
|
+
const implicitWildcards = wildcardEntries.filter((entry) => entry.functionName && (!entry.originalConfig?.dependsOn || entry.originalConfig.dependsOn.length === 0));
|
|
7812
|
+
if (implicitWildcards.length > 0) {
|
|
7813
|
+
const derivationDescs = implicitWildcards.map((e) => `${e.sourceFieldKey} -> ${e.targetFieldKey} (${e.functionName})`);
|
|
7814
|
+
this.logger.warn('[Derivation] Derivations using custom functions without explicit dependsOn detected. ' +
|
|
7815
|
+
`These run on EVERY form change, which may impact performance (form has ${fieldCount} fields). ` +
|
|
7816
|
+
'Consider specifying explicit dependsOn arrays for better performance.', derivationDescs);
|
|
7817
|
+
}
|
|
7818
|
+
}
|
|
7819
|
+
}
|
|
7820
|
+
/**
|
|
7821
|
+
* Creates a DerivationOrchestrator for a dynamic form.
|
|
7822
|
+
* Must be called within an injection context.
|
|
7823
|
+
*
|
|
7824
|
+
* @param config - Form-specific signals configuration
|
|
7825
|
+
* @returns The created DerivationOrchestrator
|
|
7826
|
+
*
|
|
7827
|
+
* @public
|
|
7828
|
+
*/
|
|
7829
|
+
function createDerivationOrchestrator(config) {
|
|
7830
|
+
return new DerivationOrchestrator(config);
|
|
7831
|
+
}
|
|
7832
|
+
/**
|
|
7833
|
+
* Injection token for the DerivationOrchestrator.
|
|
7834
|
+
*
|
|
7835
|
+
* @public
|
|
7836
|
+
*/
|
|
7837
|
+
const DERIVATION_ORCHESTRATOR = new InjectionToken('DERIVATION_ORCHESTRATOR');
|
|
7838
|
+
/**
|
|
7839
|
+
* Injects the DerivationOrchestrator from the current injection context.
|
|
7840
|
+
*
|
|
7841
|
+
* @returns The DerivationOrchestrator instance
|
|
7842
|
+
* @throws Error if called outside of an injection context or if orchestrator is not provided
|
|
7843
|
+
*
|
|
7844
|
+
* @public
|
|
7845
|
+
*/
|
|
7846
|
+
function injectDerivationOrchestrator() {
|
|
7847
|
+
return inject(DERIVATION_ORCHESTRATOR);
|
|
7848
|
+
}
|
|
7849
|
+
|
|
7850
|
+
class NextPageEvent {
|
|
7851
|
+
type = 'next-page';
|
|
7852
|
+
}
|
|
7853
|
+
|
|
7854
|
+
class PageChangeEvent {
|
|
7855
|
+
currentPageIndex;
|
|
7856
|
+
totalPages;
|
|
7857
|
+
previousPageIndex;
|
|
7858
|
+
type = 'page-change';
|
|
7859
|
+
constructor(
|
|
7860
|
+
/** The current page index (0-based) */
|
|
7861
|
+
currentPageIndex,
|
|
7862
|
+
/** Total number of pages */
|
|
7863
|
+
totalPages,
|
|
7864
|
+
/** Previous page index (0-based), undefined if first navigation */
|
|
7865
|
+
previousPageIndex) {
|
|
7866
|
+
this.currentPageIndex = currentPageIndex;
|
|
7867
|
+
this.totalPages = totalPages;
|
|
7868
|
+
this.previousPageIndex = previousPageIndex;
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7871
|
+
|
|
7872
|
+
class PreviousPageEvent {
|
|
7873
|
+
type = 'previous-page';
|
|
7874
|
+
}
|
|
7875
|
+
|
|
7876
|
+
/**
|
|
7877
|
+
* Event dispatched to add a new item to an array field.
|
|
7878
|
+
*
|
|
7879
|
+
* This event is used to dynamically add items to array fields at runtime.
|
|
7880
|
+
* The array field component listens for this event and adds a new item
|
|
7881
|
+
* based on the provided field template, or uses the array's own template if not provided.
|
|
7882
|
+
*
|
|
7883
|
+
* @example
|
|
7884
|
+
* ```typescript
|
|
7885
|
+
* // Add item using array's own template
|
|
7886
|
+
* eventBus.dispatch(AddArrayItemEvent, 'contacts');
|
|
7887
|
+
*
|
|
7888
|
+
* // Add item with explicit field template
|
|
7889
|
+
* const fieldTemplate = { key: 'email', type: 'input' };
|
|
7890
|
+
* eventBus.dispatch(AddArrayItemEvent, 'contacts', fieldTemplate);
|
|
7891
|
+
*
|
|
7892
|
+
* // Add item at specific index
|
|
7893
|
+
* eventBus.dispatch(AddArrayItemEvent, 'contacts', fieldTemplate, 2);
|
|
7894
|
+
* ```
|
|
7895
|
+
*/
|
|
7896
|
+
class AddArrayItemEvent {
|
|
7897
|
+
arrayKey;
|
|
7898
|
+
field;
|
|
7899
|
+
index;
|
|
7900
|
+
type = 'add-array-item';
|
|
7901
|
+
constructor(
|
|
7902
|
+
/** The key of the array field to add an item to */
|
|
7903
|
+
arrayKey,
|
|
7904
|
+
/**
|
|
7905
|
+
* Optional field template definition to render for the new array item.
|
|
7906
|
+
* If not provided, the array field component will use its own template.
|
|
7907
|
+
*/
|
|
7908
|
+
field,
|
|
7909
|
+
/** Optional index where to insert the item (defaults to end of array) */
|
|
7910
|
+
index) {
|
|
7911
|
+
this.arrayKey = arrayKey;
|
|
7912
|
+
this.field = field;
|
|
7913
|
+
this.index = index;
|
|
7914
|
+
}
|
|
7915
|
+
}
|
|
7916
|
+
|
|
7917
|
+
/**
|
|
7918
|
+
* Event dispatched to remove an item from an array field.
|
|
7919
|
+
*
|
|
7920
|
+
* This event is used to dynamically remove items from array fields at runtime.
|
|
7921
|
+
* The array field component listens for this event and removes the item
|
|
7922
|
+
* at the specified index.
|
|
7923
|
+
*
|
|
7924
|
+
* @example
|
|
7925
|
+
* ```typescript
|
|
7926
|
+
* // Remove item at index 1 from 'contacts' array
|
|
7927
|
+
* eventBus.dispatch(RemoveArrayItemEvent, 'contacts', 1);
|
|
7928
|
+
*
|
|
7929
|
+
* // Remove last item
|
|
7930
|
+
* eventBus.dispatch(RemoveArrayItemEvent, 'contacts');
|
|
7931
|
+
* ```
|
|
7932
|
+
*/
|
|
7933
|
+
class RemoveArrayItemEvent {
|
|
7934
|
+
arrayKey;
|
|
7935
|
+
index;
|
|
7936
|
+
type = 'remove-array-item';
|
|
7937
|
+
constructor(
|
|
7938
|
+
/** The key of the array field to remove an item from */
|
|
7939
|
+
arrayKey,
|
|
7940
|
+
/** Index of the item to remove (defaults to last item) */
|
|
7941
|
+
index) {
|
|
7942
|
+
this.arrayKey = arrayKey;
|
|
7943
|
+
this.index = index;
|
|
7944
|
+
}
|
|
7945
|
+
}
|
|
7946
|
+
|
|
7947
|
+
/**
|
|
7948
|
+
* Event dispatched when the form should be reset to its default values.
|
|
7949
|
+
*
|
|
7950
|
+
* This event instructs the dynamic form component to restore all field values
|
|
7951
|
+
* to their initial default values as defined in the form configuration.
|
|
7952
|
+
*
|
|
7953
|
+
* @example
|
|
7954
|
+
* ```typescript
|
|
7955
|
+
* // Dispatch from a button component
|
|
7956
|
+
* eventBus.dispatch(FormResetEvent);
|
|
7957
|
+
* ```
|
|
7958
|
+
*
|
|
7959
|
+
* @example
|
|
7960
|
+
* ```typescript
|
|
7961
|
+
* // Listen for reset events
|
|
7962
|
+
* eventBus.on<FormResetEvent>('form-reset').subscribe(() => {
|
|
7963
|
+
* console.log('Form was reset to defaults');
|
|
7964
|
+
* });
|
|
7965
|
+
* ```
|
|
7966
|
+
*/
|
|
7967
|
+
class FormResetEvent {
|
|
7968
|
+
type = 'form-reset';
|
|
7969
|
+
}
|
|
7970
|
+
|
|
7971
|
+
/**
|
|
7972
|
+
* Event dispatched when the form should be cleared.
|
|
7973
|
+
*
|
|
7974
|
+
* This event instructs the dynamic form component to clear all field values,
|
|
7975
|
+
* resetting them to empty/undefined state regardless of their default values.
|
|
7976
|
+
*
|
|
7977
|
+
* @example
|
|
7978
|
+
* ```typescript
|
|
7979
|
+
* // Dispatch from a button component
|
|
7980
|
+
* eventBus.dispatch(FormClearEvent);
|
|
7981
|
+
* ```
|
|
7982
|
+
*
|
|
7983
|
+
* @example
|
|
7984
|
+
* ```typescript
|
|
7985
|
+
* // Listen for clear events
|
|
7986
|
+
* eventBus.on<FormClearEvent>('form-clear').subscribe(() => {
|
|
7987
|
+
* console.log('Form was cleared');
|
|
7988
|
+
* });
|
|
7989
|
+
* ```
|
|
7990
|
+
*/
|
|
7991
|
+
class FormClearEvent {
|
|
7992
|
+
type = 'form-clear';
|
|
7993
|
+
}
|
|
7994
|
+
|
|
7995
|
+
class PageNavigationStateChangeEvent {
|
|
7996
|
+
state;
|
|
7997
|
+
type = 'page-navigation-state-change';
|
|
7998
|
+
constructor(state) {
|
|
7999
|
+
this.state = state;
|
|
8000
|
+
}
|
|
8001
|
+
}
|
|
8002
|
+
|
|
4966
8003
|
/**
|
|
4967
8004
|
* PageOrchestrator manages page navigation and visibility for paged forms.
|
|
4968
8005
|
* It acts as an intermediary between the DynamicForm component and PageField components,
|
|
@@ -4994,7 +8031,6 @@ class PageOrchestratorComponent {
|
|
|
4994
8031
|
eventBus = inject(EventBus);
|
|
4995
8032
|
fieldContextRegistry = inject(FieldContextRegistryService);
|
|
4996
8033
|
functionRegistry = inject(FunctionRegistryService);
|
|
4997
|
-
logger = inject(DynamicFormLogger);
|
|
4998
8034
|
/**
|
|
4999
8035
|
* Array of page field definitions to render
|
|
5000
8036
|
*/
|
|
@@ -5269,8 +8305,8 @@ class PageOrchestratorComponent {
|
|
|
5269
8305
|
}
|
|
5270
8306
|
return false;
|
|
5271
8307
|
}
|
|
5272
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0
|
|
5273
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0
|
|
8308
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8309
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: PageOrchestratorComponent, isStandalone: true, selector: "div[page-orchestrator]", inputs: { pageFields: { classPropertyName: "pageFields", publicName: "pageFields", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, fieldSignalContext: { classPropertyName: "fieldSignalContext", publicName: "fieldSignalContext", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.data-current-page": "state().currentPageIndex", "attr.data-total-pages": "state().totalPages" }, classAttribute: "df-page-orchestrator" }, providers: [
|
|
5274
8310
|
{
|
|
5275
8311
|
provide: FIELD_SIGNAL_CONTEXT,
|
|
5276
8312
|
useFactory: (orchestrator) => orchestrator.extendedFieldSignalContext(),
|
|
@@ -5300,9 +8336,9 @@ class PageOrchestratorComponent {
|
|
|
5300
8336
|
}
|
|
5301
8337
|
}
|
|
5302
8338
|
}
|
|
5303
|
-
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-
|
|
8339
|
+
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-CrO9WTDw.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-CrO9WTDw.mjs').then(m => m.default)]] });
|
|
5304
8340
|
}
|
|
5305
|
-
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.0
|
|
8341
|
+
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.1.0", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-CrO9WTDw.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
|
|
5306
8342
|
type: Component,
|
|
5307
8343
|
args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
|
|
5308
8344
|
@for (pageField of pageFields(); track pageField.key; let i = $index) {
|
|
@@ -5341,6 +8377,136 @@ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.0.8", ng
|
|
|
5341
8377
|
], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative;width:100%}\n"] }]
|
|
5342
8378
|
}], 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 }] }] } }) });
|
|
5343
8379
|
|
|
8380
|
+
/**
|
|
8381
|
+
* Type guard to check if a value is a DynamicFormFeature
|
|
8382
|
+
*/
|
|
8383
|
+
function isDynamicFormFeature(value) {
|
|
8384
|
+
return (typeof value === 'object' &&
|
|
8385
|
+
value !== null &&
|
|
8386
|
+
'ɵkind' in value &&
|
|
8387
|
+
typeof value.ɵkind === 'string' &&
|
|
8388
|
+
'ɵproviders' in value &&
|
|
8389
|
+
Array.isArray(value.ɵproviders));
|
|
8390
|
+
}
|
|
8391
|
+
/**
|
|
8392
|
+
* Helper to create a feature with proper typing
|
|
8393
|
+
*/
|
|
8394
|
+
function createFeature(kind, providers) {
|
|
8395
|
+
return {
|
|
8396
|
+
ɵkind: kind,
|
|
8397
|
+
ɵproviders: providers,
|
|
8398
|
+
};
|
|
8399
|
+
}
|
|
8400
|
+
|
|
8401
|
+
/**
|
|
8402
|
+
* Console-based logger implementation.
|
|
8403
|
+
*/
|
|
8404
|
+
class ConsoleLogger {
|
|
8405
|
+
prefix = '[Dynamic Forms]';
|
|
8406
|
+
debug(message, ...args) {
|
|
8407
|
+
console.debug(this.prefix, message, ...args);
|
|
8408
|
+
}
|
|
8409
|
+
info(message, ...args) {
|
|
8410
|
+
console.info(this.prefix, message, ...args);
|
|
8411
|
+
}
|
|
8412
|
+
warn(message, ...args) {
|
|
8413
|
+
console.warn(this.prefix, message, ...args);
|
|
8414
|
+
}
|
|
8415
|
+
error(message, ...args) {
|
|
8416
|
+
console.error(this.prefix, message, ...args);
|
|
8417
|
+
}
|
|
8418
|
+
}
|
|
8419
|
+
|
|
8420
|
+
/**
|
|
8421
|
+
* No-operation logger implementation.
|
|
8422
|
+
* All methods are no-ops - used in production by default.
|
|
8423
|
+
*/
|
|
8424
|
+
class NoopLogger {
|
|
8425
|
+
debug() {
|
|
8426
|
+
// Intentionally empty
|
|
8427
|
+
}
|
|
8428
|
+
info() {
|
|
8429
|
+
// Intentionally empty
|
|
8430
|
+
}
|
|
8431
|
+
warn() {
|
|
8432
|
+
// Intentionally empty
|
|
8433
|
+
}
|
|
8434
|
+
error() {
|
|
8435
|
+
// Intentionally empty
|
|
8436
|
+
}
|
|
8437
|
+
}
|
|
8438
|
+
|
|
8439
|
+
/**
|
|
8440
|
+
* Injection token for derivation logging configuration.
|
|
8441
|
+
*
|
|
8442
|
+
* @internal
|
|
8443
|
+
*/
|
|
8444
|
+
const DERIVATION_LOG_CONFIG = new InjectionToken('DERIVATION_LOG_CONFIG', {
|
|
8445
|
+
providedIn: 'root',
|
|
8446
|
+
factory: createDefaultDerivationLogConfig,
|
|
8447
|
+
});
|
|
8448
|
+
/**
|
|
8449
|
+
* Configure logging for dynamic forms.
|
|
8450
|
+
*
|
|
8451
|
+
* By default, general logging is enabled (ConsoleLogger) and derivation
|
|
8452
|
+
* logging is disabled ('none'). Use this feature to enable derivation debugging.
|
|
8453
|
+
*
|
|
8454
|
+
* @example
|
|
8455
|
+
* ```typescript
|
|
8456
|
+
* // Disable all logging
|
|
8457
|
+
* provideDynamicForm(
|
|
8458
|
+
* ...withMaterialFields(),
|
|
8459
|
+
* withLoggerConfig(false)
|
|
8460
|
+
* )
|
|
8461
|
+
*
|
|
8462
|
+
* // Enable verbose derivation logging
|
|
8463
|
+
* provideDynamicForm(
|
|
8464
|
+
* ...withMaterialFields(),
|
|
8465
|
+
* withLoggerConfig({ derivations: 'verbose' })
|
|
8466
|
+
* )
|
|
8467
|
+
*
|
|
8468
|
+
* // Disable derivation logging but keep general logging
|
|
8469
|
+
* provideDynamicForm(
|
|
8470
|
+
* ...withMaterialFields(),
|
|
8471
|
+
* withLoggerConfig({ derivations: 'none' })
|
|
8472
|
+
* )
|
|
8473
|
+
*
|
|
8474
|
+
* // Conditional logging (e.g., only in dev mode)
|
|
8475
|
+
* provideDynamicForm(
|
|
8476
|
+
* ...withMaterialFields(),
|
|
8477
|
+
* withLoggerConfig(() => isDevMode())
|
|
8478
|
+
* )
|
|
8479
|
+
* ```
|
|
8480
|
+
*
|
|
8481
|
+
* @param config - Boolean to enable/disable logging, a function returning boolean, or a config object
|
|
8482
|
+
* @returns A DynamicFormFeature that configures logging
|
|
8483
|
+
*
|
|
8484
|
+
* @public
|
|
8485
|
+
*/
|
|
8486
|
+
function withLoggerConfig(config = true) {
|
|
8487
|
+
// Handle boolean or function
|
|
8488
|
+
if (typeof config === 'boolean' || typeof config === 'function') {
|
|
8489
|
+
const isEnabled = typeof config === 'function' ? config() : config;
|
|
8490
|
+
const logger = isEnabled ? new ConsoleLogger() : new NoopLogger();
|
|
8491
|
+
return createFeature('logger', [{ provide: DynamicFormLogger, useValue: logger }]);
|
|
8492
|
+
}
|
|
8493
|
+
// Handle config object
|
|
8494
|
+
const isEnabled = config.enabled !== false;
|
|
8495
|
+
const logger = isEnabled ? new ConsoleLogger() : new NoopLogger();
|
|
8496
|
+
const providers = [{ provide: DynamicFormLogger, useValue: logger }];
|
|
8497
|
+
// Add derivation log config if specified
|
|
8498
|
+
if (config.derivations !== undefined) {
|
|
8499
|
+
providers.push({
|
|
8500
|
+
provide: DERIVATION_LOG_CONFIG,
|
|
8501
|
+
useValue: { level: config.derivations },
|
|
8502
|
+
});
|
|
8503
|
+
}
|
|
8504
|
+
return createFeature('logger', providers);
|
|
8505
|
+
}
|
|
8506
|
+
|
|
8507
|
+
function derivationOrchestratorFactoryWrapper(dynamicForm) {
|
|
8508
|
+
return createDerivationOrchestrator(dynamicForm.derivationOrchestratorConfig);
|
|
8509
|
+
}
|
|
5344
8510
|
/**
|
|
5345
8511
|
* Dynamic form component that renders a complete form based on configuration.
|
|
5346
8512
|
*
|
|
@@ -5372,6 +8538,7 @@ class DynamicForm {
|
|
|
5372
8538
|
functionRegistry = inject(FunctionRegistryService);
|
|
5373
8539
|
schemaRegistry = inject(SchemaRegistryService);
|
|
5374
8540
|
logger = inject(DynamicFormLogger);
|
|
8541
|
+
derivationLogConfig = inject(DERIVATION_LOG_CONFIG);
|
|
5375
8542
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5376
8543
|
// Inputs
|
|
5377
8544
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -5462,22 +8629,28 @@ class DynamicForm {
|
|
|
5462
8629
|
const inputOptions = this.formOptions();
|
|
5463
8630
|
return { ...configOptions, ...inputOptions };
|
|
5464
8631
|
}, ...(ngDevMode ? [{ debugName: "effectiveFormOptions" }] : []));
|
|
8632
|
+
/**
|
|
8633
|
+
* Computed derivation logger based on injected config.
|
|
8634
|
+
*
|
|
8635
|
+
* Uses factory pattern to return no-op logger when logging is disabled,
|
|
8636
|
+
* avoiding any logging overhead in production.
|
|
8637
|
+
*/
|
|
8638
|
+
derivationLogger = computed(() => {
|
|
8639
|
+
return createDerivationLogger(this.derivationLogConfig, this.logger);
|
|
8640
|
+
}, ...(ngDevMode ? [{ debugName: "derivationLogger" }] : []));
|
|
5465
8641
|
fieldSignalContext = computed(() => ({
|
|
5466
8642
|
injector: this.injector,
|
|
5467
8643
|
value: this.value,
|
|
5468
8644
|
defaultValues: this.defaultValues,
|
|
5469
8645
|
form: this.form(),
|
|
5470
|
-
defaultValidationMessages: this.config().defaultValidationMessages,
|
|
5471
|
-
formOptions: this.effectiveFormOptions(),
|
|
5472
8646
|
}), ...(ngDevMode ? [{ debugName: "fieldSignalContext" }] : []));
|
|
8647
|
+
/**
|
|
8648
|
+
* Injector for field components with FIELD_SIGNAL_CONTEXT.
|
|
8649
|
+
* Other form-level tokens (DEFAULT_PROPS, etc.) are provided at component level.
|
|
8650
|
+
*/
|
|
5473
8651
|
fieldInjector = computed(() => Injector.create({
|
|
5474
8652
|
parent: this.injector,
|
|
5475
|
-
providers: [
|
|
5476
|
-
{
|
|
5477
|
-
provide: FIELD_SIGNAL_CONTEXT,
|
|
5478
|
-
useValue: this.fieldSignalContext(),
|
|
5479
|
-
},
|
|
5480
|
-
],
|
|
8653
|
+
providers: [{ provide: FIELD_SIGNAL_CONTEXT, useValue: this.fieldSignalContext() }],
|
|
5481
8654
|
}), ...(ngDevMode ? [{ debugName: "fieldInjector" }] : []));
|
|
5482
8655
|
defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...(ngDevMode ? [{ debugName: "defaultValues" }] : []));
|
|
5483
8656
|
entity = linkedSignal(() => {
|
|
@@ -5500,12 +8673,27 @@ class DynamicForm {
|
|
|
5500
8673
|
form = computed(() => {
|
|
5501
8674
|
return runInInjectionContext(this.injector, () => {
|
|
5502
8675
|
const setup = this.formSetup();
|
|
8676
|
+
const config = this.config();
|
|
5503
8677
|
let formInstance;
|
|
5504
8678
|
untracked(() => this.rootFormRegistry.registerFormValueSignal(this.entity));
|
|
5505
|
-
|
|
8679
|
+
const hasFields = setup.schemaFields && setup.schemaFields.length > 0;
|
|
8680
|
+
const hasFormSchema = config.schema !== undefined;
|
|
8681
|
+
if (hasFields) {
|
|
5506
8682
|
const crossFieldCollection = collectCrossFieldEntries(setup.schemaFields);
|
|
5507
|
-
|
|
5508
|
-
|
|
8683
|
+
// Create schema with both field-level and form-level validation
|
|
8684
|
+
// Type assertion needed due to complex generic type inference between TModel and InferFormValue
|
|
8685
|
+
const combinedSchema = createSchemaFromFields(setup.schemaFields, setup.registry, {
|
|
8686
|
+
crossFieldValidators: crossFieldCollection.validators,
|
|
8687
|
+
formLevelSchema: config.schema,
|
|
8688
|
+
});
|
|
8689
|
+
formInstance = untracked(() => form(this.entity, combinedSchema));
|
|
8690
|
+
}
|
|
8691
|
+
else if (hasFormSchema) {
|
|
8692
|
+
// Only form-level schema (no fields with validation)
|
|
8693
|
+
// Non-null assertion safe: hasFormSchema guarantees config.schema is defined
|
|
8694
|
+
// Type assertion needed due to generic type inference
|
|
8695
|
+
const formSchema = createFormLevelSchema(config.schema);
|
|
8696
|
+
formInstance = untracked(() => form(this.entity, formSchema));
|
|
5509
8697
|
}
|
|
5510
8698
|
else {
|
|
5511
8699
|
formInstance = untracked(() => form(this.entity));
|
|
@@ -5551,6 +8739,18 @@ class DynamicForm {
|
|
|
5551
8739
|
}, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
5552
8740
|
/** Whether the form is currently submitting. */
|
|
5553
8741
|
submitting = computed(() => this.form()().submitting(), ...(ngDevMode ? [{ debugName: "submitting" }] : []));
|
|
8742
|
+
/**
|
|
8743
|
+
* Configuration for the derivation orchestrator.
|
|
8744
|
+
* Bundles the form-specific signals needed by the orchestrator.
|
|
8745
|
+
*
|
|
8746
|
+
* @internal Used by DERIVATION_ORCHESTRATOR provider.
|
|
8747
|
+
*/
|
|
8748
|
+
derivationOrchestratorConfig = {
|
|
8749
|
+
schemaFields: computed(() => this.formSetup().schemaFields),
|
|
8750
|
+
formValue: this.formValue,
|
|
8751
|
+
form: computed(() => this.form()),
|
|
8752
|
+
derivationLogger: this.derivationLogger,
|
|
8753
|
+
};
|
|
5554
8754
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5555
8755
|
// Initialization
|
|
5556
8756
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -5630,6 +8830,11 @@ class DynamicForm {
|
|
|
5630
8830
|
constructor() {
|
|
5631
8831
|
this.setupEffects();
|
|
5632
8832
|
this.setupEventHandlers();
|
|
8833
|
+
// Lazily inject the derivation orchestrator to avoid circular dependency.
|
|
8834
|
+
// The orchestrator's constructor sets up RxJS subscriptions that process derivations.
|
|
8835
|
+
afterNextRender(() => {
|
|
8836
|
+
this.injector.get(DERIVATION_ORCHESTRATOR);
|
|
8837
|
+
});
|
|
5633
8838
|
}
|
|
5634
8839
|
/**
|
|
5635
8840
|
* Handles native form submission triggered by:
|
|
@@ -5707,6 +8912,10 @@ class DynamicForm {
|
|
|
5707
8912
|
this.functionRegistry.registerCustomFunction(name, fn);
|
|
5708
8913
|
});
|
|
5709
8914
|
}
|
|
8915
|
+
// Register derivation functions (stored separately for use by derivation applicator)
|
|
8916
|
+
if (customFnConfig.derivations) {
|
|
8917
|
+
this.functionRegistry.setDerivationFunctions(customFnConfig.derivations);
|
|
8918
|
+
}
|
|
5710
8919
|
this.functionRegistry.setValidators(customFnConfig.validators);
|
|
5711
8920
|
this.functionRegistry.setAsyncValidators(customFnConfig.asyncValidators);
|
|
5712
8921
|
this.functionRegistry.setHttpValidators(customFnConfig.httpValidators);
|
|
@@ -5755,8 +8964,36 @@ class DynamicForm {
|
|
|
5755
8964
|
ngOnDestroy() {
|
|
5756
8965
|
this.rootFormRegistry.unregisterForm();
|
|
5757
8966
|
}
|
|
5758
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0
|
|
5759
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0
|
|
8967
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8968
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: DynamicForm, isStandalone: true, selector: "form[dynamic-form]", inputs: { config: { classPropertyName: "config", publicName: "dynamic-form", isSignal: true, isRequired: true, transformFunction: null }, formOptions: { classPropertyName: "formOptions", publicName: "formOptions", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted", reset: "reset", cleared: "cleared", events: "events", initialized: "initialized", onPageChange: "onPageChange", onPageNavigationStateChange: "onPageNavigationStateChange" }, host: { attributes: { "novalidate": "" }, listeners: { "submit": "onNativeSubmit($event)" }, properties: { "class.disabled": "disabled()", "class.df-form-paged": "formModeDetection().mode === \"paged\"", "class.df-form-non-paged": "formModeDetection().mode === \"non-paged\"", "attr.data-form-mode": "formModeDetection().mode" }, classAttribute: "df-dynamic-form df-form" }, providers: [
|
|
8969
|
+
EventBus,
|
|
8970
|
+
SchemaRegistryService,
|
|
8971
|
+
FunctionRegistryService,
|
|
8972
|
+
RootFormRegistryService,
|
|
8973
|
+
FieldContextRegistryService,
|
|
8974
|
+
// Form-level config tokens provided via useFactory to enable reactive updates
|
|
8975
|
+
{
|
|
8976
|
+
provide: DEFAULT_PROPS,
|
|
8977
|
+
useFactory: (form) => computed(() => form.config().defaultProps),
|
|
8978
|
+
deps: [DynamicForm],
|
|
8979
|
+
},
|
|
8980
|
+
{
|
|
8981
|
+
provide: DEFAULT_VALIDATION_MESSAGES,
|
|
8982
|
+
useFactory: (form) => computed(() => form.config().defaultValidationMessages),
|
|
8983
|
+
deps: [DynamicForm],
|
|
8984
|
+
},
|
|
8985
|
+
{
|
|
8986
|
+
provide: FORM_OPTIONS,
|
|
8987
|
+
useFactory: (form) => form.effectiveFormOptions,
|
|
8988
|
+
deps: [DynamicForm],
|
|
8989
|
+
},
|
|
8990
|
+
{ provide: DERIVATION_WARNING_TRACKER, useFactory: createDerivationWarningTracker },
|
|
8991
|
+
{
|
|
8992
|
+
provide: DERIVATION_ORCHESTRATOR,
|
|
8993
|
+
useFactory: derivationOrchestratorFactoryWrapper,
|
|
8994
|
+
deps: [DynamicForm],
|
|
8995
|
+
},
|
|
8996
|
+
], ngImport: i0, template: `
|
|
5760
8997
|
@if (formModeDetection().mode === 'paged') {
|
|
5761
8998
|
<div page-orchestrator [pageFields]="pageFieldDefinitions()" [form]="form()" [fieldSignalContext]="fieldSignalContext()"></div>
|
|
5762
8999
|
} @else {
|
|
@@ -5766,7 +9003,7 @@ class DynamicForm {
|
|
|
5766
9003
|
}
|
|
5767
9004
|
`, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}:host>*{grid-column:1/-1}:host.df-form-spacing-custom{--df-grid-row-gap: var(--form-custom-spacing, 1.5rem)}:host.df-form-gap-none{--df-grid-row-gap: 0}:host.df-form-gap-xs{--df-grid-row-gap: .25rem}:host.df-form-gap-sm{--df-grid-row-gap: .5rem}:host.df-form-gap-md{--df-grid-row-gap: 1rem}:host.df-form-gap-lg{--df-grid-row-gap: 1.5rem}:host.df-form-gap-xl{--df-grid-row-gap: 2rem}:host.df-form-paged{--df-grid-row-gap: 0}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: PageOrchestratorComponent, selector: "div[page-orchestrator]", inputs: ["pageFields", "form", "fieldSignalContext"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5768
9005
|
}
|
|
5769
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0
|
|
9006
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DynamicForm, decorators: [{
|
|
5770
9007
|
type: Component,
|
|
5771
9008
|
args: [{ selector: 'form[dynamic-form]', imports: [NgComponentOutlet, PageOrchestratorComponent], template: `
|
|
5772
9009
|
@if (formModeDetection().mode === 'paged') {
|
|
@@ -5776,7 +9013,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
|
|
|
5776
9013
|
<ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
|
|
5777
9014
|
}
|
|
5778
9015
|
}
|
|
5779
|
-
`, providers: [
|
|
9016
|
+
`, providers: [
|
|
9017
|
+
EventBus,
|
|
9018
|
+
SchemaRegistryService,
|
|
9019
|
+
FunctionRegistryService,
|
|
9020
|
+
RootFormRegistryService,
|
|
9021
|
+
FieldContextRegistryService,
|
|
9022
|
+
// Form-level config tokens provided via useFactory to enable reactive updates
|
|
9023
|
+
{
|
|
9024
|
+
provide: DEFAULT_PROPS,
|
|
9025
|
+
useFactory: (form) => computed(() => form.config().defaultProps),
|
|
9026
|
+
deps: [DynamicForm],
|
|
9027
|
+
},
|
|
9028
|
+
{
|
|
9029
|
+
provide: DEFAULT_VALIDATION_MESSAGES,
|
|
9030
|
+
useFactory: (form) => computed(() => form.config().defaultValidationMessages),
|
|
9031
|
+
deps: [DynamicForm],
|
|
9032
|
+
},
|
|
9033
|
+
{
|
|
9034
|
+
provide: FORM_OPTIONS,
|
|
9035
|
+
useFactory: (form) => form.effectiveFormOptions,
|
|
9036
|
+
deps: [DynamicForm],
|
|
9037
|
+
},
|
|
9038
|
+
{ provide: DERIVATION_WARNING_TRACKER, useFactory: createDerivationWarningTracker },
|
|
9039
|
+
{
|
|
9040
|
+
provide: DERIVATION_ORCHESTRATOR,
|
|
9041
|
+
useFactory: derivationOrchestratorFactoryWrapper,
|
|
9042
|
+
deps: [DynamicForm],
|
|
9043
|
+
},
|
|
9044
|
+
], host: {
|
|
5780
9045
|
class: 'df-dynamic-form df-form',
|
|
5781
9046
|
novalidate: '', // Disable browser validation - Angular Signal Forms handles validation
|
|
5782
9047
|
'[class.disabled]': 'disabled()',
|
|
@@ -5878,12 +9143,12 @@ function pageFieldMapper(fieldDef) {
|
|
|
5878
9143
|
function textFieldMapper(fieldDef) {
|
|
5879
9144
|
const fieldContextRegistry = inject(FieldContextRegistryService);
|
|
5880
9145
|
const functionRegistry = inject(FunctionRegistryService);
|
|
5881
|
-
|
|
5882
|
-
const baseInputs = buildBaseInputs(fieldDef);
|
|
9146
|
+
const defaultProps = inject(DEFAULT_PROPS);
|
|
5883
9147
|
// Create computed signal for hidden state based on logic configuration
|
|
5884
9148
|
const hiddenLogic = fieldDef.logic?.filter((l) => l.type === 'hidden') ?? [];
|
|
5885
9149
|
// Return computed signal for reactive updates
|
|
5886
9150
|
return computed(() => {
|
|
9151
|
+
const baseInputs = buildBaseInputs(fieldDef, defaultProps());
|
|
5887
9152
|
const inputs = { ...baseInputs };
|
|
5888
9153
|
// Evaluate hidden logic if present
|
|
5889
9154
|
if (hiddenLogic.length > 0) {
|
|
@@ -5960,13 +9225,13 @@ const BUILT_IN_FIELDS = [
|
|
|
5960
9225
|
},
|
|
5961
9226
|
{
|
|
5962
9227
|
name: 'page',
|
|
5963
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-
|
|
9228
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-CrO9WTDw.mjs'),
|
|
5964
9229
|
mapper: pageFieldMapper,
|
|
5965
9230
|
valueHandling: 'flatten',
|
|
5966
9231
|
},
|
|
5967
9232
|
{
|
|
5968
9233
|
name: 'text',
|
|
5969
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-
|
|
9234
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-BcFx_kOz.mjs'),
|
|
5970
9235
|
mapper: textFieldMapper,
|
|
5971
9236
|
valueHandling: 'exclude',
|
|
5972
9237
|
},
|
|
@@ -5977,46 +9242,6 @@ const BUILT_IN_FIELDS = [
|
|
|
5977
9242
|
},
|
|
5978
9243
|
];
|
|
5979
9244
|
|
|
5980
|
-
/**
|
|
5981
|
-
* Type guard to check if a value is a DynamicFormFeature
|
|
5982
|
-
*/
|
|
5983
|
-
function isDynamicFormFeature(value) {
|
|
5984
|
-
return (typeof value === 'object' &&
|
|
5985
|
-
value !== null &&
|
|
5986
|
-
'ɵkind' in value &&
|
|
5987
|
-
typeof value.ɵkind === 'string' &&
|
|
5988
|
-
'ɵproviders' in value &&
|
|
5989
|
-
Array.isArray(value.ɵproviders));
|
|
5990
|
-
}
|
|
5991
|
-
/**
|
|
5992
|
-
* Helper to create a feature with proper typing
|
|
5993
|
-
*/
|
|
5994
|
-
function createFeature(kind, providers) {
|
|
5995
|
-
return {
|
|
5996
|
-
ɵkind: kind,
|
|
5997
|
-
ɵproviders: providers,
|
|
5998
|
-
};
|
|
5999
|
-
}
|
|
6000
|
-
|
|
6001
|
-
/**
|
|
6002
|
-
* Console-based logger implementation.
|
|
6003
|
-
*/
|
|
6004
|
-
class ConsoleLogger {
|
|
6005
|
-
prefix = '[Dynamic Forms]';
|
|
6006
|
-
debug(message, ...args) {
|
|
6007
|
-
console.debug(this.prefix, message, ...args);
|
|
6008
|
-
}
|
|
6009
|
-
info(message, ...args) {
|
|
6010
|
-
console.info(this.prefix, message, ...args);
|
|
6011
|
-
}
|
|
6012
|
-
warn(message, ...args) {
|
|
6013
|
-
console.warn(this.prefix, message, ...args);
|
|
6014
|
-
}
|
|
6015
|
-
error(message, ...args) {
|
|
6016
|
-
console.error(this.prefix, message, ...args);
|
|
6017
|
-
}
|
|
6018
|
-
}
|
|
6019
|
-
|
|
6020
9245
|
/**
|
|
6021
9246
|
* Provider function to configure the dynamic form system with field types and options.
|
|
6022
9247
|
*
|
|
@@ -6110,55 +9335,6 @@ function provideDynamicForm(...items) {
|
|
|
6110
9335
|
]);
|
|
6111
9336
|
}
|
|
6112
9337
|
|
|
6113
|
-
/**
|
|
6114
|
-
* No-operation logger implementation.
|
|
6115
|
-
* All methods are no-ops - used in production by default.
|
|
6116
|
-
*/
|
|
6117
|
-
class NoopLogger {
|
|
6118
|
-
debug() {
|
|
6119
|
-
// Intentionally empty
|
|
6120
|
-
}
|
|
6121
|
-
info() {
|
|
6122
|
-
// Intentionally empty
|
|
6123
|
-
}
|
|
6124
|
-
warn() {
|
|
6125
|
-
// Intentionally empty
|
|
6126
|
-
}
|
|
6127
|
-
error() {
|
|
6128
|
-
// Intentionally empty
|
|
6129
|
-
}
|
|
6130
|
-
}
|
|
6131
|
-
|
|
6132
|
-
/**
|
|
6133
|
-
* Configure logging for dynamic forms.
|
|
6134
|
-
*
|
|
6135
|
-
* By default, logging is enabled (ConsoleLogger). Use this feature to disable logging
|
|
6136
|
-
* or configure it conditionally.
|
|
6137
|
-
*
|
|
6138
|
-
* @example
|
|
6139
|
-
* ```typescript
|
|
6140
|
-
* // Disable logging
|
|
6141
|
-
* provideDynamicForm(
|
|
6142
|
-
* ...withMaterialFields(),
|
|
6143
|
-
* withLoggerConfig(false)
|
|
6144
|
-
* )
|
|
6145
|
-
*
|
|
6146
|
-
* // Conditional logging (e.g., only in dev mode)
|
|
6147
|
-
* provideDynamicForm(
|
|
6148
|
-
* ...withMaterialFields(),
|
|
6149
|
-
* withLoggerConfig(() => isDevMode())
|
|
6150
|
-
* )
|
|
6151
|
-
* ```
|
|
6152
|
-
*
|
|
6153
|
-
* @param enabled - Whether logging is enabled. Can be a boolean or a function that returns a boolean. Defaults to true.
|
|
6154
|
-
* @returns A DynamicFormFeature that configures logging
|
|
6155
|
-
*/
|
|
6156
|
-
function withLoggerConfig(enabled = true) {
|
|
6157
|
-
const isEnabled = typeof enabled === 'function' ? enabled() : enabled;
|
|
6158
|
-
const logger = isEnabled ? new ConsoleLogger() : new NoopLogger();
|
|
6159
|
-
return createFeature('logger', [{ provide: DynamicFormLogger, useValue: logger }]);
|
|
6160
|
-
}
|
|
6161
|
-
|
|
6162
9338
|
/**
|
|
6163
9339
|
* No-op logger for when no logger is provided.
|
|
6164
9340
|
* Used as fallback in button logic evaluation to avoid breaking downstream packages.
|
|
@@ -6414,6 +9590,62 @@ function resolveTokens(args, context) {
|
|
|
6414
9590
|
});
|
|
6415
9591
|
}
|
|
6416
9592
|
|
|
9593
|
+
/**
|
|
9594
|
+
* Type-safe form config builder that ensures schema matches field structure.
|
|
9595
|
+
*
|
|
9596
|
+
* This helper function provides type safety when using form-level schemas by
|
|
9597
|
+
* automatically inferring the form value type from fields and constraining
|
|
9598
|
+
* the schema type parameter accordingly.
|
|
9599
|
+
*
|
|
9600
|
+
* **When to use:**
|
|
9601
|
+
* - When you have a form-level `schema` and want type safety between fields and schema
|
|
9602
|
+
* - When you prefer function syntax over `as const satisfies FormConfig`
|
|
9603
|
+
*
|
|
9604
|
+
* **Note:** This is optional. Users who don't need schema type safety can
|
|
9605
|
+
* continue using `as const satisfies FormConfig` directly.
|
|
9606
|
+
*
|
|
9607
|
+
* @typeParam TFields - The field definitions array (narrowed via `as const`)
|
|
9608
|
+
* @typeParam TProps - Optional form-level default props type
|
|
9609
|
+
*
|
|
9610
|
+
* @param config - The form configuration object
|
|
9611
|
+
* @returns The same configuration with proper type inference
|
|
9612
|
+
*
|
|
9613
|
+
* @example Basic usage with Zod schema
|
|
9614
|
+
* ```typescript
|
|
9615
|
+
* import { z } from 'zod';
|
|
9616
|
+
* import { formConfig } from '@ng-forge/dynamic-forms';
|
|
9617
|
+
* import { standardSchema } from '@ng-forge/dynamic-forms/schema';
|
|
9618
|
+
*
|
|
9619
|
+
* const passwordSchema = z.object({
|
|
9620
|
+
* password: z.string().min(8),
|
|
9621
|
+
* confirmPassword: z.string(),
|
|
9622
|
+
* }).refine(
|
|
9623
|
+
* (data) => data.password === data.confirmPassword,
|
|
9624
|
+
* { message: 'Passwords must match', path: ['confirmPassword'] }
|
|
9625
|
+
* );
|
|
9626
|
+
*
|
|
9627
|
+
* const config = formConfig({
|
|
9628
|
+
* schema: standardSchema(passwordSchema),
|
|
9629
|
+
* fields: [
|
|
9630
|
+
* { key: 'password', type: 'input', label: 'Password', required: true, props: { type: 'password' } },
|
|
9631
|
+
* { key: 'confirmPassword', type: 'input', label: 'Confirm', required: true, props: { type: 'password' } },
|
|
9632
|
+
* { key: 'submit', type: 'submit', label: 'Register' },
|
|
9633
|
+
* ] as const,
|
|
9634
|
+
* });
|
|
9635
|
+
* ```
|
|
9636
|
+
*
|
|
9637
|
+
* @example Equivalent without helper
|
|
9638
|
+
* ```typescript
|
|
9639
|
+
* const config = {
|
|
9640
|
+
* schema: standardSchema(passwordSchema),
|
|
9641
|
+
* fields: [...],
|
|
9642
|
+
* } as const satisfies FormConfig;
|
|
9643
|
+
* ```
|
|
9644
|
+
*/
|
|
9645
|
+
function formConfig(config) {
|
|
9646
|
+
return config;
|
|
9647
|
+
}
|
|
9648
|
+
|
|
6417
9649
|
/**
|
|
6418
9650
|
* Converts DynamicText (string | Observable | Signal) to Observable<string>
|
|
6419
9651
|
* Unifies all three types into a consistent Observable stream
|
|
@@ -6520,10 +9752,10 @@ class DynamicTextPipe {
|
|
|
6520
9752
|
}
|
|
6521
9753
|
return of(value || '');
|
|
6522
9754
|
}
|
|
6523
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0
|
|
6524
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0
|
|
9755
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
9756
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
|
|
6525
9757
|
}
|
|
6526
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0
|
|
9758
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DynamicTextPipe, decorators: [{
|
|
6527
9759
|
type: Pipe,
|
|
6528
9760
|
args: [{
|
|
6529
9761
|
name: 'dynamicText',
|
|
@@ -6632,14 +9864,14 @@ class RowFieldComponent {
|
|
|
6632
9864
|
}
|
|
6633
9865
|
});
|
|
6634
9866
|
}
|
|
6635
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0
|
|
6636
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0
|
|
9867
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RowFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9868
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: RowFieldComponent, isStandalone: true, selector: "div[row-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "class.disabled": "disabled()", "id": "`${key()}`", "attr.data-testid": "key()" } }, ngImport: i0, template: `
|
|
6637
9869
|
@for (field of resolvedFields(); track field.key) {
|
|
6638
9870
|
<ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
|
|
6639
9871
|
}
|
|
6640
9872
|
`, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:flex;flex-wrap:wrap;gap:var(--df-grid-gap);align-items:flex-start;width:100%}:host ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto}:host ::ng-deep>.df-col-1{flex:0 0 calc(8.3333333333% - var(--df-grid-gap) * .9166666667);max-width:calc(8.3333333333% - var(--df-grid-gap) * .9166666667)}:host ::ng-deep>.df-col-2{flex:0 0 calc(16.6666666667% - var(--df-grid-gap) * .8333333333);max-width:calc(16.6666666667% - var(--df-grid-gap) * .8333333333)}:host ::ng-deep>.df-col-3{flex:0 0 calc(25% - var(--df-grid-gap) * .75);max-width:calc(25% - var(--df-grid-gap) * .75)}:host ::ng-deep>.df-col-4{flex:0 0 calc(33.3333333333% - var(--df-grid-gap) * .6666666667);max-width:calc(33.3333333333% - var(--df-grid-gap) * .6666666667)}:host ::ng-deep>.df-col-5{flex:0 0 calc(41.6666666667% - var(--df-grid-gap) * .5833333333);max-width:calc(41.6666666667% - var(--df-grid-gap) * .5833333333)}:host ::ng-deep>.df-col-6{flex:0 0 calc(50% - var(--df-grid-gap) * .5);max-width:calc(50% - var(--df-grid-gap) * .5)}:host ::ng-deep>.df-col-7{flex:0 0 calc(58.3333333333% - var(--df-grid-gap) * .4166666667);max-width:calc(58.3333333333% - var(--df-grid-gap) * .4166666667)}:host ::ng-deep>.df-col-8{flex:0 0 calc(66.6666666667% - var(--df-grid-gap) * .3333333333);max-width:calc(66.6666666667% - var(--df-grid-gap) * .3333333333)}:host ::ng-deep>.df-col-9{flex:0 0 calc(75% - var(--df-grid-gap) * .25);max-width:calc(75% - var(--df-grid-gap) * .25)}:host ::ng-deep>.df-col-10{flex:0 0 calc(83.3333333333% - var(--df-grid-gap) * .1666666667);max-width:calc(83.3333333333% - var(--df-grid-gap) * .1666666667)}:host ::ng-deep>.df-col-11{flex:0 0 calc(91.6666666667% - var(--df-grid-gap) * .0833333333);max-width:calc(91.6666666667% - var(--df-grid-gap) * .0833333333)}:host ::ng-deep>.df-col-12{flex:0 0 calc(100% - var(--df-grid-gap) * 0);max-width:calc(100% - var(--df-grid-gap) * 0)}:host.df-gap-custom{--df-grid-gap: var(--row-custom-gap, 1rem)}@media(max-width:576px){:host.df-row-mobile-stack{flex-direction:column}:host.df-row-mobile-stack ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}}@media(min-width:577px)and (max-width:768px){:host.df-row-tablet-2col ::ng-deep>*{flex:0 0 calc(50% - var(--df-grid-gap) / 2);max-width:calc(50% - var(--df-grid-gap) / 2)}}@media(max-width:576px){:host{flex-direction:column}:host ::ng-deep>*{flex:0 0 100%!important;max-width:100%!important}:host.df-row-mobile-keep-cols{flex-direction:row}:host.df-row-mobile-keep-cols ::ng-deep>*:not([class*=df-col-]){flex:0 0 auto!important;max-width:none!important}}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6641
9873
|
}
|
|
6642
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0
|
|
9874
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RowFieldComponent, decorators: [{
|
|
6643
9875
|
type: Component,
|
|
6644
9876
|
args: [{ selector: 'div[row-field]', imports: [NgComponentOutlet], template: `
|
|
6645
9877
|
@for (field of resolvedFields(); track field.key) {
|
|
@@ -6770,7 +10002,6 @@ class GroupFieldComponent {
|
|
|
6770
10002
|
value: this.parentFieldSignalContext.value,
|
|
6771
10003
|
defaultValues: this.defaultValues,
|
|
6772
10004
|
form: this.nestedFieldTree(),
|
|
6773
|
-
defaultValidationMessages: this.parentFieldSignalContext.defaultValidationMessages,
|
|
6774
10005
|
};
|
|
6775
10006
|
return Injector.create({
|
|
6776
10007
|
parent: this.injector,
|
|
@@ -6815,14 +10046,14 @@ class GroupFieldComponent {
|
|
|
6815
10046
|
}
|
|
6816
10047
|
});
|
|
6817
10048
|
}
|
|
6818
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0
|
|
6819
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0
|
|
10049
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10050
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: GroupFieldComponent, isStandalone: true, selector: "fieldset[group-field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { validityChange: "validityChange", dirtyChange: "dirtyChange", submitted: "submitted" }, host: { attributes: { "role": "group" }, properties: { "class": "hostClasses()", "class.disabled": "disabled()", "id": "`${key()}`", "attr.data-testid": "key()" } }, ngImport: i0, template: `
|
|
6820
10051
|
@for (field of resolvedFields(); track field.key) {
|
|
6821
10052
|
<ng-container *ngComponentOutlet="field.component; injector: field.injector; inputs: field.inputs()" />
|
|
6822
10053
|
}
|
|
6823
10054
|
`, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{border:none;margin:0;padding:0;min-width:0;display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}:host>*{grid-column:1/-1}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6824
10055
|
}
|
|
6825
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0
|
|
10056
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: GroupFieldComponent, decorators: [{
|
|
6826
10057
|
type: Component,
|
|
6827
10058
|
args: [{ selector: 'fieldset[group-field]', imports: [NgComponentOutlet], template: `
|
|
6828
10059
|
@for (field of resolvedFields(); track field.key) {
|
|
@@ -6999,7 +10230,6 @@ function createItemInjector(options) {
|
|
|
6999
10230
|
value: parentFieldSignalContext.value,
|
|
7000
10231
|
defaultValues: () => ({}),
|
|
7001
10232
|
form: formRef,
|
|
7002
|
-
defaultValidationMessages: parentFieldSignalContext.defaultValidationMessages,
|
|
7003
10233
|
};
|
|
7004
10234
|
const injector = Injector.create({
|
|
7005
10235
|
parent: parentInjector,
|
|
@@ -7261,14 +10491,14 @@ class ArrayFieldComponent {
|
|
|
7261
10491
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7262
10492
|
parentForm().value.set({ ...currentValue, [arrayKey]: newArray });
|
|
7263
10493
|
}
|
|
7264
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0
|
|
7265
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0
|
|
10494
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10495
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: ArrayFieldComponent, isStandalone: true, selector: "array-field", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "id": "`${key()}`", "attr.data-testid": "key()" } }, ngImport: i0, template: `
|
|
7266
10496
|
@for (item of resolvedItems(); track item.id) {
|
|
7267
10497
|
<ng-container *ngComponentOutlet="item.component; injector: item.injector; inputs: item.inputs()" />
|
|
7268
10498
|
}
|
|
7269
10499
|
`, isInline: true, styles: [":host,.df-form{--df-grid-columns: 12;--df-grid-gap: .5rem;--df-grid-row-gap: .5rem;--df-breakpoint-sm: 576px;--df-breakpoint-md: 768px;--df-breakpoint-lg: 992px;--df-breakpoint-xl: 1200px;--df-grid-gap-sm: .5rem;--df-grid-gap-md: .5rem;--df-grid-gap-lg: .5rem;--df-grid-gap-xl: .5rem;--df-grid-row-gap-sm: .5rem;--df-grid-row-gap-md: .5rem;--df-grid-row-gap-lg: .5rem;--df-grid-row-gap-xl: .5rem;--df-group-gap: var(--df-grid-gap);--df-group-padding: var(--df-grid-gap)}.df-form{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap);width:100%}.df-form>*{grid-column:1/-1}.df-row{display:grid;grid-template-columns:repeat(var(--df-grid-columns, 12),1fr);gap:var(--df-grid-gap);align-items:start;width:100%}.df-row>*:not([class*=df-col-]){grid-column:1/-1}.df-col-1{grid-column:span 1}.df-col-2{grid-column:span 2}.df-col-3{grid-column:span 3}.df-col-4{grid-column:span 4}.df-col-5{grid-column:span 5}.df-col-6{grid-column:span 6}.df-col-7{grid-column:span 7}.df-col-8{grid-column:span 8}.df-col-9{grid-column:span 9}.df-col-10{grid-column:span 10}.df-col-11{grid-column:span 11}.df-col-12{grid-column:span 12}.df-col-auto{grid-column:span auto;width:auto}.df-col-full{grid-column:1/-1}.df-col-start-1{grid-column-start:1}.df-col-start-2{grid-column-start:2}.df-col-start-3{grid-column-start:3}.df-col-start-4{grid-column-start:4}.df-col-start-5{grid-column-start:5}.df-col-start-6{grid-column-start:6}.df-col-start-7{grid-column-start:7}.df-col-start-8{grid-column-start:8}.df-col-start-9{grid-column-start:9}.df-col-start-10{grid-column-start:10}.df-col-start-11{grid-column-start:11}.df-col-start-12{grid-column-start:12}.df-col-end-1{grid-column-end:1}.df-col-end-2{grid-column-end:2}.df-col-end-3{grid-column-end:3}.df-col-end-4{grid-column-end:4}.df-col-end-5{grid-column-end:5}.df-col-end-6{grid-column-end:6}.df-col-end-7{grid-column-end:7}.df-col-end-8{grid-column-end:8}.df-col-end-9{grid-column-end:9}.df-col-end-10{grid-column-end:10}.df-col-end-11{grid-column-end:11}.df-col-end-12{grid-column-end:12}.df-col-end-13{grid-column-end:13}@media(max-width:576px){.df-form{--df-grid-gap: var(--df-grid-gap-sm);--df-grid-row-gap: var(--df-grid-row-gap-sm)}.df-row{grid-template-columns:1fr}.df-row>*{grid-column:1/-1!important}.df-row.df-row-mobile-keep-cols{grid-template-columns:repeat(var(--df-grid-columns),1fr)}.df-row.df-row-mobile-keep-cols>*{grid-column:revert!important}}@media(min-width:577px)and (max-width:768px){.df-form{--df-grid-gap: var(--df-grid-gap-md);--df-grid-row-gap: var(--df-grid-row-gap-md)}.df-row{--df-grid-columns: 6}.df-col-sm-1{grid-column:span 1}.df-col-sm-2{grid-column:span 2}.df-col-sm-3{grid-column:span 3}.df-col-sm-4{grid-column:span 4}.df-col-sm-5{grid-column:span 5}.df-col-sm-6{grid-column:span 6}.df-col-sm-full{grid-column:1/-1}}@media(min-width:769px)and (max-width:992px){.df-form{--df-grid-gap: var(--df-grid-gap-lg);--df-grid-row-gap: var(--df-grid-row-gap-lg)}.df-col-md-1{grid-column:span 1}.df-col-md-2{grid-column:span 2}.df-col-md-3{grid-column:span 3}.df-col-md-4{grid-column:span 4}.df-col-md-5{grid-column:span 5}.df-col-md-6{grid-column:span 6}.df-col-md-7{grid-column:span 7}.df-col-md-8{grid-column:span 8}.df-col-md-9{grid-column:span 9}.df-col-md-10{grid-column:span 10}.df-col-md-11{grid-column:span 11}.df-col-md-12{grid-column:span 12}.df-col-md-full{grid-column:1/-1}}@media(min-width:993px){.df-form{--df-grid-gap: var(--df-grid-gap-xl);--df-grid-row-gap: var(--df-grid-row-gap-xl)}.df-col-lg-1{grid-column:span 1}.df-col-lg-2{grid-column:span 2}.df-col-lg-3{grid-column:span 3}.df-col-lg-4{grid-column:span 4}.df-col-lg-5{grid-column:span 5}.df-col-lg-6{grid-column:span 6}.df-col-lg-7{grid-column:span 7}.df-col-lg-8{grid-column:span 8}.df-col-lg-9{grid-column:span 9}.df-col-lg-10{grid-column:span 10}.df-col-lg-11{grid-column:span 11}.df-col-lg-12{grid-column:span 12}.df-col-lg-full{grid-column:1/-1}}.df-gap-none{--df-grid-gap: 0}.df-gap-xs{--df-grid-gap: .25rem}.df-gap-sm{--df-grid-gap: .5rem}.df-gap-md{--df-grid-gap: 1rem}.df-gap-lg{--df-grid-gap: 1.5rem}.df-gap-xl{--df-grid-gap: 2rem}.df-row-gap-none{--df-grid-row-gap: 0}.df-row-gap-xs{--df-grid-row-gap: .25rem}.df-row-gap-sm{--df-grid-row-gap: .5rem}.df-row-gap-md{--df-grid-row-gap: 1rem}.df-row-gap-lg{--df-grid-row-gap: 1.5rem}.df-row-gap-xl{--df-grid-row-gap: 2rem}.df-field{display:block;width:100%;min-width:0;overflow:hidden;margin:0}.df-group,.df-page{display:block;width:100%}.df-form.disabled,.df-row.disabled,.df-field.disabled{opacity:.6;pointer-events:none}.df-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-sr-only-focusable:focus,.df-sr-only-focusable:active{position:static;width:auto;height:auto;padding:inherit;margin:inherit;overflow:visible;clip:auto;white-space:normal}.df-live-region{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.df-form :focus-visible{outline:2px solid var(--df-focus-color, #005fcc);outline-offset:2px}.df-group:focus-within{outline:1px dashed var(--df-focus-color, #005fcc);outline-offset:4px}:host{display:grid;grid-template-columns:1fr;gap:var(--df-grid-row-gap, .5rem);width:100%}:host>*{grid-column:1/-1}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7270
10500
|
}
|
|
7271
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0
|
|
10501
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ArrayFieldComponent, decorators: [{
|
|
7272
10502
|
type: Component,
|
|
7273
10503
|
args: [{ selector: 'array-field', imports: [NgComponentOutlet], template: `
|
|
7274
10504
|
@for (item of resolvedItems(); track item.id) {
|
|
@@ -7416,10 +10646,6 @@ function extractErrorParams(error) {
|
|
|
7416
10646
|
* - Field mappers (valueFieldMapper, checkboxFieldMapper, etc.)
|
|
7417
10647
|
* - Error utilities (createResolvedErrorsSignal, shouldShowErrors)
|
|
7418
10648
|
*
|
|
7419
|
-
* ## Testing API
|
|
7420
|
-
* Import from '@ng-forge/dynamic-forms/testing' for:
|
|
7421
|
-
* - Test utilities and harnesses
|
|
7422
|
-
* - Form config builders for testing
|
|
7423
10649
|
*/
|
|
7424
10650
|
// ============================================================
|
|
7425
10651
|
// PUBLIC API - For end users
|
|
@@ -7430,5 +10656,5 @@ function extractErrorParams(error) {
|
|
|
7430
10656
|
* Generated bundle index. Do not edit.
|
|
7431
10657
|
*/
|
|
7432
10658
|
|
|
7433
|
-
export {
|
|
7434
|
-
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-
|
|
10659
|
+
export { applyValidator as $, AddArrayItemEvent as A, DynamicFormError as B, ConsoleLogger as C, DynamicFormLogger as D, EventBus as E, FormClearEvent as F, baseFieldMapper as G, buildBaseInputs as H, arrayFieldMapper as I, groupFieldMapper as J, pageFieldMapper as K, rowFieldMapper as L, textFieldMapper as M, NoopLogger as N, FIELD_REGISTRY as O, PageChangeEvent as P, ARRAY_CONTEXT as Q, RemoveArrayItemEvent as R, SubmitEvent as S, DEFAULT_PROPS as T, DEFAULT_VALIDATION_MESSAGES as U, FIELD_SIGNAL_CONTEXT as V, FORM_OPTIONS as W, dynamicTextToObservable as X, ArrayFieldComponent as Y, GroupFieldComponent as Z, RowFieldComponent as _, DynamicTextPipe as a, applyValidators as a0, getArrayLength as a1, resolveTokens as a2, BUILT_IN_FIELDS as a3, isEqual as a4, omit as a5, interpolateParams as a6, applyMetaToElement as a7, RootFormRegistryService as a8, 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, formConfig as z };
|
|
10660
|
+
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-WGww2nXx.mjs.map
|