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