@ng-forge/dynamic-forms 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +120 -6
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-WGww2nXx.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-BWFdMPyw.mjs} +740 -325
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BWFdMPyw.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-CrO9WTDw.mjs → ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-CrO9WTDw.mjs.map → ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BcFx_kOz.mjs → ng-forge-dynamic-forms-text-field.component-DIVyioMX.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-BcFx_kOz.mjs.map → ng-forge-dynamic-forms-text-field.component-DIVyioMX.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +1 -1
- package/types/ng-forge-dynamic-forms-integration.d.ts +3 -0
- package/types/ng-forge-dynamic-forms.d.ts +341 -77
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-WGww2nXx.mjs.map +0 -1
|
@@ -300,7 +300,7 @@ function resolveField(fieldDef, context) {
|
|
|
300
300
|
inputs,
|
|
301
301
|
};
|
|
302
302
|
}), catchError((error) => {
|
|
303
|
-
// Only
|
|
303
|
+
// Only call onError if component is not destroyed to avoid accessing cleaned-up state
|
|
304
304
|
if (!context.destroyRef.destroyed) {
|
|
305
305
|
context.onError?.(fieldDef, error);
|
|
306
306
|
}
|
|
@@ -348,19 +348,22 @@ function createFieldResolutionPipe(getContext) {
|
|
|
348
348
|
* Wraps a submission action to handle both Promise and Observable returns.
|
|
349
349
|
* Converts Observables to Promises for compatibility with Angular Signal Forms' submit().
|
|
350
350
|
*
|
|
351
|
+
* The return type uses `void` to match Angular Signal Forms' expected action signature.
|
|
352
|
+
* The actual result from the action is preserved but typed as void since Angular's
|
|
353
|
+
* submit() function handles the result internally.
|
|
354
|
+
*
|
|
351
355
|
* @param action - The submission action function
|
|
352
356
|
* @returns A wrapped function that always returns a Promise
|
|
353
357
|
*/
|
|
354
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Form tree type is dynamic, action signatures vary
|
|
355
358
|
function wrapSubmissionAction(action) {
|
|
356
|
-
|
|
357
|
-
return (formTree) => {
|
|
359
|
+
return async (formTree) => {
|
|
358
360
|
const result = action(formTree);
|
|
359
361
|
// If the action returns an Observable, convert it to a Promise
|
|
360
362
|
if (isObservable(result)) {
|
|
361
|
-
|
|
363
|
+
await firstValueFrom(result);
|
|
364
|
+
return;
|
|
362
365
|
}
|
|
363
|
-
|
|
366
|
+
await Promise.resolve(result);
|
|
364
367
|
};
|
|
365
368
|
}
|
|
366
369
|
/**
|
|
@@ -398,6 +401,8 @@ function createSubmissionHandler(options) {
|
|
|
398
401
|
if (!submissionConfig?.action) {
|
|
399
402
|
return EMPTY;
|
|
400
403
|
}
|
|
404
|
+
// Type assertion needed: submission.action accepts the form tree but its signature
|
|
405
|
+
// is defined broadly in FormConfig. The actual runtime type is FieldTree<TModel>.
|
|
401
406
|
const wrappedAction = wrapSubmissionAction(submissionConfig.action);
|
|
402
407
|
// Use Angular Signal Forms' native submit() function
|
|
403
408
|
// This automatically:
|
|
@@ -837,6 +842,36 @@ const DEFAULT_VALIDATION_MESSAGES = new InjectionToken('DEFAULT_VALIDATION_MESSA
|
|
|
837
842
|
* ```
|
|
838
843
|
*/
|
|
839
844
|
const FORM_OPTIONS = new InjectionToken('FORM_OPTIONS');
|
|
845
|
+
/**
|
|
846
|
+
* Injection token for external data signals.
|
|
847
|
+
*
|
|
848
|
+
* External data provides a way to inject application state into form expressions.
|
|
849
|
+
* Each property in the record is a Signal that gets unwrapped and made available
|
|
850
|
+
* in the `EvaluationContext` under `externalData`.
|
|
851
|
+
*
|
|
852
|
+
* Like DEFAULT_PROPS, this token is provided ONCE at the DynamicForm level
|
|
853
|
+
* and inherited by all children via Angular's hierarchical injector.
|
|
854
|
+
*
|
|
855
|
+
* The token provides a Signal containing the external data record, which itself
|
|
856
|
+
* contains Signals. This allows the form to react to changes in which external
|
|
857
|
+
* data is available (outer Signal) and to changes in the external data values
|
|
858
|
+
* (inner Signals).
|
|
859
|
+
*
|
|
860
|
+
* @example
|
|
861
|
+
* ```typescript
|
|
862
|
+
* // In FieldContextRegistryService
|
|
863
|
+
* const externalDataSignal = inject(EXTERNAL_DATA, { optional: true });
|
|
864
|
+
*
|
|
865
|
+
* // When creating evaluation context:
|
|
866
|
+
* const externalDataRecord = externalDataSignal?.();
|
|
867
|
+
* const resolvedExternalData = externalDataRecord
|
|
868
|
+
* ? Object.fromEntries(
|
|
869
|
+
* Object.entries(externalDataRecord).map(([k, v]) => [k, v()])
|
|
870
|
+
* )
|
|
871
|
+
* : {};
|
|
872
|
+
* ```
|
|
873
|
+
*/
|
|
874
|
+
const EXTERNAL_DATA = new InjectionToken('EXTERNAL_DATA');
|
|
840
875
|
|
|
841
876
|
/**
|
|
842
877
|
* Base error class for all Dynamic Forms errors.
|
|
@@ -1179,7 +1214,7 @@ class RootFormRegistryService {
|
|
|
1179
1214
|
}
|
|
1180
1215
|
}
|
|
1181
1216
|
catch {
|
|
1182
|
-
// Ignore errors when reading form value
|
|
1217
|
+
// Ignore errors when reading form value - can happen during initialization
|
|
1183
1218
|
}
|
|
1184
1219
|
}
|
|
1185
1220
|
return {};
|
|
@@ -1243,6 +1278,7 @@ function isChildFieldContext(context) {
|
|
|
1243
1278
|
class FieldContextRegistryService {
|
|
1244
1279
|
rootFormRegistry = inject(RootFormRegistryService);
|
|
1245
1280
|
logger = inject(DynamicFormLogger);
|
|
1281
|
+
externalDataSignal = inject(EXTERNAL_DATA, { optional: true });
|
|
1246
1282
|
/**
|
|
1247
1283
|
* Creates an evaluation context for a field by combining:
|
|
1248
1284
|
* - The field's current value
|
|
@@ -1277,6 +1313,7 @@ class FieldContextRegistryService {
|
|
|
1277
1313
|
formValue,
|
|
1278
1314
|
fieldPath,
|
|
1279
1315
|
customFunctions: customFunctions || {},
|
|
1316
|
+
externalData: this.resolveExternalData(false),
|
|
1280
1317
|
logger: this.logger,
|
|
1281
1318
|
};
|
|
1282
1319
|
}
|
|
@@ -1295,6 +1332,24 @@ class FieldContextRegistryService {
|
|
|
1295
1332
|
// For root fields or when key is not available
|
|
1296
1333
|
return '';
|
|
1297
1334
|
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Resolves external data signals to their current values.
|
|
1337
|
+
*
|
|
1338
|
+
* @param reactive - If true, reads signals reactively (creates dependencies).
|
|
1339
|
+
* If false, reads signals with untracked() (no dependencies).
|
|
1340
|
+
* @returns Record of resolved external data values, or undefined if no external data.
|
|
1341
|
+
*/
|
|
1342
|
+
resolveExternalData(reactive) {
|
|
1343
|
+
const externalDataRecord = reactive ? this.externalDataSignal?.() : untracked(() => this.externalDataSignal?.());
|
|
1344
|
+
if (!externalDataRecord) {
|
|
1345
|
+
return undefined;
|
|
1346
|
+
}
|
|
1347
|
+
const resolved = {};
|
|
1348
|
+
for (const [key, signal] of Object.entries(externalDataRecord)) {
|
|
1349
|
+
resolved[key] = reactive ? signal() : untracked(() => signal());
|
|
1350
|
+
}
|
|
1351
|
+
return resolved;
|
|
1352
|
+
}
|
|
1298
1353
|
/**
|
|
1299
1354
|
* Creates a REACTIVE evaluation context for logic functions.
|
|
1300
1355
|
*
|
|
@@ -1323,6 +1378,7 @@ class FieldContextRegistryService {
|
|
|
1323
1378
|
formValue,
|
|
1324
1379
|
fieldPath,
|
|
1325
1380
|
customFunctions: customFunctions || {},
|
|
1381
|
+
externalData: this.resolveExternalData(true),
|
|
1326
1382
|
logger: this.logger,
|
|
1327
1383
|
};
|
|
1328
1384
|
}
|
|
@@ -1347,6 +1403,7 @@ class FieldContextRegistryService {
|
|
|
1347
1403
|
formValue,
|
|
1348
1404
|
fieldPath,
|
|
1349
1405
|
customFunctions: customFunctions || {},
|
|
1406
|
+
externalData: this.resolveExternalData(true),
|
|
1350
1407
|
logger: this.logger,
|
|
1351
1408
|
};
|
|
1352
1409
|
}
|
|
@@ -2714,14 +2771,18 @@ function evaluateCondition(expression, context) {
|
|
|
2714
2771
|
}
|
|
2715
2772
|
}
|
|
2716
2773
|
function evaluateFieldValueCondition(expression, context) {
|
|
2717
|
-
if (!expression.fieldPath || !expression.operator)
|
|
2774
|
+
if (!expression.fieldPath || !expression.operator) {
|
|
2775
|
+
context.logger.debug('Invalid fieldValue condition config: missing fieldPath or operator', expression);
|
|
2718
2776
|
return false;
|
|
2777
|
+
}
|
|
2719
2778
|
const fieldValue = getNestedValue(context.formValue, expression.fieldPath);
|
|
2720
2779
|
return compareValues(fieldValue, expression.value, expression.operator);
|
|
2721
2780
|
}
|
|
2722
2781
|
function evaluateFormValueCondition(expression, context) {
|
|
2723
|
-
if (!expression.operator)
|
|
2782
|
+
if (!expression.operator) {
|
|
2783
|
+
context.logger.debug('Invalid formValue condition config: missing operator', expression);
|
|
2724
2784
|
return false;
|
|
2785
|
+
}
|
|
2725
2786
|
return compareValues(context.formValue, expression.value, expression.operator);
|
|
2726
2787
|
}
|
|
2727
2788
|
function evaluateJavaScriptExpression(expression, context) {
|
|
@@ -3463,9 +3524,7 @@ function createFunctionValidator(config) {
|
|
|
3463
3524
|
const registry = inject(FunctionRegistryService);
|
|
3464
3525
|
const validatorFn = registry.getValidator(functionName);
|
|
3465
3526
|
if (!validatorFn) {
|
|
3466
|
-
|
|
3467
|
-
`Ensure it's registered using customFnConfig.validators or check the function name for typos.`);
|
|
3468
|
-
return () => null;
|
|
3527
|
+
throw new DynamicFormError(`Custom validator "${functionName}" not found. Register it with customFnConfig.validators.`);
|
|
3469
3528
|
}
|
|
3470
3529
|
return (ctx) => validatorFn(ctx, config.params);
|
|
3471
3530
|
}
|
|
@@ -3517,13 +3576,10 @@ function createExpressionValidator(config) {
|
|
|
3517
3576
|
* - onError: Optional handler for resource errors
|
|
3518
3577
|
*/
|
|
3519
3578
|
function applyAsyncValidator(config, fieldPath) {
|
|
3520
|
-
const logger = inject(DynamicFormLogger);
|
|
3521
3579
|
const registry = inject(FunctionRegistryService);
|
|
3522
3580
|
const validatorConfig = registry.getAsyncValidator(config.functionName);
|
|
3523
3581
|
if (!validatorConfig) {
|
|
3524
|
-
|
|
3525
|
-
`Ensure it's registered using customFnConfig.asyncValidators or check the function name for typos.`);
|
|
3526
|
-
return;
|
|
3582
|
+
throw new DynamicFormError(`Async validator "${config.functionName}" not found. Register it with customFnConfig.asyncValidators.`);
|
|
3527
3583
|
}
|
|
3528
3584
|
const whenLogic = createConditionalLogic(config.when);
|
|
3529
3585
|
const asyncOptions = {
|
|
@@ -3547,13 +3603,10 @@ function applyAsyncValidator(config, fieldPath) {
|
|
|
3547
3603
|
* - onError: Optional handler for HTTP errors
|
|
3548
3604
|
*/
|
|
3549
3605
|
function applyHttpValidator(config, fieldPath) {
|
|
3550
|
-
const logger = inject(DynamicFormLogger);
|
|
3551
3606
|
const registry = inject(FunctionRegistryService);
|
|
3552
3607
|
const httpValidatorConfig = registry.getHttpValidator(config.functionName);
|
|
3553
3608
|
if (!httpValidatorConfig) {
|
|
3554
|
-
|
|
3555
|
-
`Ensure it's registered using customFnConfig.httpValidators or check the function name for typos.`);
|
|
3556
|
-
return;
|
|
3609
|
+
throw new DynamicFormError(`HTTP validator "${config.functionName}" not found. Register it with customFnConfig.httpValidators.`);
|
|
3557
3610
|
}
|
|
3558
3611
|
const whenLogic = createConditionalLogic(config.when);
|
|
3559
3612
|
const httpOptions = {
|
|
@@ -4275,9 +4328,9 @@ function hasChildFields(field) {
|
|
|
4275
4328
|
return 'fields' in field && field.fields != null;
|
|
4276
4329
|
}
|
|
4277
4330
|
/** Container field type names */
|
|
4278
|
-
const CONTAINER_TYPES = new Set(['page', 'row', 'group', 'array']);
|
|
4331
|
+
const CONTAINER_TYPES$1 = new Set(['page', 'row', 'group', 'array']);
|
|
4279
4332
|
function isContainerField(field) {
|
|
4280
|
-
return CONTAINER_TYPES.has(field.type);
|
|
4333
|
+
return CONTAINER_TYPES$1.has(field.type);
|
|
4281
4334
|
}
|
|
4282
4335
|
/**
|
|
4283
4336
|
* Type guard to check if a field is a leaf field (value or display field)
|
|
@@ -4366,8 +4419,27 @@ function createFormLevelSchema(formLevelSchema) {
|
|
|
4366
4419
|
}
|
|
4367
4420
|
|
|
4368
4421
|
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4422
|
+
/**
|
|
4423
|
+
* Gets a FieldTree by key, supporting nested paths with dot notation.
|
|
4424
|
+
*
|
|
4425
|
+
* @example
|
|
4426
|
+
* getFieldTreeByKey(ctx, 'address.street') // Returns FieldTree for nested 'street' field
|
|
4427
|
+
*/
|
|
4369
4428
|
function getFieldTreeByKey(ctx, key) {
|
|
4370
|
-
|
|
4429
|
+
// Simple case - no nesting
|
|
4430
|
+
if (!key.includes('.')) {
|
|
4431
|
+
return ctx.fieldTree[key];
|
|
4432
|
+
}
|
|
4433
|
+
// Nested path - traverse through the structure
|
|
4434
|
+
const parts = key.split('.');
|
|
4435
|
+
let current = ctx.fieldTree;
|
|
4436
|
+
for (const part of parts) {
|
|
4437
|
+
if (!current || typeof current !== 'object') {
|
|
4438
|
+
return undefined;
|
|
4439
|
+
}
|
|
4440
|
+
current = current[part];
|
|
4441
|
+
}
|
|
4442
|
+
return current;
|
|
4371
4443
|
}
|
|
4372
4444
|
/**
|
|
4373
4445
|
* Creates an Angular signal forms schema from field definitions
|
|
@@ -4539,6 +4611,7 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
|
|
|
4539
4611
|
// Validation failed - create error targeting the source field
|
|
4540
4612
|
const targetField = getFieldTreeByKey(ctx, sourceFieldKey);
|
|
4541
4613
|
if (!targetField) {
|
|
4614
|
+
evaluationContext.logger.warn(`Cross-field validator references non-existent field "${sourceFieldKey}"`);
|
|
4542
4615
|
return null;
|
|
4543
4616
|
}
|
|
4544
4617
|
const errorObj = {
|
|
@@ -4551,8 +4624,8 @@ function evaluateCustomCrossFieldValidator(config, evaluationContext, sourceFiel
|
|
|
4551
4624
|
try {
|
|
4552
4625
|
errorObj[key] = ExpressionParser.evaluate(expression, evaluationContext);
|
|
4553
4626
|
}
|
|
4554
|
-
catch {
|
|
4555
|
-
|
|
4627
|
+
catch (error) {
|
|
4628
|
+
evaluationContext.logger.warn(`Failed to evaluate error param expression "${key}": ${expression}`, error);
|
|
4556
4629
|
}
|
|
4557
4630
|
}
|
|
4558
4631
|
}
|
|
@@ -4586,6 +4659,7 @@ function evaluateBuiltInCrossFieldValidator(config, evaluationContext, sourceFie
|
|
|
4586
4659
|
// Validation failed - create error targeting the source field
|
|
4587
4660
|
const targetField = getFieldTreeByKey(ctx, sourceFieldKey);
|
|
4588
4661
|
if (!targetField) {
|
|
4662
|
+
logger.warn(`Cross-field validator references non-existent field "${sourceFieldKey}"`);
|
|
4589
4663
|
return null;
|
|
4590
4664
|
}
|
|
4591
4665
|
return {
|
|
@@ -4686,6 +4760,54 @@ function fieldsToDefaultValues(fields, registry) {
|
|
|
4686
4760
|
return defaultValues;
|
|
4687
4761
|
}
|
|
4688
4762
|
|
|
4763
|
+
/**
|
|
4764
|
+
* Injection token for globally enabling form value emission on events.
|
|
4765
|
+
*
|
|
4766
|
+
* When set to `true`, all events dispatched through the EventBus will include
|
|
4767
|
+
* the current form value in the `formValue` property.
|
|
4768
|
+
*
|
|
4769
|
+
* This token is configured via `withEventFormValue()` feature function.
|
|
4770
|
+
* Per-form overrides can be set via `options.emitFormValueOnEvents` in the form config.
|
|
4771
|
+
*
|
|
4772
|
+
* @internal
|
|
4773
|
+
*/
|
|
4774
|
+
const EMIT_FORM_VALUE_ON_EVENTS = new InjectionToken('EMIT_FORM_VALUE_ON_EVENTS', {
|
|
4775
|
+
providedIn: 'root',
|
|
4776
|
+
factory: () => false,
|
|
4777
|
+
});
|
|
4778
|
+
|
|
4779
|
+
/**
|
|
4780
|
+
* Safely attempts to inject a dependency using an InjectionToken.
|
|
4781
|
+
* Returns the default value if called outside an injection context.
|
|
4782
|
+
*/
|
|
4783
|
+
function safeInjectToken(token, defaultValue) {
|
|
4784
|
+
try {
|
|
4785
|
+
return inject(token, { optional: true }) ?? defaultValue;
|
|
4786
|
+
}
|
|
4787
|
+
catch {
|
|
4788
|
+
return defaultValue;
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
/**
|
|
4792
|
+
* Safely attempts to inject a service class.
|
|
4793
|
+
* Returns the default value if called outside an injection context.
|
|
4794
|
+
*/
|
|
4795
|
+
function safeInjectClass(token, defaultValue) {
|
|
4796
|
+
try {
|
|
4797
|
+
return inject(token, { optional: true }) ?? defaultValue;
|
|
4798
|
+
}
|
|
4799
|
+
catch {
|
|
4800
|
+
return defaultValue;
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
/**
|
|
4804
|
+
* Creates a copy of an event with an additional formValue property.
|
|
4805
|
+
* Preserves the event's prototype chain so instanceof checks still work.
|
|
4806
|
+
*/
|
|
4807
|
+
function attachFormValue(event, formValue) {
|
|
4808
|
+
const prototype = Object.getPrototypeOf(event);
|
|
4809
|
+
return Object.assign(Object.create(prototype), event, { formValue });
|
|
4810
|
+
}
|
|
4689
4811
|
/**
|
|
4690
4812
|
* Event bus service for form-wide event communication.
|
|
4691
4813
|
*
|
|
@@ -4720,6 +4842,9 @@ function fieldsToDefaultValues(fields, registry) {
|
|
|
4720
4842
|
*/
|
|
4721
4843
|
class EventBus {
|
|
4722
4844
|
pipeline$ = new Subject();
|
|
4845
|
+
globalEmitFormValue = safeInjectToken(EMIT_FORM_VALUE_ON_EVENTS, false);
|
|
4846
|
+
rootFormRegistry = safeInjectClass(RootFormRegistryService, null);
|
|
4847
|
+
formOptions = safeInjectToken(FORM_OPTIONS, null);
|
|
4723
4848
|
events$ = this.pipeline$.asObservable();
|
|
4724
4849
|
/**
|
|
4725
4850
|
* Dispatches an event to all subscribers.
|
|
@@ -4727,6 +4852,10 @@ class EventBus {
|
|
|
4727
4852
|
* Creates an instance of the provided event constructor and broadcasts it
|
|
4728
4853
|
* through the event pipeline to all active subscribers.
|
|
4729
4854
|
*
|
|
4855
|
+
* If `withEventFormValue()` is enabled globally or `options.emitFormValueOnEvents`
|
|
4856
|
+
* is set to `true` in the form config, the current form value will be attached
|
|
4857
|
+
* to the event's `formValue` property.
|
|
4858
|
+
*
|
|
4730
4859
|
* @param eventConstructor - Constructor function for the event to dispatch
|
|
4731
4860
|
*
|
|
4732
4861
|
* @example
|
|
@@ -4740,7 +4869,28 @@ class EventBus {
|
|
|
4740
4869
|
*/
|
|
4741
4870
|
// TypeScript limitation: Must use ConstructorParameters which relies on `any` in FormEventConstructor
|
|
4742
4871
|
dispatch(eventConstructor, ...args) {
|
|
4743
|
-
|
|
4872
|
+
const event = new eventConstructor(...args);
|
|
4873
|
+
if (this.shouldEmitFormValue()) {
|
|
4874
|
+
const formValue = this.rootFormRegistry?.getFormValue();
|
|
4875
|
+
// Only attach if form value exists and has at least one field.
|
|
4876
|
+
// Empty objects {} are not attached - use hasFormValue() to check.
|
|
4877
|
+
if (formValue && Object.keys(formValue).length > 0) {
|
|
4878
|
+
this.pipeline$.next(attachFormValue(event, formValue));
|
|
4879
|
+
return;
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
this.pipeline$.next(event);
|
|
4883
|
+
}
|
|
4884
|
+
/**
|
|
4885
|
+
* Determines whether form value should be attached to events.
|
|
4886
|
+
*
|
|
4887
|
+
* Precedence rules:
|
|
4888
|
+
* 1. Per-form setting (if defined) takes precedence
|
|
4889
|
+
* 2. Falls back to global setting
|
|
4890
|
+
*/
|
|
4891
|
+
shouldEmitFormValue() {
|
|
4892
|
+
const formLevelSetting = this.formOptions?.()?.emitFormValueOnEvents;
|
|
4893
|
+
return formLevelSetting ?? this.globalEmitFormValue;
|
|
4744
4894
|
}
|
|
4745
4895
|
/**
|
|
4746
4896
|
* Subscribes to form events with type-safe filtering.
|
|
@@ -5313,7 +5463,14 @@ function convertBuiltInToCustomValidator(config) {
|
|
|
5313
5463
|
validationExpression = `fieldValue == null || (typeof fieldValue !== 'string' && !Array.isArray(fieldValue)) || fieldValue.length <= (${expression})`;
|
|
5314
5464
|
break;
|
|
5315
5465
|
case 'pattern':
|
|
5316
|
-
|
|
5466
|
+
// Validate regex pattern before injection to prevent runtime errors
|
|
5467
|
+
try {
|
|
5468
|
+
new RegExp(expression);
|
|
5469
|
+
}
|
|
5470
|
+
catch {
|
|
5471
|
+
throw new Error(`Invalid regex pattern in cross-field validator: ${expression}`);
|
|
5472
|
+
}
|
|
5473
|
+
validationExpression = `fieldValue == null || new RegExp(${JSON.stringify(expression)}).test(fieldValue)`;
|
|
5317
5474
|
break;
|
|
5318
5475
|
default:
|
|
5319
5476
|
throw new Error(`Cannot convert ${config.type} validator to custom validator`);
|
|
@@ -5454,61 +5611,6 @@ function isValidFormConfiguration(fields) {
|
|
|
5454
5611
|
return FormModeValidator.validateFormConfiguration(fields).isValid;
|
|
5455
5612
|
}
|
|
5456
5613
|
|
|
5457
|
-
/**
|
|
5458
|
-
* Centralized constants for the derivation system.
|
|
5459
|
-
*
|
|
5460
|
-
* These constants configure the behavior of derivation processing,
|
|
5461
|
-
* caching, and path handling. Centralizing them enables:
|
|
5462
|
-
* - Easy configuration changes
|
|
5463
|
-
* - Consistent values across all derivation modules
|
|
5464
|
-
* - Clear documentation of magic numbers
|
|
5465
|
-
*
|
|
5466
|
-
* @module
|
|
5467
|
-
*/
|
|
5468
|
-
/**
|
|
5469
|
-
* Maximum number of derivation iterations before stopping to prevent infinite loops.
|
|
5470
|
-
*
|
|
5471
|
-
* The value of 10 is chosen based on:
|
|
5472
|
-
* - Most derivation chains are 2-3 levels deep (A→B→C)
|
|
5473
|
-
* - Complex forms with conditional cascades rarely exceed 5-6 levels
|
|
5474
|
-
* - 10 provides headroom for bidirectional sync patterns (A↔B) which may need
|
|
5475
|
-
* 2 iterations per pair to stabilize
|
|
5476
|
-
* - Higher values delay detection of actual infinite loops
|
|
5477
|
-
* - Lower values risk false positives on legitimate deep chains
|
|
5478
|
-
*
|
|
5479
|
-
* If you hit this limit legitimately, consider:
|
|
5480
|
-
* 1. Restructuring derivations to reduce chain depth
|
|
5481
|
-
* 2. Using explicit `dependsOn` to control evaluation order
|
|
5482
|
-
* 3. Breaking complex derivations into computed signals outside the form
|
|
5483
|
-
*/
|
|
5484
|
-
const MAX_DERIVATION_ITERATIONS = 10;
|
|
5485
|
-
/**
|
|
5486
|
-
* Placeholder string for array index in derivation paths.
|
|
5487
|
-
*
|
|
5488
|
-
* Used in paths like `items.$.quantity` to represent "each item in the array".
|
|
5489
|
-
* The `$` is resolved to actual indices at runtime during array derivation processing.
|
|
5490
|
-
*/
|
|
5491
|
-
const ARRAY_PLACEHOLDER = '.$.';
|
|
5492
|
-
/**
|
|
5493
|
-
* Maximum size of the expression AST cache.
|
|
5494
|
-
*
|
|
5495
|
-
* Expressions are parsed into Abstract Syntax Trees (AST) once and cached
|
|
5496
|
-
* to avoid re-parsing on each evaluation. The cache uses LRU eviction.
|
|
5497
|
-
*
|
|
5498
|
-
* 1000 entries handles:
|
|
5499
|
-
* - Large forms with many derivation expressions
|
|
5500
|
-
* - Multiple form instances with different expressions
|
|
5501
|
-
* - Memory efficiency (AST nodes are relatively small)
|
|
5502
|
-
*/
|
|
5503
|
-
const MAX_AST_CACHE_SIZE = 1000;
|
|
5504
|
-
/**
|
|
5505
|
-
* Delimiter used to create unique derivation keys from source and target field keys.
|
|
5506
|
-
*
|
|
5507
|
-
* Uses null character (\x00) which is extremely unlikely to appear in field names,
|
|
5508
|
-
* avoiding collision issues that could occur with common delimiters like ':'.
|
|
5509
|
-
*/
|
|
5510
|
-
const DERIVATION_KEY_DELIMITER = '\x00';
|
|
5511
|
-
|
|
5512
5614
|
/**
|
|
5513
5615
|
* Creates an empty derivation collection.
|
|
5514
5616
|
*
|
|
@@ -5535,32 +5637,31 @@ function createDerivationChainContext() {
|
|
|
5535
5637
|
/**
|
|
5536
5638
|
* Creates a unique key for a derivation entry.
|
|
5537
5639
|
*
|
|
5538
|
-
*
|
|
5539
|
-
*
|
|
5640
|
+
* Since derivations are self-targeting, the key is simply the field key.
|
|
5641
|
+
* This function exists for semantic clarity and to provide a single point
|
|
5642
|
+
* of change if the key format needs to be extended in the future.
|
|
5540
5643
|
*
|
|
5541
|
-
*
|
|
5542
|
-
*
|
|
5644
|
+
* Note: For array derivations, callers must resolve placeholders
|
|
5645
|
+
* (e.g., 'items.$.lineTotal' -> 'items.0.lineTotal') before calling this.
|
|
5646
|
+
*
|
|
5647
|
+
* @param fieldKey - The field key (with array index already resolved)
|
|
5543
5648
|
* @returns Unique key for the derivation
|
|
5544
5649
|
*
|
|
5545
5650
|
* @internal
|
|
5546
5651
|
*/
|
|
5547
|
-
function createDerivationKey(
|
|
5548
|
-
return
|
|
5652
|
+
function createDerivationKey(fieldKey) {
|
|
5653
|
+
return fieldKey;
|
|
5549
5654
|
}
|
|
5550
5655
|
/**
|
|
5551
|
-
* Parses a derivation key back into
|
|
5656
|
+
* Parses a derivation key back into field key.
|
|
5552
5657
|
*
|
|
5553
5658
|
* @param key - The derivation key to parse
|
|
5554
|
-
* @returns Object containing
|
|
5659
|
+
* @returns Object containing the field key
|
|
5555
5660
|
*
|
|
5556
5661
|
* @internal
|
|
5557
5662
|
*/
|
|
5558
5663
|
function parseDerivationKey(key) {
|
|
5559
|
-
|
|
5560
|
-
return {
|
|
5561
|
-
sourceKey: key.substring(0, delimiterIndex),
|
|
5562
|
-
targetKey: key.substring(delimiterIndex + DERIVATION_KEY_DELIMITER.length),
|
|
5563
|
-
};
|
|
5664
|
+
return { fieldKey: key };
|
|
5564
5665
|
}
|
|
5565
5666
|
|
|
5566
5667
|
/**
|
|
@@ -5691,9 +5792,12 @@ function extractArrayPath(path) {
|
|
|
5691
5792
|
return parseArrayPath(path).arrayPath;
|
|
5692
5793
|
}
|
|
5693
5794
|
/**
|
|
5694
|
-
* Splits a
|
|
5795
|
+
* Splits a path into segments, supporting both dot notation and bracket notation.
|
|
5695
5796
|
*
|
|
5696
|
-
* Handles
|
|
5797
|
+
* Handles:
|
|
5798
|
+
* - Dot notation: `parent.child.grandchild`
|
|
5799
|
+
* - Bracket notation: `items[0].quantity`
|
|
5800
|
+
* - Mixed notation: `items[0].address.city`
|
|
5697
5801
|
*
|
|
5698
5802
|
* @param path - The path to split
|
|
5699
5803
|
* @returns Array of path segments
|
|
@@ -5705,6 +5809,12 @@ function extractArrayPath(path) {
|
|
|
5705
5809
|
*
|
|
5706
5810
|
* splitPath('items.0.quantity')
|
|
5707
5811
|
* // ['items', '0', 'quantity']
|
|
5812
|
+
*
|
|
5813
|
+
* splitPath('items[0].quantity')
|
|
5814
|
+
* // ['items', '0', 'quantity']
|
|
5815
|
+
*
|
|
5816
|
+
* splitPath('a[0][1].b')
|
|
5817
|
+
* // ['a', '0', '1', 'b']
|
|
5708
5818
|
* ```
|
|
5709
5819
|
*
|
|
5710
5820
|
* @public
|
|
@@ -5712,7 +5822,12 @@ function extractArrayPath(path) {
|
|
|
5712
5822
|
function splitPath(path) {
|
|
5713
5823
|
if (!path)
|
|
5714
5824
|
return [];
|
|
5715
|
-
|
|
5825
|
+
// Convert bracket notation [n] to .n, then split by dots
|
|
5826
|
+
// Filter out empty segments from leading/trailing/consecutive dots
|
|
5827
|
+
return path
|
|
5828
|
+
.replace(/\[(\d+)\]/g, '.$1')
|
|
5829
|
+
.split('.')
|
|
5830
|
+
.filter(Boolean);
|
|
5716
5831
|
}
|
|
5717
5832
|
/**
|
|
5718
5833
|
* Joins path segments into a dot-separated path.
|
|
@@ -5912,8 +6027,7 @@ class DerivationLookup {
|
|
|
5912
6027
|
// Track the entries array reference to auto-invalidate on change
|
|
5913
6028
|
_cachedEntriesRef;
|
|
5914
6029
|
// Cached lookup maps (built lazily on first access)
|
|
5915
|
-
|
|
5916
|
-
_bySource;
|
|
6030
|
+
_byField;
|
|
5917
6031
|
_byDependency;
|
|
5918
6032
|
_byArrayPath;
|
|
5919
6033
|
_wildcardEntries;
|
|
@@ -5933,18 +6047,12 @@ class DerivationLookup {
|
|
|
5933
6047
|
return currentEntries;
|
|
5934
6048
|
}
|
|
5935
6049
|
/**
|
|
5936
|
-
* Map of
|
|
6050
|
+
* Map of field key to entries that target it.
|
|
6051
|
+
* Since derivations are self-targeting, this maps field keys to their derivations.
|
|
5937
6052
|
* Used for quick lookup when processing derivations.
|
|
5938
6053
|
*/
|
|
5939
|
-
get
|
|
5940
|
-
return (this.
|
|
5941
|
-
}
|
|
5942
|
-
/**
|
|
5943
|
-
* Map of source field key to entries defined on it.
|
|
5944
|
-
* Used for finding derivations when a field's value changes.
|
|
5945
|
-
*/
|
|
5946
|
-
get bySource() {
|
|
5947
|
-
return (this._bySource ??= this.buildBySourceMap());
|
|
6054
|
+
get byField() {
|
|
6055
|
+
return (this._byField ??= this.buildByFieldMap());
|
|
5948
6056
|
}
|
|
5949
6057
|
/**
|
|
5950
6058
|
* Map of dependency field key to entries that depend on it.
|
|
@@ -6039,8 +6147,7 @@ class DerivationLookup {
|
|
|
6039
6147
|
* Invalidates cached maps. Call this when entries change.
|
|
6040
6148
|
*/
|
|
6041
6149
|
invalidateCache() {
|
|
6042
|
-
this.
|
|
6043
|
-
this._bySource = undefined;
|
|
6150
|
+
this._byField = undefined;
|
|
6044
6151
|
this._byDependency = undefined;
|
|
6045
6152
|
this._byArrayPath = undefined;
|
|
6046
6153
|
this._wildcardEntries = undefined;
|
|
@@ -6050,21 +6157,12 @@ class DerivationLookup {
|
|
|
6050
6157
|
// ─────────────────────────────────────────────────────────────────
|
|
6051
6158
|
// Private builders
|
|
6052
6159
|
// ─────────────────────────────────────────────────────────────────
|
|
6053
|
-
|
|
6054
|
-
const map = new Map();
|
|
6055
|
-
for (const entry of this.entries) {
|
|
6056
|
-
const targetEntries = map.get(entry.targetFieldKey) ?? [];
|
|
6057
|
-
targetEntries.push(entry);
|
|
6058
|
-
map.set(entry.targetFieldKey, targetEntries);
|
|
6059
|
-
}
|
|
6060
|
-
return map;
|
|
6061
|
-
}
|
|
6062
|
-
buildBySourceMap() {
|
|
6160
|
+
buildByFieldMap() {
|
|
6063
6161
|
const map = new Map();
|
|
6064
6162
|
for (const entry of this.entries) {
|
|
6065
|
-
const
|
|
6066
|
-
|
|
6067
|
-
map.set(entry.
|
|
6163
|
+
const fieldEntries = map.get(entry.fieldKey) ?? [];
|
|
6164
|
+
fieldEntries.push(entry);
|
|
6165
|
+
map.set(entry.fieldKey, fieldEntries);
|
|
6068
6166
|
}
|
|
6069
6167
|
return map;
|
|
6070
6168
|
}
|
|
@@ -6084,8 +6182,8 @@ class DerivationLookup {
|
|
|
6084
6182
|
buildByArrayPathMap() {
|
|
6085
6183
|
const map = new Map();
|
|
6086
6184
|
for (const entry of this.entries) {
|
|
6087
|
-
if (isArrayPlaceholderPath(entry.
|
|
6088
|
-
const arrayPath = extractArrayPath(entry.
|
|
6185
|
+
if (isArrayPlaceholderPath(entry.fieldKey)) {
|
|
6186
|
+
const arrayPath = extractArrayPath(entry.fieldKey);
|
|
6089
6187
|
if (arrayPath) {
|
|
6090
6188
|
const arrayEntries = map.get(arrayPath) ?? [];
|
|
6091
6189
|
arrayEntries.push(entry);
|
|
@@ -6158,22 +6256,22 @@ function topologicalSort(entries) {
|
|
|
6158
6256
|
graph.set(entry, new Set());
|
|
6159
6257
|
inDegree.set(entry, 0);
|
|
6160
6258
|
}
|
|
6161
|
-
// Pre-build index:
|
|
6259
|
+
// Pre-build index: field key -> entries that produce/target it
|
|
6162
6260
|
// This enables O(1) lookup instead of O(n) scanning
|
|
6163
|
-
const
|
|
6261
|
+
const derivationsByField = new Map();
|
|
6164
6262
|
for (const entry of entries) {
|
|
6165
|
-
// Index by exact
|
|
6166
|
-
const
|
|
6167
|
-
|
|
6168
|
-
|
|
6263
|
+
// Index by exact field key
|
|
6264
|
+
const fieldEntries = derivationsByField.get(entry.fieldKey) ?? [];
|
|
6265
|
+
fieldEntries.push(entry);
|
|
6266
|
+
derivationsByField.set(entry.fieldKey, fieldEntries);
|
|
6169
6267
|
// Also index array field relative paths for array derivations
|
|
6170
6268
|
// If entry targets 'items.$.lineTotal', index under 'lineTotal' too
|
|
6171
|
-
if (entry.
|
|
6172
|
-
const relativePath = entry.
|
|
6269
|
+
if (entry.fieldKey.includes('.$.')) {
|
|
6270
|
+
const relativePath = entry.fieldKey.split('.$.')[1];
|
|
6173
6271
|
if (relativePath) {
|
|
6174
|
-
const
|
|
6175
|
-
|
|
6176
|
-
|
|
6272
|
+
const relativeEntries = derivationsByField.get(relativePath) ?? [];
|
|
6273
|
+
relativeEntries.push(entry);
|
|
6274
|
+
derivationsByField.set(relativePath, relativeEntries);
|
|
6177
6275
|
}
|
|
6178
6276
|
}
|
|
6179
6277
|
}
|
|
@@ -6185,14 +6283,15 @@ function topologicalSort(entries) {
|
|
|
6185
6283
|
}
|
|
6186
6284
|
}
|
|
6187
6285
|
// Build edges using pre-computed index
|
|
6188
|
-
// For each entry B, find all entries A whose
|
|
6286
|
+
// For each entry B, find all entries A whose field is in B's dependencies
|
|
6287
|
+
// If B depends on field X, and A produces X, then A must run before B
|
|
6189
6288
|
for (const entryB of entries) {
|
|
6190
6289
|
for (const dep of entryB.dependsOn) {
|
|
6191
6290
|
// Skip wildcards here - handled separately below
|
|
6192
6291
|
if (dep === '*')
|
|
6193
6292
|
continue;
|
|
6194
|
-
// O(1) lookup: find all
|
|
6195
|
-
const producers =
|
|
6293
|
+
// O(1) lookup: find all derivations that produce this dependency
|
|
6294
|
+
const producers = derivationsByField.get(dep);
|
|
6196
6295
|
if (producers) {
|
|
6197
6296
|
for (const entryA of producers) {
|
|
6198
6297
|
const entryAEdges = graph.get(entryA);
|
|
@@ -6329,7 +6428,7 @@ function collectFromField(field, entries, context) {
|
|
|
6329
6428
|
const validationField = field;
|
|
6330
6429
|
// Collect shorthand derivation property
|
|
6331
6430
|
if (validationField.derivation) {
|
|
6332
|
-
const entry = createShorthandEntry(fieldKey, validationField.derivation);
|
|
6431
|
+
const entry = createShorthandEntry(fieldKey, validationField.derivation, context);
|
|
6333
6432
|
entries.push(entry);
|
|
6334
6433
|
}
|
|
6335
6434
|
// Collect logic array derivations
|
|
@@ -6346,16 +6445,17 @@ function collectFromField(field, entries, context) {
|
|
|
6346
6445
|
* Creates a derivation entry from the shorthand `derivation` property.
|
|
6347
6446
|
*
|
|
6348
6447
|
* Shorthand derivations:
|
|
6349
|
-
* -
|
|
6448
|
+
* - Target the same field they're defined on (self-targeting)
|
|
6350
6449
|
* - Always apply (condition defaults to true)
|
|
6351
6450
|
* - Always trigger on change
|
|
6352
6451
|
*
|
|
6353
6452
|
* @internal
|
|
6354
6453
|
*/
|
|
6355
|
-
function createShorthandEntry(fieldKey, expression) {
|
|
6454
|
+
function createShorthandEntry(fieldKey, expression, context) {
|
|
6455
|
+
// Build the effective field key, including array path if in array context
|
|
6456
|
+
const effectiveFieldKey = context.arrayPath ? `${context.arrayPath}.$.${fieldKey}` : fieldKey;
|
|
6356
6457
|
return {
|
|
6357
|
-
|
|
6358
|
-
targetFieldKey: fieldKey,
|
|
6458
|
+
fieldKey: effectiveFieldKey,
|
|
6359
6459
|
dependsOn: extractStringDependencies(expression),
|
|
6360
6460
|
condition: true,
|
|
6361
6461
|
expression,
|
|
@@ -6366,16 +6466,21 @@ function createShorthandEntry(fieldKey, expression) {
|
|
|
6366
6466
|
/**
|
|
6367
6467
|
* Creates a derivation entry from a full `DerivationLogicConfig`.
|
|
6368
6468
|
*
|
|
6469
|
+
* All derivations are self-targeting: the derivation is defined on and targets
|
|
6470
|
+
* the same field (fieldKey). For array fields, the context is used to build
|
|
6471
|
+
* the array placeholder path.
|
|
6472
|
+
*
|
|
6369
6473
|
* Handles:
|
|
6370
6474
|
* - Condition extraction and dependency analysis
|
|
6371
|
-
* -
|
|
6475
|
+
* - Array field context for placeholder paths
|
|
6372
6476
|
* - Multiple value source types (static, expression, function)
|
|
6373
6477
|
* - Trigger and debounce configuration
|
|
6374
6478
|
*
|
|
6375
6479
|
* @internal
|
|
6376
6480
|
*/
|
|
6377
6481
|
function createLogicEntry(fieldKey, config, context) {
|
|
6378
|
-
|
|
6482
|
+
// Build the effective field key, including array path if in array context
|
|
6483
|
+
const effectiveFieldKey = context.arrayPath ? `${context.arrayPath}.$.${fieldKey}` : fieldKey;
|
|
6379
6484
|
const dependsOn = extractDependencies(config);
|
|
6380
6485
|
const condition = config.condition ?? true;
|
|
6381
6486
|
const trigger = config.trigger ?? 'onChange';
|
|
@@ -6383,8 +6488,7 @@ function createLogicEntry(fieldKey, config, context) {
|
|
|
6383
6488
|
// The type system ensures debounceMs is only present when trigger is 'debounced'
|
|
6384
6489
|
const debounceMs = trigger === 'debounced' ? config.debounceMs : undefined;
|
|
6385
6490
|
return {
|
|
6386
|
-
|
|
6387
|
-
targetFieldKey,
|
|
6491
|
+
fieldKey: effectiveFieldKey,
|
|
6388
6492
|
dependsOn,
|
|
6389
6493
|
condition,
|
|
6390
6494
|
value: config.value,
|
|
@@ -6397,46 +6501,6 @@ function createLogicEntry(fieldKey, config, context) {
|
|
|
6397
6501
|
debugName: config.debugName,
|
|
6398
6502
|
};
|
|
6399
6503
|
}
|
|
6400
|
-
/**
|
|
6401
|
-
* Resolves a target field key, handling relative paths for array fields.
|
|
6402
|
-
*
|
|
6403
|
-
* Relative paths start with '$.' and reference siblings at the same array index.
|
|
6404
|
-
*
|
|
6405
|
-
* @param targetField - The target field specification from config
|
|
6406
|
-
* @param sourceFieldKey - The source field key
|
|
6407
|
-
* @param context - Current collection context with array path info
|
|
6408
|
-
* @returns Resolved target field key
|
|
6409
|
-
*
|
|
6410
|
-
* @example
|
|
6411
|
-
* ```typescript
|
|
6412
|
-
* // Absolute path - returned as-is
|
|
6413
|
-
* resolveTargetFieldKey('phonePrefix', 'country', {})
|
|
6414
|
-
* // Returns: 'phonePrefix'
|
|
6415
|
-
*
|
|
6416
|
-
* // Relative path in array context
|
|
6417
|
-
* resolveTargetFieldKey('$.lineTotal', 'items[0].quantity', { arrayPath: 'items' })
|
|
6418
|
-
* // Returns: 'items.$.lineTotal'
|
|
6419
|
-
* ```
|
|
6420
|
-
*
|
|
6421
|
-
* @internal
|
|
6422
|
-
*/
|
|
6423
|
-
function resolveTargetFieldKey(targetField, _sourceFieldKey, context) {
|
|
6424
|
-
// Check for relative path indicator
|
|
6425
|
-
if (targetField.startsWith('$.')) {
|
|
6426
|
-
// Extract the relative field name (after '$.')
|
|
6427
|
-
const relativePath = targetField.substring(2);
|
|
6428
|
-
if (context.arrayPath) {
|
|
6429
|
-
// Replace '$' with the array path for pattern matching
|
|
6430
|
-
return `${context.arrayPath}.$.${relativePath}`;
|
|
6431
|
-
}
|
|
6432
|
-
// Not in array context - keep relative path as-is.
|
|
6433
|
-
// This indicates a configuration error (relative path used outside array).
|
|
6434
|
-
// The runtime applicator will handle this gracefully.
|
|
6435
|
-
return targetField;
|
|
6436
|
-
}
|
|
6437
|
-
// Absolute path - return as-is
|
|
6438
|
-
return targetField;
|
|
6439
|
-
}
|
|
6440
6504
|
/**
|
|
6441
6505
|
* Extracts all field dependencies from a derivation config.
|
|
6442
6506
|
*
|
|
@@ -6516,11 +6580,15 @@ function detectCycles(collection) {
|
|
|
6516
6580
|
return result;
|
|
6517
6581
|
}
|
|
6518
6582
|
/**
|
|
6519
|
-
* Detects bidirectional derivation pairs (A
|
|
6583
|
+
* Detects bidirectional derivation pairs (A↔B patterns).
|
|
6520
6584
|
*
|
|
6521
6585
|
* These patterns are allowed because they stabilize via equality checks.
|
|
6522
6586
|
* Example: USD/EUR conversion where both fields derive from each other.
|
|
6523
6587
|
*
|
|
6588
|
+
* A bidirectional pair exists when:
|
|
6589
|
+
* - Field A derives its value from field B (A depends on B)
|
|
6590
|
+
* - Field B derives its value from field A (B depends on A)
|
|
6591
|
+
*
|
|
6524
6592
|
* ## Floating-Point Precision Note
|
|
6525
6593
|
*
|
|
6526
6594
|
* Bidirectional derivations stabilize via equality checks using exact IEEE 754
|
|
@@ -6536,36 +6604,35 @@ function detectCycles(collection) {
|
|
|
6536
6604
|
*
|
|
6537
6605
|
* @example
|
|
6538
6606
|
* ```typescript
|
|
6539
|
-
* //
|
|
6540
|
-
*
|
|
6541
|
-
*
|
|
6542
|
-
* type: 'derivation',
|
|
6543
|
-
* targetField: 'eur',
|
|
6544
|
-
* expression: 'Math.round(formValue.usd * 0.92 * 100) / 100'
|
|
6545
|
-
* }
|
|
6546
|
-
* ]
|
|
6607
|
+
* // Bidirectional currency conversion
|
|
6608
|
+
* { key: 'amountUSD', derivation: 'formValue.amountEUR * 1.1' }
|
|
6609
|
+
* { key: 'amountEUR', derivation: 'formValue.amountUSD / 1.1' }
|
|
6547
6610
|
* ```
|
|
6548
6611
|
*
|
|
6549
6612
|
* @internal
|
|
6550
6613
|
*/
|
|
6551
6614
|
function detectBidirectionalPairs(collection) {
|
|
6552
6615
|
const pairs = new Set();
|
|
6553
|
-
|
|
6554
|
-
|
|
6616
|
+
// Build a map of fieldKey -> dependencies (other fields this derivation reads from)
|
|
6617
|
+
const dependencyMap = new Map();
|
|
6555
6618
|
for (const entry of collection.entries) {
|
|
6556
|
-
|
|
6557
|
-
|
|
6619
|
+
const deps = dependencyMap.get(entry.fieldKey) ?? new Set();
|
|
6620
|
+
for (const dep of entry.dependsOn) {
|
|
6621
|
+
if (dep !== '*' && dep !== entry.fieldKey) {
|
|
6622
|
+
deps.add(dep);
|
|
6623
|
+
}
|
|
6558
6624
|
}
|
|
6625
|
+
dependencyMap.set(entry.fieldKey, deps);
|
|
6559
6626
|
}
|
|
6560
|
-
// Find bidirectional pairs
|
|
6561
|
-
for (const
|
|
6562
|
-
const
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
? `${
|
|
6567
|
-
|
|
6568
|
-
|
|
6627
|
+
// Find bidirectional pairs: A depends on B AND B depends on A
|
|
6628
|
+
for (const [fieldA, depsA] of dependencyMap) {
|
|
6629
|
+
for (const fieldB of depsA) {
|
|
6630
|
+
const depsB = dependencyMap.get(fieldB);
|
|
6631
|
+
if (depsB?.has(fieldA)) {
|
|
6632
|
+
// Normalize pair key (alphabetically sorted)
|
|
6633
|
+
const pairKey = fieldA < fieldB ? `${fieldA}↔${fieldB}` : `${fieldB}↔${fieldA}`;
|
|
6634
|
+
pairs.add(pairKey);
|
|
6635
|
+
}
|
|
6569
6636
|
}
|
|
6570
6637
|
}
|
|
6571
6638
|
return pairs;
|
|
@@ -6594,8 +6661,9 @@ function isBidirectionalCycle(cyclePath, bidirectionalPairs) {
|
|
|
6594
6661
|
* Creates nodes for each field involved in derivations and
|
|
6595
6662
|
* edges representing the derivation dependencies.
|
|
6596
6663
|
*
|
|
6597
|
-
* Edge direction
|
|
6598
|
-
*
|
|
6664
|
+
* Edge direction is based on dependencies:
|
|
6665
|
+
* - If field A's derivation depends on field B, then B -> A (changing B triggers A)
|
|
6666
|
+
* - A cycle exists if: A -> B -> C -> A (circular dependency chain)
|
|
6599
6667
|
*
|
|
6600
6668
|
* @internal
|
|
6601
6669
|
*/
|
|
@@ -6615,32 +6683,19 @@ function buildDependencyGraph(collection) {
|
|
|
6615
6683
|
return node;
|
|
6616
6684
|
};
|
|
6617
6685
|
for (const entry of collection.entries) {
|
|
6618
|
-
const
|
|
6619
|
-
|
|
6620
|
-
// For cycle detection,
|
|
6621
|
-
//
|
|
6622
|
-
//
|
|
6623
|
-
//
|
|
6624
|
-
// A cycle exists if: A -> B -> A
|
|
6625
|
-
// (A triggers B, B triggers A)
|
|
6626
|
-
// For self-referential derivations (where source === target), don't add source->target edge.
|
|
6627
|
-
// This includes:
|
|
6628
|
-
// 1. Shorthand derivations (field.derivation = 'expression')
|
|
6629
|
-
// 2. Self-transform derivations (logic: [{ targetField: 'self', ... }])
|
|
6630
|
-
// These are legitimate patterns for computed fields and value transformations,
|
|
6631
|
-
// not actual cycles. The real dependencies come from the expression (tracked in dependsOn).
|
|
6632
|
-
const isSelfReferential = entry.sourceFieldKey === entry.targetFieldKey;
|
|
6633
|
-
if (!isSelfReferential) {
|
|
6634
|
-
// The target depends on the source for its value
|
|
6635
|
-
targetNode.dependsOn.add(entry.sourceFieldKey);
|
|
6636
|
-
sourceNode.dependedOnBy.add(entry.targetFieldKey);
|
|
6637
|
-
}
|
|
6638
|
-
// Also track dependencies from the derivation expression/condition
|
|
6686
|
+
const fieldNode = ensureNode(entry.fieldKey);
|
|
6687
|
+
// Track dependencies from the derivation expression/condition.
|
|
6688
|
+
// For cycle detection, edges go from dependency -> field:
|
|
6689
|
+
// If field A depends on field B, then when B changes, A gets updated.
|
|
6690
|
+
// A cycle exists when there's a circular chain of such dependencies.
|
|
6639
6691
|
for (const dep of entry.dependsOn) {
|
|
6640
|
-
|
|
6692
|
+
// Skip wildcard dependencies and self-references
|
|
6693
|
+
if (dep !== '*' && dep !== entry.fieldKey) {
|
|
6641
6694
|
const depNode = ensureNode(dep);
|
|
6642
|
-
|
|
6643
|
-
|
|
6695
|
+
// The field depends on 'dep'
|
|
6696
|
+
fieldNode.dependsOn.add(dep);
|
|
6697
|
+
// 'dep' is depended on by the field
|
|
6698
|
+
depNode.dependedOnBy.add(entry.fieldKey);
|
|
6644
6699
|
}
|
|
6645
6700
|
}
|
|
6646
6701
|
}
|
|
@@ -6788,6 +6843,61 @@ function validateNoCycles(collection, logger) {
|
|
|
6788
6843
|
}
|
|
6789
6844
|
}
|
|
6790
6845
|
|
|
6846
|
+
/**
|
|
6847
|
+
* Centralized constants for the derivation system.
|
|
6848
|
+
*
|
|
6849
|
+
* These constants configure the behavior of derivation processing,
|
|
6850
|
+
* caching, and path handling. Centralizing them enables:
|
|
6851
|
+
* - Easy configuration changes
|
|
6852
|
+
* - Consistent values across all derivation modules
|
|
6853
|
+
* - Clear documentation of magic numbers
|
|
6854
|
+
*
|
|
6855
|
+
* @module
|
|
6856
|
+
*/
|
|
6857
|
+
/**
|
|
6858
|
+
* Maximum number of derivation iterations before stopping to prevent infinite loops.
|
|
6859
|
+
*
|
|
6860
|
+
* The value of 10 is chosen based on:
|
|
6861
|
+
* - Most derivation chains are 2-3 levels deep (A→B→C)
|
|
6862
|
+
* - Complex forms with conditional cascades rarely exceed 5-6 levels
|
|
6863
|
+
* - 10 provides headroom for bidirectional sync patterns (A↔B) which may need
|
|
6864
|
+
* 2 iterations per pair to stabilize
|
|
6865
|
+
* - Higher values delay detection of actual infinite loops
|
|
6866
|
+
* - Lower values risk false positives on legitimate deep chains
|
|
6867
|
+
*
|
|
6868
|
+
* If you hit this limit legitimately, consider:
|
|
6869
|
+
* 1. Restructuring derivations to reduce chain depth
|
|
6870
|
+
* 2. Using explicit `dependsOn` to control evaluation order
|
|
6871
|
+
* 3. Breaking complex derivations into computed signals outside the form
|
|
6872
|
+
*/
|
|
6873
|
+
const MAX_DERIVATION_ITERATIONS = 10;
|
|
6874
|
+
/**
|
|
6875
|
+
* Placeholder string for array index in derivation paths.
|
|
6876
|
+
*
|
|
6877
|
+
* Used in paths like `items.$.quantity` to represent "each item in the array".
|
|
6878
|
+
* The `$` is resolved to actual indices at runtime during array derivation processing.
|
|
6879
|
+
*/
|
|
6880
|
+
const ARRAY_PLACEHOLDER = '.$.';
|
|
6881
|
+
/**
|
|
6882
|
+
* Maximum size of the expression AST cache.
|
|
6883
|
+
*
|
|
6884
|
+
* Expressions are parsed into Abstract Syntax Trees (AST) once and cached
|
|
6885
|
+
* to avoid re-parsing on each evaluation. The cache uses LRU eviction.
|
|
6886
|
+
*
|
|
6887
|
+
* 1000 entries handles:
|
|
6888
|
+
* - Large forms with many derivation expressions
|
|
6889
|
+
* - Multiple form instances with different expressions
|
|
6890
|
+
* - Memory efficiency (AST nodes are relatively small)
|
|
6891
|
+
*/
|
|
6892
|
+
const MAX_AST_CACHE_SIZE = 1000;
|
|
6893
|
+
/**
|
|
6894
|
+
* Delimiter used to create unique derivation keys from source and target field keys.
|
|
6895
|
+
*
|
|
6896
|
+
* Uses null character (\x00) which is extremely unlikely to appear in field names,
|
|
6897
|
+
* avoiding collision issues that could occur with common delimiters like ':'.
|
|
6898
|
+
*/
|
|
6899
|
+
const DERIVATION_KEY_DELIMITER = '\x00';
|
|
6900
|
+
|
|
6791
6901
|
/**
|
|
6792
6902
|
* Applies all pending derivations from a collection.
|
|
6793
6903
|
*
|
|
@@ -6904,9 +7014,9 @@ function getEntriesForChangedFields(entries, changedFields) {
|
|
|
6904
7014
|
* ```typescript
|
|
6905
7015
|
* // Two derivations for conditional value with cleanup:
|
|
6906
7016
|
* // When country is USA, set phonePrefix to '+1'
|
|
6907
|
-
* { condition: { field: 'country', operator: '==', value: 'USA' },
|
|
7017
|
+
* { key: 'phonePrefix', logic: [{ type: 'derivation', condition: { field: 'country', operator: '==', value: 'USA' }, value: '+1' }] }
|
|
6908
7018
|
* // When country is NOT USA, clear phonePrefix
|
|
6909
|
-
* { condition: { field: 'country', operator: '!=', value: 'USA' },
|
|
7019
|
+
* { key: 'phonePrefix', logic: [{ type: 'derivation', condition: { field: 'country', operator: '!=', value: 'USA' }, value: '' }] }
|
|
6910
7020
|
* ```
|
|
6911
7021
|
*
|
|
6912
7022
|
* @internal
|
|
@@ -6914,20 +7024,19 @@ function getEntriesForChangedFields(entries, changedFields) {
|
|
|
6914
7024
|
function tryApplyDerivation(entry, context, chainContext) {
|
|
6915
7025
|
const { derivationLogger } = context;
|
|
6916
7026
|
// Check if this is an array field derivation (has '$' placeholder)
|
|
6917
|
-
if (isArrayPlaceholderPath(entry.
|
|
7027
|
+
if (isArrayPlaceholderPath(entry.fieldKey)) {
|
|
6918
7028
|
return tryApplyArrayDerivation(entry, context, chainContext);
|
|
6919
7029
|
}
|
|
6920
|
-
const derivationKey = createDerivationKey(entry.
|
|
7030
|
+
const derivationKey = createDerivationKey(entry.fieldKey);
|
|
6921
7031
|
// Check if already applied in this cycle
|
|
6922
7032
|
if (chainContext.appliedDerivations.has(derivationKey)) {
|
|
6923
7033
|
derivationLogger.evaluation({
|
|
6924
7034
|
debugName: entry.debugName,
|
|
6925
|
-
|
|
6926
|
-
targetFieldKey: entry.targetFieldKey,
|
|
7035
|
+
fieldKey: entry.fieldKey,
|
|
6927
7036
|
result: 'skipped',
|
|
6928
7037
|
skipReason: 'already-applied',
|
|
6929
7038
|
});
|
|
6930
|
-
return { applied: false,
|
|
7039
|
+
return { applied: false, fieldKey: entry.fieldKey };
|
|
6931
7040
|
}
|
|
6932
7041
|
// Create evaluation context
|
|
6933
7042
|
const formValue = untracked(() => context.formValue());
|
|
@@ -6936,12 +7045,11 @@ function tryApplyDerivation(entry, context, chainContext) {
|
|
|
6936
7045
|
if (!evaluateDerivationCondition(entry.condition, evalContext)) {
|
|
6937
7046
|
derivationLogger.evaluation({
|
|
6938
7047
|
debugName: entry.debugName,
|
|
6939
|
-
|
|
6940
|
-
targetFieldKey: entry.targetFieldKey,
|
|
7048
|
+
fieldKey: entry.fieldKey,
|
|
6941
7049
|
result: 'skipped',
|
|
6942
7050
|
skipReason: 'condition-false',
|
|
6943
7051
|
});
|
|
6944
|
-
return { applied: false,
|
|
7052
|
+
return { applied: false, fieldKey: entry.fieldKey };
|
|
6945
7053
|
}
|
|
6946
7054
|
// Compute derived value
|
|
6947
7055
|
let newValue;
|
|
@@ -6953,14 +7061,13 @@ function tryApplyDerivation(entry, context, chainContext) {
|
|
|
6953
7061
|
context.logger.error(formatDerivationError(entry, 'compute', errorMessage));
|
|
6954
7062
|
derivationLogger.evaluation({
|
|
6955
7063
|
debugName: entry.debugName,
|
|
6956
|
-
|
|
6957
|
-
targetFieldKey: entry.targetFieldKey,
|
|
7064
|
+
fieldKey: entry.fieldKey,
|
|
6958
7065
|
result: 'error',
|
|
6959
7066
|
error: errorMessage,
|
|
6960
7067
|
});
|
|
6961
7068
|
return {
|
|
6962
7069
|
applied: false,
|
|
6963
|
-
|
|
7070
|
+
fieldKey: entry.fieldKey,
|
|
6964
7071
|
error: errorMessage,
|
|
6965
7072
|
};
|
|
6966
7073
|
}
|
|
@@ -6969,46 +7076,43 @@ function tryApplyDerivation(entry, context, chainContext) {
|
|
|
6969
7076
|
// equality for numbers. Bidirectional derivations with floating-point math
|
|
6970
7077
|
// may oscillate due to rounding errors. Use explicit rounding in expressions
|
|
6971
7078
|
// or integer arithmetic to avoid this issue.
|
|
6972
|
-
const currentValue = getNestedValue(formValue, entry.
|
|
7079
|
+
const currentValue = getNestedValue(formValue, entry.fieldKey);
|
|
6973
7080
|
if (isEqual(currentValue, newValue)) {
|
|
6974
7081
|
derivationLogger.evaluation({
|
|
6975
7082
|
debugName: entry.debugName,
|
|
6976
|
-
|
|
6977
|
-
targetFieldKey: entry.targetFieldKey,
|
|
7083
|
+
fieldKey: entry.fieldKey,
|
|
6978
7084
|
result: 'skipped',
|
|
6979
7085
|
skipReason: 'value-unchanged',
|
|
6980
7086
|
});
|
|
6981
|
-
return { applied: false,
|
|
7087
|
+
return { applied: false, fieldKey: entry.fieldKey };
|
|
6982
7088
|
}
|
|
6983
7089
|
// Apply the value
|
|
6984
7090
|
try {
|
|
6985
|
-
const wasApplied = applyValueToForm(entry.
|
|
7091
|
+
const wasApplied = applyValueToForm(entry.fieldKey, newValue, context.rootForm, context.logger, context.warningTracker);
|
|
6986
7092
|
if (wasApplied) {
|
|
6987
7093
|
chainContext.appliedDerivations.add(derivationKey);
|
|
6988
7094
|
derivationLogger.evaluation({
|
|
6989
7095
|
debugName: entry.debugName,
|
|
6990
|
-
|
|
6991
|
-
targetFieldKey: entry.targetFieldKey,
|
|
7096
|
+
fieldKey: entry.fieldKey,
|
|
6992
7097
|
result: 'applied',
|
|
6993
7098
|
previousValue: currentValue,
|
|
6994
7099
|
newValue,
|
|
6995
7100
|
});
|
|
6996
|
-
return { applied: true,
|
|
7101
|
+
return { applied: true, fieldKey: entry.fieldKey, newValue };
|
|
6997
7102
|
}
|
|
6998
7103
|
else {
|
|
6999
7104
|
// Field not found - this is a warning, not an error
|
|
7000
7105
|
// The warning was already logged by warnMissingField
|
|
7001
7106
|
derivationLogger.evaluation({
|
|
7002
7107
|
debugName: entry.debugName,
|
|
7003
|
-
|
|
7004
|
-
targetFieldKey: entry.targetFieldKey,
|
|
7108
|
+
fieldKey: entry.fieldKey,
|
|
7005
7109
|
result: 'skipped',
|
|
7006
7110
|
skipReason: 'target-not-found',
|
|
7007
7111
|
});
|
|
7008
7112
|
return {
|
|
7009
7113
|
applied: false,
|
|
7010
|
-
|
|
7011
|
-
warning: `
|
|
7114
|
+
fieldKey: entry.fieldKey,
|
|
7115
|
+
warning: `Field '${entry.fieldKey}' not found`,
|
|
7012
7116
|
};
|
|
7013
7117
|
}
|
|
7014
7118
|
}
|
|
@@ -7017,14 +7121,13 @@ function tryApplyDerivation(entry, context, chainContext) {
|
|
|
7017
7121
|
context.logger.error(formatDerivationError(entry, 'apply', errorMessage));
|
|
7018
7122
|
derivationLogger.evaluation({
|
|
7019
7123
|
debugName: entry.debugName,
|
|
7020
|
-
|
|
7021
|
-
targetFieldKey: entry.targetFieldKey,
|
|
7124
|
+
fieldKey: entry.fieldKey,
|
|
7022
7125
|
result: 'error',
|
|
7023
7126
|
error: errorMessage,
|
|
7024
7127
|
});
|
|
7025
7128
|
return {
|
|
7026
7129
|
applied: false,
|
|
7027
|
-
|
|
7130
|
+
fieldKey: entry.fieldKey,
|
|
7028
7131
|
error: errorMessage,
|
|
7029
7132
|
};
|
|
7030
7133
|
}
|
|
@@ -7039,22 +7142,22 @@ function tryApplyDerivation(entry, context, chainContext) {
|
|
|
7039
7142
|
*/
|
|
7040
7143
|
function tryApplyArrayDerivation(entry, context, chainContext) {
|
|
7041
7144
|
const formValue = untracked(() => context.formValue());
|
|
7042
|
-
// Parse the
|
|
7043
|
-
const pathInfo = parseArrayPath(entry.
|
|
7145
|
+
// Parse the field path using path utilities
|
|
7146
|
+
const pathInfo = parseArrayPath(entry.fieldKey);
|
|
7044
7147
|
if (!pathInfo.isArrayPath) {
|
|
7045
|
-
return { applied: false,
|
|
7148
|
+
return { applied: false, fieldKey: entry.fieldKey, error: 'Invalid array derivation path' };
|
|
7046
7149
|
}
|
|
7047
7150
|
const { arrayPath } = pathInfo;
|
|
7048
7151
|
// Get the array from form values
|
|
7049
7152
|
const arrayValue = getNestedValue(formValue, arrayPath);
|
|
7050
7153
|
if (!Array.isArray(arrayValue)) {
|
|
7051
|
-
return { applied: false,
|
|
7154
|
+
return { applied: false, fieldKey: entry.fieldKey };
|
|
7052
7155
|
}
|
|
7053
7156
|
let appliedAny = false;
|
|
7054
7157
|
// Process each array item
|
|
7055
7158
|
for (let i = 0; i < arrayValue.length; i++) {
|
|
7056
|
-
const
|
|
7057
|
-
const derivationKey = createDerivationKey(
|
|
7159
|
+
const resolvedPath = resolveArrayPath(entry.fieldKey, i);
|
|
7160
|
+
const derivationKey = createDerivationKey(resolvedPath);
|
|
7058
7161
|
// Skip if already applied in this cycle
|
|
7059
7162
|
if (chainContext.appliedDerivations.has(derivationKey)) {
|
|
7060
7163
|
continue;
|
|
@@ -7077,13 +7180,13 @@ function tryApplyArrayDerivation(entry, context, chainContext) {
|
|
|
7077
7180
|
continue;
|
|
7078
7181
|
}
|
|
7079
7182
|
// Check if value actually changed
|
|
7080
|
-
const currentValue = getNestedValue(formValue,
|
|
7183
|
+
const currentValue = getNestedValue(formValue, resolvedPath);
|
|
7081
7184
|
if (isEqual(currentValue, newValue)) {
|
|
7082
7185
|
continue;
|
|
7083
7186
|
}
|
|
7084
7187
|
// Apply the value
|
|
7085
7188
|
try {
|
|
7086
|
-
applyValueToForm(
|
|
7189
|
+
applyValueToForm(resolvedPath, newValue, context.rootForm, context.logger, context.warningTracker);
|
|
7087
7190
|
chainContext.appliedDerivations.add(derivationKey);
|
|
7088
7191
|
appliedAny = true;
|
|
7089
7192
|
}
|
|
@@ -7092,7 +7195,7 @@ function tryApplyArrayDerivation(entry, context, chainContext) {
|
|
|
7092
7195
|
context.logger.error(formatDerivationError(entry, 'apply', `[index ${i}] ${errorMessage}`));
|
|
7093
7196
|
}
|
|
7094
7197
|
}
|
|
7095
|
-
return { applied: appliedAny,
|
|
7198
|
+
return { applied: appliedAny, fieldKey: entry.fieldKey };
|
|
7096
7199
|
}
|
|
7097
7200
|
/**
|
|
7098
7201
|
* Creates an evaluation context for derivation processing.
|
|
@@ -7100,12 +7203,13 @@ function tryApplyArrayDerivation(entry, context, chainContext) {
|
|
|
7100
7203
|
* @internal
|
|
7101
7204
|
*/
|
|
7102
7205
|
function createEvaluationContext(entry, formValue, context) {
|
|
7103
|
-
const fieldValue = getNestedValue(formValue, entry.
|
|
7206
|
+
const fieldValue = getNestedValue(formValue, entry.fieldKey);
|
|
7104
7207
|
return {
|
|
7105
7208
|
fieldValue,
|
|
7106
7209
|
formValue,
|
|
7107
|
-
fieldPath: entry.
|
|
7210
|
+
fieldPath: entry.fieldKey,
|
|
7108
7211
|
customFunctions: context.customFunctions,
|
|
7212
|
+
externalData: context.externalData,
|
|
7109
7213
|
logger: context.logger,
|
|
7110
7214
|
};
|
|
7111
7215
|
}
|
|
@@ -7126,6 +7230,7 @@ function createArrayItemEvaluationContext(entry, arrayItem, rootFormValue, itemI
|
|
|
7126
7230
|
formValue: arrayItem,
|
|
7127
7231
|
fieldPath: `${arrayPath}.${itemIndex}`,
|
|
7128
7232
|
customFunctions: context.customFunctions,
|
|
7233
|
+
externalData: context.externalData,
|
|
7129
7234
|
logger: context.logger,
|
|
7130
7235
|
// Provide access to root form value for cross-scope references
|
|
7131
7236
|
rootFormValue,
|
|
@@ -7167,7 +7272,7 @@ function computeDerivedValue(entry, evalContext, applicatorContext) {
|
|
|
7167
7272
|
return fn(evalContext);
|
|
7168
7273
|
}
|
|
7169
7274
|
// No value source specified
|
|
7170
|
-
throw new Error(`Derivation for ${entry.
|
|
7275
|
+
throw new Error(`Derivation for ${entry.fieldKey} has no value source. ` + `Specify 'value', 'expression', or 'functionName'.`);
|
|
7171
7276
|
}
|
|
7172
7277
|
/**
|
|
7173
7278
|
* Applies a value to the form at the specified path.
|
|
@@ -7225,7 +7330,7 @@ function formatDerivationError(entry, phase, message) {
|
|
|
7225
7330
|
: entry.value !== undefined
|
|
7226
7331
|
? 'static value'
|
|
7227
7332
|
: 'unknown source';
|
|
7228
|
-
return `${ERROR_PREFIX} Failed to ${phase} derivation (
|
|
7333
|
+
return `${ERROR_PREFIX} Failed to ${phase} derivation (field: ${entry.fieldKey}, ${source}): ${message}`;
|
|
7229
7334
|
}
|
|
7230
7335
|
/**
|
|
7231
7336
|
* Sets a field value using the Angular Signal Forms pattern.
|
|
@@ -7413,12 +7518,11 @@ function logMaxIterationsReached(result, trigger, logger) {
|
|
|
7413
7518
|
function logDerivationEvaluation(entry, logger, config) {
|
|
7414
7519
|
if (!shouldLog(config, 'verbose'))
|
|
7415
7520
|
return;
|
|
7416
|
-
const name = entry.debugName ? `"${entry.debugName}"` :
|
|
7521
|
+
const name = entry.debugName ? `"${entry.debugName}"` : entry.fieldKey;
|
|
7417
7522
|
switch (entry.result) {
|
|
7418
7523
|
case 'applied':
|
|
7419
7524
|
logger.debug(`${LOG_PREFIX} -> Applied: ${name}`, {
|
|
7420
|
-
|
|
7421
|
-
target: entry.targetFieldKey,
|
|
7525
|
+
field: entry.fieldKey,
|
|
7422
7526
|
previousValue: entry.previousValue,
|
|
7423
7527
|
newValue: entry.newValue,
|
|
7424
7528
|
...(entry.durationMs !== undefined && { durationMs: entry.durationMs }),
|
|
@@ -7426,15 +7530,13 @@ function logDerivationEvaluation(entry, logger, config) {
|
|
|
7426
7530
|
break;
|
|
7427
7531
|
case 'skipped':
|
|
7428
7532
|
logger.debug(`${LOG_PREFIX} -> Skipped: ${name} (${formatSkipReason(entry.skipReason)})`, {
|
|
7429
|
-
|
|
7430
|
-
target: entry.targetFieldKey,
|
|
7533
|
+
field: entry.fieldKey,
|
|
7431
7534
|
reason: entry.skipReason,
|
|
7432
7535
|
});
|
|
7433
7536
|
break;
|
|
7434
7537
|
case 'error':
|
|
7435
7538
|
logger.debug(`${LOG_PREFIX} -> Error: ${name}`, {
|
|
7436
|
-
|
|
7437
|
-
target: entry.targetFieldKey,
|
|
7539
|
+
field: entry.fieldKey,
|
|
7438
7540
|
error: entry.error,
|
|
7439
7541
|
});
|
|
7440
7542
|
break;
|
|
@@ -7518,12 +7620,11 @@ class ActiveDerivationLogger {
|
|
|
7518
7620
|
evaluation(entry) {
|
|
7519
7621
|
if (!shouldLog(this.config, 'verbose'))
|
|
7520
7622
|
return;
|
|
7521
|
-
const name = entry.debugName ? `"${entry.debugName}"` :
|
|
7623
|
+
const name = entry.debugName ? `"${entry.debugName}"` : entry.fieldKey;
|
|
7522
7624
|
switch (entry.result) {
|
|
7523
7625
|
case 'applied':
|
|
7524
7626
|
this.logger.debug(`Derivation - Applied ${name}`, {
|
|
7525
|
-
|
|
7526
|
-
target: entry.targetFieldKey,
|
|
7627
|
+
field: entry.fieldKey,
|
|
7527
7628
|
previousValue: entry.previousValue,
|
|
7528
7629
|
newValue: entry.newValue,
|
|
7529
7630
|
...(entry.durationMs !== undefined && { durationMs: entry.durationMs }),
|
|
@@ -7531,15 +7632,13 @@ class ActiveDerivationLogger {
|
|
|
7531
7632
|
break;
|
|
7532
7633
|
case 'skipped':
|
|
7533
7634
|
this.logger.debug(`Derivation - Skipped ${name} (${this.formatSkipReason(entry.skipReason)})`, {
|
|
7534
|
-
|
|
7535
|
-
target: entry.targetFieldKey,
|
|
7635
|
+
field: entry.fieldKey,
|
|
7536
7636
|
reason: entry.skipReason,
|
|
7537
7637
|
});
|
|
7538
7638
|
break;
|
|
7539
7639
|
case 'error':
|
|
7540
7640
|
this.logger.debug(`Derivation - Error ${name}`, {
|
|
7541
|
-
|
|
7542
|
-
target: entry.targetFieldKey,
|
|
7641
|
+
field: entry.fieldKey,
|
|
7543
7642
|
error: entry.error,
|
|
7544
7643
|
});
|
|
7545
7644
|
break;
|
|
@@ -7652,6 +7751,7 @@ class DerivationOrchestrator {
|
|
|
7652
7751
|
warningTracker = inject(DERIVATION_WARNING_TRACKER);
|
|
7653
7752
|
functionRegistry = inject(FunctionRegistryService);
|
|
7654
7753
|
formOptions = inject(FORM_OPTIONS);
|
|
7754
|
+
externalDataSignal = inject(EXTERNAL_DATA, { optional: true });
|
|
7655
7755
|
/**
|
|
7656
7756
|
* Computed signal containing the collected and validated derivations.
|
|
7657
7757
|
* Returns null if no derivations are defined.
|
|
@@ -7696,7 +7796,9 @@ class DerivationOrchestrator {
|
|
|
7696
7796
|
// Without this, exhaustMap would block indefinitely.
|
|
7697
7797
|
return scheduled([null], queueScheduler);
|
|
7698
7798
|
}), takeUntilDestroyed(this.destroyRef))
|
|
7699
|
-
.subscribe(
|
|
7799
|
+
.subscribe({
|
|
7800
|
+
error: (err) => this.logger.error('Derivation onChange stream error', err),
|
|
7801
|
+
});
|
|
7700
7802
|
}
|
|
7701
7803
|
setupDebouncedStream() {
|
|
7702
7804
|
toObservable(this.config.formValue, { injector: this.injector })
|
|
@@ -7727,7 +7829,9 @@ class DerivationOrchestrator {
|
|
|
7727
7829
|
const periodStreams = debouncePeriods.map((debounceMs) => this.createPeriodStream(debounceMs, collection, formAccessor, changedFields));
|
|
7728
7830
|
return merge(...periodStreams);
|
|
7729
7831
|
}), takeUntilDestroyed(this.destroyRef))
|
|
7730
|
-
.subscribe(
|
|
7832
|
+
.subscribe({
|
|
7833
|
+
error: (err) => this.logger.error('Derivation debounced stream error', err),
|
|
7834
|
+
});
|
|
7731
7835
|
}
|
|
7732
7836
|
createPeriodStream(debounceMs, collection, formAccessor, changedFields) {
|
|
7733
7837
|
const additionalWait = Math.max(0, debounceMs - DEFAULT_DEBOUNCE_MS);
|
|
@@ -7750,6 +7854,7 @@ class DerivationOrchestrator {
|
|
|
7750
7854
|
rootForm: formAccessor,
|
|
7751
7855
|
derivationFunctions: this.functionRegistry.getDerivationFunctions(),
|
|
7752
7856
|
customFunctions: this.functionRegistry.getCustomFunctions(),
|
|
7857
|
+
externalData: this.resolveExternalData(),
|
|
7753
7858
|
logger: this.logger,
|
|
7754
7859
|
warningTracker: this.warningTracker,
|
|
7755
7860
|
derivationLogger: untracked(() => this.config.derivationLogger()),
|
|
@@ -7777,6 +7882,7 @@ class DerivationOrchestrator {
|
|
|
7777
7882
|
rootForm: formAccessor,
|
|
7778
7883
|
derivationFunctions: this.functionRegistry.getDerivationFunctions(),
|
|
7779
7884
|
customFunctions: this.functionRegistry.getCustomFunctions(),
|
|
7885
|
+
externalData: this.resolveExternalData(),
|
|
7780
7886
|
logger: this.logger,
|
|
7781
7887
|
warningTracker: this.warningTracker,
|
|
7782
7888
|
derivationLogger: untracked(() => this.config.derivationLogger()),
|
|
@@ -7810,12 +7916,36 @@ class DerivationOrchestrator {
|
|
|
7810
7916
|
// Find implicit wildcards (custom functions without explicit dependsOn)
|
|
7811
7917
|
const implicitWildcards = wildcardEntries.filter((entry) => entry.functionName && (!entry.originalConfig?.dependsOn || entry.originalConfig.dependsOn.length === 0));
|
|
7812
7918
|
if (implicitWildcards.length > 0) {
|
|
7813
|
-
const derivationDescs = implicitWildcards.map((e) => `${e.
|
|
7919
|
+
const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey} (${e.functionName})`);
|
|
7814
7920
|
this.logger.warn('[Derivation] Derivations using custom functions without explicit dependsOn detected. ' +
|
|
7815
7921
|
`These run on EVERY form change, which may impact performance (form has ${fieldCount} fields). ` +
|
|
7816
7922
|
'Consider specifying explicit dependsOn arrays for better performance.', derivationDescs);
|
|
7817
7923
|
}
|
|
7818
7924
|
}
|
|
7925
|
+
/**
|
|
7926
|
+
* Resolves external data signals to their current values without creating dependencies.
|
|
7927
|
+
*
|
|
7928
|
+
* Uses `untracked()` to read signals without establishing reactive dependencies,
|
|
7929
|
+
* which is important for derivations that shouldn't re-trigger on every external data change.
|
|
7930
|
+
*
|
|
7931
|
+
* @returns Record of resolved external data values, or undefined if no external data.
|
|
7932
|
+
*/
|
|
7933
|
+
resolveExternalData() {
|
|
7934
|
+
const externalDataRecord = untracked(() => this.externalDataSignal?.());
|
|
7935
|
+
if (!externalDataRecord) {
|
|
7936
|
+
return undefined;
|
|
7937
|
+
}
|
|
7938
|
+
const resolved = {};
|
|
7939
|
+
for (const [key, signal] of Object.entries(externalDataRecord)) {
|
|
7940
|
+
if (isSignal(signal)) {
|
|
7941
|
+
resolved[key] = untracked(() => signal());
|
|
7942
|
+
}
|
|
7943
|
+
else {
|
|
7944
|
+
resolved[key] = signal;
|
|
7945
|
+
}
|
|
7946
|
+
}
|
|
7947
|
+
return resolved;
|
|
7948
|
+
}
|
|
7819
7949
|
}
|
|
7820
7950
|
/**
|
|
7821
7951
|
* Creates a DerivationOrchestrator for a dynamic form.
|
|
@@ -8336,9 +8466,9 @@ class PageOrchestratorComponent {
|
|
|
8336
8466
|
}
|
|
8337
8467
|
}
|
|
8338
8468
|
}
|
|
8339
|
-
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-
|
|
8469
|
+
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs').then(m => m.default)]] });
|
|
8340
8470
|
}
|
|
8341
|
-
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.1.0", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-
|
|
8471
|
+
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.1.0", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
|
|
8342
8472
|
type: Component,
|
|
8343
8473
|
args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
|
|
8344
8474
|
@for (pageField of pageFields(); track pageField.key; let i = $index) {
|
|
@@ -8768,10 +8898,21 @@ class DynamicForm {
|
|
|
8768
8898
|
/** Emits when form dirty state changes. */
|
|
8769
8899
|
dirtyChange = outputFromObservable(toObservable(this.dirty));
|
|
8770
8900
|
/**
|
|
8771
|
-
* Emits form values when submitted (via SubmitEvent).
|
|
8901
|
+
* Emits form values when submitted (via SubmitEvent) and form is valid.
|
|
8902
|
+
*
|
|
8903
|
+
* **Important:** This output only emits when the form is valid. If you need to
|
|
8904
|
+
* handle submit events regardless of validity, use the `(events)` output and
|
|
8905
|
+
* filter for `'submit'` events.
|
|
8906
|
+
*
|
|
8772
8907
|
* Note: Does not emit when `submission.action` is configured - use one or the other.
|
|
8773
8908
|
*/
|
|
8774
|
-
submitted = outputFromObservable(this.eventBus.on('submit').pipe(
|
|
8909
|
+
submitted = outputFromObservable(this.eventBus.on('submit').pipe(filter(() => {
|
|
8910
|
+
if (!this.valid()) {
|
|
8911
|
+
this.logger.debug('Form submitted while invalid, not emitting to (submitted) output');
|
|
8912
|
+
return false;
|
|
8913
|
+
}
|
|
8914
|
+
return true;
|
|
8915
|
+
}), switchMap(() => {
|
|
8775
8916
|
const submissionConfig = this.config().submission;
|
|
8776
8917
|
if (submissionConfig?.action) {
|
|
8777
8918
|
this.logger.warn('Both `submission.action` and `(submitted)` output are configured. ' +
|
|
@@ -8779,7 +8920,8 @@ class DynamicForm {
|
|
|
8779
8920
|
'Use either `submission.action` OR `(submitted)`, not both.');
|
|
8780
8921
|
return EMPTY;
|
|
8781
8922
|
}
|
|
8782
|
-
|
|
8923
|
+
// Form is valid here, cast to TModel (no longer Partial)
|
|
8924
|
+
return of(this.formValue());
|
|
8783
8925
|
})));
|
|
8784
8926
|
/** Emits when form is reset to default values. */
|
|
8785
8927
|
reset = outputFromObservable(this.eventBus.on('form-reset'));
|
|
@@ -8896,7 +9038,9 @@ class DynamicForm {
|
|
|
8896
9038
|
formSignal: this.form,
|
|
8897
9039
|
})
|
|
8898
9040
|
.pipe(takeUntilDestroyed())
|
|
8899
|
-
.subscribe(
|
|
9041
|
+
.subscribe({
|
|
9042
|
+
error: (err) => this.logger.error('Submission handler error', err),
|
|
9043
|
+
});
|
|
8900
9044
|
}
|
|
8901
9045
|
registerValidatorsFromConfig({ customFnConfig, schemas }) {
|
|
8902
9046
|
if (schemas) {
|
|
@@ -8987,6 +9131,11 @@ class DynamicForm {
|
|
|
8987
9131
|
useFactory: (form) => form.effectiveFormOptions,
|
|
8988
9132
|
deps: [DynamicForm],
|
|
8989
9133
|
},
|
|
9134
|
+
{
|
|
9135
|
+
provide: EXTERNAL_DATA,
|
|
9136
|
+
useFactory: (form) => computed(() => form.config().externalData),
|
|
9137
|
+
deps: [DynamicForm],
|
|
9138
|
+
},
|
|
8990
9139
|
{ provide: DERIVATION_WARNING_TRACKER, useFactory: createDerivationWarningTracker },
|
|
8991
9140
|
{
|
|
8992
9141
|
provide: DERIVATION_ORCHESTRATOR,
|
|
@@ -9035,6 +9184,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
9035
9184
|
useFactory: (form) => form.effectiveFormOptions,
|
|
9036
9185
|
deps: [DynamicForm],
|
|
9037
9186
|
},
|
|
9187
|
+
{
|
|
9188
|
+
provide: EXTERNAL_DATA,
|
|
9189
|
+
useFactory: (form) => computed(() => form.config().externalData),
|
|
9190
|
+
deps: [DynamicForm],
|
|
9191
|
+
},
|
|
9038
9192
|
{ provide: DERIVATION_WARNING_TRACKER, useFactory: createDerivationWarningTracker },
|
|
9039
9193
|
{
|
|
9040
9194
|
provide: DERIVATION_ORCHESTRATOR,
|
|
@@ -9225,13 +9379,13 @@ const BUILT_IN_FIELDS = [
|
|
|
9225
9379
|
},
|
|
9226
9380
|
{
|
|
9227
9381
|
name: 'page',
|
|
9228
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-
|
|
9382
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-RN1hmG69.mjs'),
|
|
9229
9383
|
mapper: pageFieldMapper,
|
|
9230
9384
|
valueHandling: 'flatten',
|
|
9231
9385
|
},
|
|
9232
9386
|
{
|
|
9233
9387
|
name: 'text',
|
|
9234
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-
|
|
9388
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-DIVyioMX.mjs'),
|
|
9235
9389
|
mapper: textFieldMapper,
|
|
9236
9390
|
valueHandling: 'exclude',
|
|
9237
9391
|
},
|
|
@@ -9335,6 +9489,79 @@ function provideDynamicForm(...items) {
|
|
|
9335
9489
|
]);
|
|
9336
9490
|
}
|
|
9337
9491
|
|
|
9492
|
+
/**
|
|
9493
|
+
* Enables automatic form value attachment to all events dispatched through the EventBus.
|
|
9494
|
+
*
|
|
9495
|
+
* When this feature is enabled, events dispatched via `eventBus.dispatch()` will include
|
|
9496
|
+
* the current form value in the `formValue` property. This is useful for event handlers
|
|
9497
|
+
* that need access to the complete form state at the time of the event.
|
|
9498
|
+
*
|
|
9499
|
+
* @example Global opt-in
|
|
9500
|
+
* ```typescript
|
|
9501
|
+
* provideDynamicForm(
|
|
9502
|
+
* ...withMaterialFields(),
|
|
9503
|
+
* withEventFormValue()
|
|
9504
|
+
* )
|
|
9505
|
+
* ```
|
|
9506
|
+
*
|
|
9507
|
+
* @example Consumer usage
|
|
9508
|
+
* ```typescript
|
|
9509
|
+
* eventBus.on<SubmitEvent>('submit').subscribe(event => {
|
|
9510
|
+
* if (hasFormValue(event)) {
|
|
9511
|
+
* console.log('Form value:', event.formValue);
|
|
9512
|
+
* }
|
|
9513
|
+
* });
|
|
9514
|
+
* ```
|
|
9515
|
+
*
|
|
9516
|
+
* @example Per-form disable (when globally enabled)
|
|
9517
|
+
* ```typescript
|
|
9518
|
+
* const config: FormConfig = {
|
|
9519
|
+
* fields: [...],
|
|
9520
|
+
* options: { emitFormValueOnEvents: false }
|
|
9521
|
+
* };
|
|
9522
|
+
* ```
|
|
9523
|
+
*
|
|
9524
|
+
* @remarks
|
|
9525
|
+
* **Precedence rules:**
|
|
9526
|
+
* 1. Per-form `false` - Disables for this form, regardless of global setting
|
|
9527
|
+
* 2. Per-form `true` - Enables for this form, regardless of global setting
|
|
9528
|
+
* 3. Per-form `undefined` - Uses global setting
|
|
9529
|
+
* 4. Global `withEventFormValue()` - Enables globally
|
|
9530
|
+
* 5. No global feature - Disabled globally (default)
|
|
9531
|
+
*
|
|
9532
|
+
* @returns A DynamicFormFeature that enables form value attachment to events
|
|
9533
|
+
*
|
|
9534
|
+
* @public
|
|
9535
|
+
*/
|
|
9536
|
+
function withEventFormValue() {
|
|
9537
|
+
return createFeature('event-form-value', [{ provide: EMIT_FORM_VALUE_ON_EVENTS, useValue: true }]);
|
|
9538
|
+
}
|
|
9539
|
+
|
|
9540
|
+
/**
|
|
9541
|
+
* Type guard to check if a form event has a form value attached.
|
|
9542
|
+
*
|
|
9543
|
+
* Use this to safely access the `formValue` property on events when
|
|
9544
|
+
* `withEventFormValue()` is enabled globally or `options.emitFormValueOnEvents`
|
|
9545
|
+
* is set to `true` in the form config.
|
|
9546
|
+
*
|
|
9547
|
+
* @example
|
|
9548
|
+
* ```typescript
|
|
9549
|
+
* eventBus.on<SubmitEvent>('submit').subscribe(event => {
|
|
9550
|
+
* if (hasFormValue(event)) {
|
|
9551
|
+
* console.log('Form value:', event.formValue);
|
|
9552
|
+
* }
|
|
9553
|
+
* });
|
|
9554
|
+
* ```
|
|
9555
|
+
*
|
|
9556
|
+
* @param event - The form event to check
|
|
9557
|
+
* @returns `true` if the event has a form value attached, narrowing the type
|
|
9558
|
+
*
|
|
9559
|
+
* @public
|
|
9560
|
+
*/
|
|
9561
|
+
function hasFormValue(event) {
|
|
9562
|
+
return 'formValue' in event && event.formValue !== undefined;
|
|
9563
|
+
}
|
|
9564
|
+
|
|
9338
9565
|
/**
|
|
9339
9566
|
* No-op logger for when no logger is provided.
|
|
9340
9567
|
* Used as fallback in button logic evaluation to avoid breaking downstream packages.
|
|
@@ -9646,6 +9873,176 @@ function formConfig(config) {
|
|
|
9646
9873
|
return config;
|
|
9647
9874
|
}
|
|
9648
9875
|
|
|
9876
|
+
/**
|
|
9877
|
+
* Container field types that do NOT support labels or most logic
|
|
9878
|
+
*/
|
|
9879
|
+
const CONTAINER_TYPES = ['group', 'row', 'array'];
|
|
9880
|
+
/**
|
|
9881
|
+
* Field types that support options at the field level
|
|
9882
|
+
*/
|
|
9883
|
+
const OPTION_FIELD_TYPES = ['select', 'radio', 'multi-checkbox'];
|
|
9884
|
+
/**
|
|
9885
|
+
* Field type that uses minValue/maxValue instead of min/max in props
|
|
9886
|
+
*/
|
|
9887
|
+
const SLIDER_TYPE = 'slider';
|
|
9888
|
+
/**
|
|
9889
|
+
* Hidden field type - no logic, no validators, no label
|
|
9890
|
+
*/
|
|
9891
|
+
const HIDDEN_TYPE = 'hidden';
|
|
9892
|
+
/**
|
|
9893
|
+
* Page field type - only supports hidden logic
|
|
9894
|
+
*/
|
|
9895
|
+
const PAGE_TYPE = 'page';
|
|
9896
|
+
/**
|
|
9897
|
+
* Creates a typed field configuration with helpful error messages for common mistakes.
|
|
9898
|
+
*
|
|
9899
|
+
* This helper function provides early validation and clear error messages
|
|
9900
|
+
* for common configuration pitfalls that would otherwise cause runtime errors.
|
|
9901
|
+
*
|
|
9902
|
+
* @example
|
|
9903
|
+
* ```typescript
|
|
9904
|
+
* // Basic usage
|
|
9905
|
+
* const nameField = createField('input', {
|
|
9906
|
+
* key: 'name',
|
|
9907
|
+
* label: 'Name',
|
|
9908
|
+
* value: ''
|
|
9909
|
+
* });
|
|
9910
|
+
*
|
|
9911
|
+
* // With validation
|
|
9912
|
+
* const emailField = createField('input', {
|
|
9913
|
+
* key: 'email',
|
|
9914
|
+
* label: 'Email',
|
|
9915
|
+
* required: true,
|
|
9916
|
+
* email: true,
|
|
9917
|
+
* props: { type: 'email' }
|
|
9918
|
+
* });
|
|
9919
|
+
*
|
|
9920
|
+
* // Select with options (at field level)
|
|
9921
|
+
* const countryField = createField('select', {
|
|
9922
|
+
* key: 'country',
|
|
9923
|
+
* label: 'Country',
|
|
9924
|
+
* options: [{ label: 'USA', value: 'us' }]
|
|
9925
|
+
* });
|
|
9926
|
+
*
|
|
9927
|
+
* // Slider with minValue/maxValue (at field level)
|
|
9928
|
+
* const ratingField = createField('slider', {
|
|
9929
|
+
* key: 'rating',
|
|
9930
|
+
* label: 'Rating',
|
|
9931
|
+
* minValue: 1,
|
|
9932
|
+
* maxValue: 10,
|
|
9933
|
+
* step: 1,
|
|
9934
|
+
* value: 5
|
|
9935
|
+
* });
|
|
9936
|
+
* ```
|
|
9937
|
+
*
|
|
9938
|
+
* @param type - The field type (e.g., 'input', 'select', 'group')
|
|
9939
|
+
* @param config - The field configuration (excluding the 'type' property)
|
|
9940
|
+
* @returns A properly typed field configuration
|
|
9941
|
+
* @throws DynamicFormError if the configuration contains common mistakes
|
|
9942
|
+
*/
|
|
9943
|
+
function createField(type, config) {
|
|
9944
|
+
const configWithProps = config;
|
|
9945
|
+
// Validate key is present
|
|
9946
|
+
if (!configWithProps['key']) {
|
|
9947
|
+
throw new DynamicFormError(`createField('${type}'): 'key' property is required`);
|
|
9948
|
+
}
|
|
9949
|
+
// Container validation: no label allowed
|
|
9950
|
+
if (CONTAINER_TYPES.includes(type)) {
|
|
9951
|
+
if ('label' in configWithProps && configWithProps['label'] !== undefined) {
|
|
9952
|
+
throw new DynamicFormError(`createField('${type}'): Container fields (${CONTAINER_TYPES.join(', ')}) do NOT support 'label'. ` +
|
|
9953
|
+
`Labels go on the child fields inside the container.`);
|
|
9954
|
+
}
|
|
9955
|
+
}
|
|
9956
|
+
// Container validation: no logic allowed (except page with hidden)
|
|
9957
|
+
if (CONTAINER_TYPES.includes(type)) {
|
|
9958
|
+
if ('logic' in configWithProps && configWithProps['logic'] !== undefined) {
|
|
9959
|
+
throw new DynamicFormError(`createField('${type}'): Container fields (${CONTAINER_TYPES.join(', ')}) do NOT support 'logic'. ` +
|
|
9960
|
+
`Apply logic to child fields instead, or use 'page' containers which support 'hidden' logic only.`);
|
|
9961
|
+
}
|
|
9962
|
+
}
|
|
9963
|
+
// Page validation: only hidden logic allowed
|
|
9964
|
+
if (type === PAGE_TYPE && 'logic' in configWithProps) {
|
|
9965
|
+
const logic = configWithProps['logic'];
|
|
9966
|
+
if (Array.isArray(logic)) {
|
|
9967
|
+
const nonHiddenLogic = logic.filter((l) => l.type !== 'hidden');
|
|
9968
|
+
if (nonHiddenLogic.length > 0) {
|
|
9969
|
+
throw new DynamicFormError(`createField('page'): Pages only support 'hidden' logic type. ` +
|
|
9970
|
+
`Found unsupported logic types: ${nonHiddenLogic.map((l) => l.type).join(', ')}. ` +
|
|
9971
|
+
`For other logic types, apply them to child fields instead.`);
|
|
9972
|
+
}
|
|
9973
|
+
}
|
|
9974
|
+
}
|
|
9975
|
+
// Options placement validation
|
|
9976
|
+
if (OPTION_FIELD_TYPES.includes(type)) {
|
|
9977
|
+
const props = configWithProps['props'];
|
|
9978
|
+
if (props && 'options' in props) {
|
|
9979
|
+
throw new DynamicFormError(`createField('${type}'): 'options' must be at FIELD level, NOT inside 'props'. ` +
|
|
9980
|
+
`Move 'options' from props to the field root: { type: '${type}', key: '...', options: [...] }`);
|
|
9981
|
+
}
|
|
9982
|
+
}
|
|
9983
|
+
// Slider props validation
|
|
9984
|
+
if (type === SLIDER_TYPE) {
|
|
9985
|
+
const props = configWithProps['props'];
|
|
9986
|
+
if (props) {
|
|
9987
|
+
if ('min' in props || 'max' in props) {
|
|
9988
|
+
throw new DynamicFormError(`createField('slider'): Use 'minValue' and 'maxValue' at FIELD level, NOT 'min'/'max' in props. ` +
|
|
9989
|
+
`Example: { type: 'slider', key: '...', minValue: 0, maxValue: 100, step: 1 }`);
|
|
9990
|
+
}
|
|
9991
|
+
if ('step' in props) {
|
|
9992
|
+
throw new DynamicFormError(`createField('slider'): Use 'step' at FIELD level, NOT in props. ` +
|
|
9993
|
+
`Example: { type: 'slider', key: '...', minValue: 0, maxValue: 100, step: 1 }`);
|
|
9994
|
+
}
|
|
9995
|
+
}
|
|
9996
|
+
}
|
|
9997
|
+
// Hidden field validation
|
|
9998
|
+
if (type === HIDDEN_TYPE) {
|
|
9999
|
+
if ('logic' in configWithProps && configWithProps['logic'] !== undefined) {
|
|
10000
|
+
throw new DynamicFormError(`createField('hidden'): Hidden fields do NOT support 'logic'. ` + `Hidden fields are purely for passing values through the form.`);
|
|
10001
|
+
}
|
|
10002
|
+
if ('validators' in configWithProps && configWithProps['validators'] !== undefined) {
|
|
10003
|
+
throw new DynamicFormError(`createField('hidden'): Hidden fields do NOT support 'validators'. ` +
|
|
10004
|
+
`Hidden fields are purely for passing values through the form.`);
|
|
10005
|
+
}
|
|
10006
|
+
if ('required' in configWithProps && configWithProps['required']) {
|
|
10007
|
+
throw new DynamicFormError(`createField('hidden'): Hidden fields do NOT support 'required' validation. ` +
|
|
10008
|
+
`Hidden fields are purely for passing values through the form.`);
|
|
10009
|
+
}
|
|
10010
|
+
if ('label' in configWithProps && configWithProps['label'] !== undefined) {
|
|
10011
|
+
throw new DynamicFormError(`createField('hidden'): Hidden fields do NOT support 'label'. ` +
|
|
10012
|
+
`Hidden fields are not rendered and have no visual representation.`);
|
|
10013
|
+
}
|
|
10014
|
+
}
|
|
10015
|
+
// Array field validation
|
|
10016
|
+
if (type === 'array') {
|
|
10017
|
+
if ('template' in configWithProps) {
|
|
10018
|
+
throw new DynamicFormError(`createField('array'): Use 'fields' (NOT 'template') to define the array item structure. ` +
|
|
10019
|
+
`Example: { type: 'array', key: '...', fields: [{ type: 'group', key: 'item', fields: [...] }] }`);
|
|
10020
|
+
}
|
|
10021
|
+
if ('minItems' in configWithProps || 'maxItems' in configWithProps) {
|
|
10022
|
+
throw new DynamicFormError(`createField('array'): 'minItems' and 'maxItems' properties are NOT supported. ` +
|
|
10023
|
+
`Use custom validation if you need to enforce array length constraints.`);
|
|
10024
|
+
}
|
|
10025
|
+
}
|
|
10026
|
+
// Generic button validation
|
|
10027
|
+
if (type === 'button') {
|
|
10028
|
+
if (!('event' in configWithProps) || configWithProps['event'] === undefined) {
|
|
10029
|
+
throw new DynamicFormError(`createField('button'): Generic buttons REQUIRE the 'event' property with a FormEventConstructor. ` +
|
|
10030
|
+
`For common actions, use 'submit', 'next', or 'previous' instead which don't require event configuration. ` +
|
|
10031
|
+
`Example: { type: 'submit', key: 'submit', label: 'Submit' }`);
|
|
10032
|
+
}
|
|
10033
|
+
}
|
|
10034
|
+
return { type, ...config };
|
|
10035
|
+
}
|
|
10036
|
+
/**
|
|
10037
|
+
* Shorthand alias for createField
|
|
10038
|
+
*
|
|
10039
|
+
* @example
|
|
10040
|
+
* ```typescript
|
|
10041
|
+
* const nameField = field('input', { key: 'name', label: 'Name', value: '' });
|
|
10042
|
+
* ```
|
|
10043
|
+
*/
|
|
10044
|
+
const field = createField;
|
|
10045
|
+
|
|
9649
10046
|
/**
|
|
9650
10047
|
* Converts DynamicText (string | Observable | Signal) to Observable<string>
|
|
9651
10048
|
* Unifies all three types into a consistent Observable stream
|
|
@@ -10392,49 +10789,65 @@ class ArrayFieldComponent {
|
|
|
10392
10789
|
this.resolvedItemsSignal.set([]);
|
|
10393
10790
|
break;
|
|
10394
10791
|
case 'initial':
|
|
10395
|
-
this.resolveAllItems(fieldTrees, updateId);
|
|
10792
|
+
void this.resolveAllItems(fieldTrees, updateId);
|
|
10396
10793
|
break;
|
|
10397
10794
|
case 'append':
|
|
10398
|
-
this.appendItems(fieldTrees, operation.startIndex, operation.endIndex, updateId);
|
|
10795
|
+
void this.appendItems(fieldTrees, operation.startIndex, operation.endIndex, updateId);
|
|
10399
10796
|
break;
|
|
10400
10797
|
case 'pop':
|
|
10401
10798
|
this.resolvedItemsSignal.set(resolvedItems.slice(0, operation.newLength));
|
|
10402
10799
|
break;
|
|
10403
10800
|
case 'recreate':
|
|
10404
10801
|
this.resolvedItemsSignal.set([]);
|
|
10405
|
-
this.resolveAllItems(fieldTrees, updateId);
|
|
10802
|
+
void this.resolveAllItems(fieldTrees, updateId);
|
|
10406
10803
|
break;
|
|
10407
10804
|
case 'none':
|
|
10408
10805
|
break;
|
|
10409
10806
|
}
|
|
10410
10807
|
}
|
|
10411
|
-
resolveAllItems(fieldTrees, updateId) {
|
|
10808
|
+
async resolveAllItems(fieldTrees, updateId) {
|
|
10412
10809
|
if (fieldTrees.length === 0) {
|
|
10413
10810
|
this.resolvedItemsSignal.set([]);
|
|
10414
10811
|
return;
|
|
10415
10812
|
}
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10813
|
+
// Wrap each item observable to catch individual errors
|
|
10814
|
+
const safeItemObservables = fieldTrees.map((fieldTree, i) => this.createResolveItemObservable(fieldTree, i).pipe(catchError((error) => {
|
|
10815
|
+
this.logger.error(`Failed to resolve array item at index ${i}:`, error);
|
|
10816
|
+
return of(undefined);
|
|
10817
|
+
})));
|
|
10818
|
+
try {
|
|
10819
|
+
const items = await firstValueFrom(forkJoin(safeItemObservables).pipe(map((items) => items.filter((item) => item !== undefined))));
|
|
10420
10820
|
if (updateId === this.updateVersion()) {
|
|
10421
|
-
this.resolvedItemsSignal.set(
|
|
10821
|
+
this.resolvedItemsSignal.set(items);
|
|
10422
10822
|
emitComponentInitialized(this.eventBus, 'array', this.field().key, this.parentInjector);
|
|
10423
10823
|
}
|
|
10424
|
-
}
|
|
10824
|
+
}
|
|
10825
|
+
catch (err) {
|
|
10826
|
+
this.logger.error('Failed to resolve array items:', err);
|
|
10827
|
+
this.resolvedItemsSignal.set([]);
|
|
10828
|
+
}
|
|
10425
10829
|
}
|
|
10426
|
-
appendItems(fieldTrees, startIndex, endIndex, updateId) {
|
|
10830
|
+
async appendItems(fieldTrees, startIndex, endIndex, updateId) {
|
|
10427
10831
|
const itemsToResolve = fieldTrees.slice(startIndex, endIndex);
|
|
10428
10832
|
if (itemsToResolve.length === 0)
|
|
10429
10833
|
return;
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
.
|
|
10834
|
+
// Wrap each item observable to catch individual errors
|
|
10835
|
+
const safeItemObservables = itemsToResolve.map((fieldTree, i) => {
|
|
10836
|
+
const index = startIndex + i;
|
|
10837
|
+
return this.createResolveItemObservable(fieldTree, index).pipe(catchError((error) => {
|
|
10838
|
+
this.logger.error(`Failed to resolve array item at index ${index}:`, error);
|
|
10839
|
+
return of(undefined);
|
|
10840
|
+
}));
|
|
10841
|
+
});
|
|
10842
|
+
try {
|
|
10843
|
+
const newItems = await firstValueFrom(forkJoin(safeItemObservables).pipe(map((items) => items.filter((item) => item !== undefined))));
|
|
10434
10844
|
if (updateId === this.updateVersion()) {
|
|
10435
10845
|
this.resolvedItemsSignal.update((current) => [...current, ...newItems]);
|
|
10436
10846
|
}
|
|
10437
|
-
}
|
|
10847
|
+
}
|
|
10848
|
+
catch (err) {
|
|
10849
|
+
this.logger.error('Failed to append array items:', err);
|
|
10850
|
+
}
|
|
10438
10851
|
}
|
|
10439
10852
|
createResolveItemObservable(fieldTree, index) {
|
|
10440
10853
|
const template = this.fieldTemplate();
|
|
@@ -10466,9 +10879,11 @@ class ArrayFieldComponent {
|
|
|
10466
10879
|
const insertIndex = index !== undefined ? Math.min(index, currentArray.length) : currentArray.length;
|
|
10467
10880
|
const rawValue = getFieldDefaultValue(fieldTemplate, this.rawFieldRegistry());
|
|
10468
10881
|
const valueHandling = getFieldValueHandling(fieldTemplate.type, this.rawFieldRegistry());
|
|
10469
|
-
|
|
10470
|
-
//
|
|
10471
|
-
|
|
10882
|
+
const templateType = fieldTemplate.type;
|
|
10883
|
+
// Container fields (group, row) already have their own structure, so use rawValue as-is.
|
|
10884
|
+
// For value fields with 'include' handling, wrap in an object using the field's key.
|
|
10885
|
+
const isContainer = templateType === 'group' || templateType === 'row';
|
|
10886
|
+
const value = isContainer ? rawValue : valueHandling === 'include' && fieldTemplate.key ? { [fieldTemplate.key]: rawValue } : rawValue;
|
|
10472
10887
|
const newArray = [...currentArray];
|
|
10473
10888
|
newArray.splice(insertIndex, 0, value);
|
|
10474
10889
|
// Update the parent form with the new array value
|
|
@@ -10656,5 +11071,5 @@ function extractErrorParams(error) {
|
|
|
10656
11071
|
* Generated bundle index. Do not edit.
|
|
10657
11072
|
*/
|
|
10658
11073
|
|
|
10659
|
-
export {
|
|
10660
|
-
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-
|
|
11074
|
+
export { dynamicTextToObservable as $, AddArrayItemEvent as A, PreviousPageEvent as B, ConsoleLogger as C, DynamicFormLogger as D, EventBus as E, FormClearEvent as F, createField as G, field as H, formConfig as I, DynamicFormError as J, baseFieldMapper as K, buildBaseInputs as L, arrayFieldMapper as M, NoopLogger as N, groupFieldMapper as O, PageChangeEvent as P, pageFieldMapper as Q, RemoveArrayItemEvent as R, SubmitEvent as S, rowFieldMapper as T, textFieldMapper as U, FIELD_REGISTRY as V, ARRAY_CONTEXT as W, DEFAULT_PROPS as X, DEFAULT_VALIDATION_MESSAGES as Y, FIELD_SIGNAL_CONTEXT as Z, FORM_OPTIONS as _, DynamicTextPipe as a, ArrayFieldComponent as a0, GroupFieldComponent as a1, RowFieldComponent as a2, applyValidator as a3, applyValidators as a4, getArrayLength as a5, resolveTokens as a6, BUILT_IN_FIELDS as a7, isEqual as a8, omit as a9, interpolateParams as aa, applyMetaToElement as ab, RootFormRegistryService as ac, DynamicForm as b, createFieldResolutionPipe as c, derivedFromDeferred as d, emitComponentInitialized as e, withEventFormValue as f, isFormStateCondition as g, hasFormValue as h, injectFieldRegistry as i, resolveNextButtonDisabled as j, isCheckedField as k, isValueField as l, isRowField as m, isArrayField as n, isContainerField as o, provideDynamicForm as p, isDisplayOnlyField as q, resolveSubmitButtonDisabled as r, isGroupField as s, isLeafField as t, isPageField as u, validatePageNesting as v, withLoggerConfig as w, isValueBearingField as x, FormResetEvent as y, NextPageEvent as z };
|
|
11075
|
+
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-BWFdMPyw.mjs.map
|