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