@ng-forge/dynamic-forms 0.9.0-next.10 → 0.9.0-next.11
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-array-field.component-DbWes42V.mjs → ng-forge-dynamic-forms-array-field.component-DJ-LiLm_.mjs} +17 -4
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-DJ-LiLm_.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs → ng-forge-dynamic-forms-container-field.component-Pcxg5PQu.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs.map → ng-forge-dynamic-forms-container-field.component-Pcxg5PQu.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs → ng-forge-dynamic-forms-container-utils-a4RstR2Z.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs.map → ng-forge-dynamic-forms-container-utils-a4RstR2Z.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs → ng-forge-dynamic-forms-css-wrapper.component-CJACCIEK.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-CJACCIEK.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs → ng-forge-dynamic-forms-emit-initialization-Bjf_I0D_.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs.map → ng-forge-dynamic-forms-emit-initialization-Bjf_I0D_.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs → ng-forge-dynamic-forms-group-field.component-fzKwPrQr.mjs} +24 -4
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-fzKwPrQr.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-o3rJX4vG.mjs} +328 -81
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-o3rJX4vG.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs → ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs.map → ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs → ng-forge-dynamic-forms-text-field.component-B9SK8Vun.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs.map → ng-forge-dynamic-forms-text-field.component-B9SK8Vun.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +1 -1
- package/types/ng-forge-dynamic-forms.d.ts +1827 -1608
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs.map +0 -1
|
@@ -139,6 +139,32 @@ const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT', {
|
|
|
139
139
|
* ```
|
|
140
140
|
*/
|
|
141
141
|
const ARRAY_CONTEXT = new InjectionToken('ARRAY_CONTEXT');
|
|
142
|
+
/**
|
|
143
|
+
* Injection token for providing group ancestry to mappers and components.
|
|
144
|
+
*
|
|
145
|
+
* Re-provided in two places:
|
|
146
|
+
* 1. `GroupFieldComponent` provides a context whose `groupPath()` is the
|
|
147
|
+
* parent path extended by its own key (e.g. `'address'` or `'user.address'`).
|
|
148
|
+
* 2. `createArrayItemInjector` provides a sentinel with an empty `groupPath`,
|
|
149
|
+
* resetting the chain at array boundaries — descendants of an array item
|
|
150
|
+
* that re-enter a group compose paths INSIDE the array item, matching the
|
|
151
|
+
* property-derivation collector's keying.
|
|
152
|
+
*
|
|
153
|
+
* `mapFieldToInputs` reads this to scope DOM IDs and override-store keys so
|
|
154
|
+
* the same leaf key can appear in different groups without colliding (#401).
|
|
155
|
+
*
|
|
156
|
+
* Inject with `{ optional: true }` because top-level fields with no group
|
|
157
|
+
* ancestor (and no enclosing array) won't have the token provided. The token
|
|
158
|
+
* IS present for fields inside an array even when no inner group exists — the
|
|
159
|
+
* array-boundary sentinel makes injection succeed with an empty `groupPath`.
|
|
160
|
+
*/
|
|
161
|
+
// No factory: GROUP_CONTEXT is optional-by-design — `GroupFieldComponent` and
|
|
162
|
+
// `createArrayItemInjector` re-provide it where it makes sense, and consumers
|
|
163
|
+
// inject with `{ optional: true }` to handle the top-level case. Mirrors the
|
|
164
|
+
// ARRAY_CONTEXT pattern. Adding a throwing factory here fires even when
|
|
165
|
+
// injected optionally (Angular still evaluates the tree-shakable factory
|
|
166
|
+
// before honoring `optional`), so non-group fields would crash on every render.
|
|
167
|
+
const GROUP_CONTEXT = new InjectionToken('GROUP_CONTEXT');
|
|
142
168
|
/**
|
|
143
169
|
* Injection token for form-level default props.
|
|
144
170
|
*
|
|
@@ -2748,16 +2774,25 @@ function evaluateCondition(expression, context) {
|
|
|
2748
2774
|
case 'javascript':
|
|
2749
2775
|
return evaluateJavaScriptExpression(expression, context);
|
|
2750
2776
|
case 'custom': {
|
|
2751
|
-
|
|
2777
|
+
// Read both keys via cast — the XOR type narrows to `never` when both
|
|
2778
|
+
// would be truthy, but JSON-loaded configs can produce that state at
|
|
2779
|
+
// runtime. We accept it here, warn, and let inline win.
|
|
2780
|
+
const bothSet = expression;
|
|
2781
|
+
const inlineFn = bothSet.fn;
|
|
2782
|
+
const registeredName = bothSet.functionName;
|
|
2783
|
+
if (inlineFn && registeredName) {
|
|
2784
|
+
context.logger.warn('Both "fn" and "functionName" are set on custom condition. Inline "fn" takes precedence; "functionName" is ignored.');
|
|
2785
|
+
}
|
|
2786
|
+
const customFn = inlineFn ?? (registeredName ? context.customFunctions?.[registeredName] : undefined);
|
|
2752
2787
|
if (!customFn) {
|
|
2753
|
-
context.logger.error('Custom function not found:',
|
|
2788
|
+
context.logger.error('Custom function not found:', registeredName);
|
|
2754
2789
|
return false;
|
|
2755
2790
|
}
|
|
2756
2791
|
try {
|
|
2757
2792
|
return !!customFn(context);
|
|
2758
2793
|
}
|
|
2759
2794
|
catch (error) {
|
|
2760
|
-
context.logger.error('Error executing custom function:',
|
|
2795
|
+
context.logger.error('Error executing custom function:', registeredName ?? '<inline>', error);
|
|
2761
2796
|
return false;
|
|
2762
2797
|
}
|
|
2763
2798
|
}
|
|
@@ -2766,7 +2801,7 @@ function evaluateCondition(expression, context) {
|
|
|
2766
2801
|
case 'or':
|
|
2767
2802
|
return evaluateOrCondition(expression, context);
|
|
2768
2803
|
case 'http':
|
|
2769
|
-
context.logger.warn('
|
|
2804
|
+
context.logger.warn('HTTP conditions are resolved asynchronously via createHttpConditionLogicFunction(). ' +
|
|
2770
2805
|
'When used inside and/or composites, the HTTP result is not available synchronously. Returning false.');
|
|
2771
2806
|
return false;
|
|
2772
2807
|
case 'async':
|
|
@@ -4145,9 +4180,9 @@ class PageOrchestratorComponent {
|
|
|
4145
4180
|
}
|
|
4146
4181
|
}
|
|
4147
4182
|
}
|
|
4148
|
-
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-
|
|
4183
|
+
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs').then(m => m.default)]] });
|
|
4149
4184
|
}
|
|
4150
|
-
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-
|
|
4185
|
+
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
|
|
4151
4186
|
type: Component,
|
|
4152
4187
|
args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
|
|
4153
4188
|
@for (pageField of pageFields(); track pageField.key; let i = $index) {
|
|
@@ -4628,7 +4663,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
4628
4663
|
* Use this guard when you need type-safe access to `FunctionHttpValidatorConfig` properties.
|
|
4629
4664
|
*/
|
|
4630
4665
|
function isFunctionHttpValidator(config) {
|
|
4631
|
-
|
|
4666
|
+
const c = config;
|
|
4667
|
+
return typeof c.functionName === 'string' || (c.fn !== null && typeof c.fn === 'object');
|
|
4632
4668
|
}
|
|
4633
4669
|
|
|
4634
4670
|
/**
|
|
@@ -5047,11 +5083,18 @@ function createAsyncConditionLogicFunction(condition) {
|
|
|
5047
5083
|
const cacheService = inject(AsyncConditionFunctionCacheService);
|
|
5048
5084
|
const pendingValue = condition.pendingValue ?? false;
|
|
5049
5085
|
const debounceMs = condition.debounceMs ?? 300;
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5086
|
+
if (condition.asyncFn && condition.asyncFunctionName) {
|
|
5087
|
+
logger.warn('Both "asyncFn" and "asyncFunctionName" are set on async condition. Inline "asyncFn" takes precedence; "asyncFunctionName" is ignored.');
|
|
5088
|
+
}
|
|
5089
|
+
// Skip cross-condition caching when an inline function is used: stableStringify
|
|
5090
|
+
// cannot distinguish two different inline functions, which would cause cache hits
|
|
5091
|
+
// to wrongly reuse a LogicFn closing over a different function reference.
|
|
5092
|
+
const cacheKey = condition.asyncFn ? undefined : stableStringify(condition);
|
|
5093
|
+
if (cacheKey !== undefined) {
|
|
5094
|
+
const cached = cacheService.asyncConditionFunctionCache.get(cacheKey);
|
|
5095
|
+
if (cached) {
|
|
5096
|
+
return cached;
|
|
5097
|
+
}
|
|
5055
5098
|
}
|
|
5056
5099
|
// Each condition needs its own per-field signal store.
|
|
5057
5100
|
const perFunctionSignalStore = new Map();
|
|
@@ -5076,8 +5119,10 @@ function createAsyncConditionLogicFunction(condition) {
|
|
|
5076
5119
|
const asyncResource = rxResource({
|
|
5077
5120
|
params: () => debouncedTrigger() ?? undefined,
|
|
5078
5121
|
stream: ({ params: triggerKey }) => {
|
|
5079
|
-
//
|
|
5080
|
-
|
|
5122
|
+
// Inline `asyncFn` wins; otherwise look up the registered function at call time
|
|
5123
|
+
// (fresh reference, in case the registry was updated after LogicFn creation).
|
|
5124
|
+
const asyncFn = condition.asyncFn ??
|
|
5125
|
+
(condition.asyncFunctionName ? functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName) : undefined);
|
|
5081
5126
|
if (!asyncFn) {
|
|
5082
5127
|
logger.warn(`Async Condition - function '${condition.asyncFunctionName}' not found. ` +
|
|
5083
5128
|
`Register it in customFnConfig.asyncConditions.`);
|
|
@@ -5129,7 +5174,9 @@ function createAsyncConditionLogicFunction(condition) {
|
|
|
5129
5174
|
});
|
|
5130
5175
|
return resultResource.value();
|
|
5131
5176
|
};
|
|
5132
|
-
|
|
5177
|
+
if (cacheKey !== undefined) {
|
|
5178
|
+
cacheService.asyncConditionFunctionCache.set(cacheKey, fn);
|
|
5179
|
+
}
|
|
5133
5180
|
return fn;
|
|
5134
5181
|
}
|
|
5135
5182
|
|
|
@@ -5764,12 +5811,12 @@ function applyCustomValidator(config, fieldPath) {
|
|
|
5764
5811
|
if (config.expression) {
|
|
5765
5812
|
validatorFn = createExpressionValidator(config);
|
|
5766
5813
|
}
|
|
5767
|
-
else if (config.functionName) {
|
|
5814
|
+
else if (config.fn || config.functionName) {
|
|
5768
5815
|
validatorFn = createFunctionValidator(config);
|
|
5769
5816
|
}
|
|
5770
5817
|
else {
|
|
5771
5818
|
const logger = inject(DynamicFormLogger);
|
|
5772
|
-
logger.warn('Custom validator must have
|
|
5819
|
+
logger.warn('Custom validator must have one of "expression", "functionName", or "fn"');
|
|
5773
5820
|
return;
|
|
5774
5821
|
}
|
|
5775
5822
|
const whenLogic = createConditionalLogic(config.when);
|
|
@@ -5781,6 +5828,13 @@ function applyCustomValidator(config, fieldPath) {
|
|
|
5781
5828
|
}
|
|
5782
5829
|
function createFunctionValidator(config) {
|
|
5783
5830
|
const logger = inject(DynamicFormLogger);
|
|
5831
|
+
if (config.fn && config.functionName) {
|
|
5832
|
+
logger.warn('Both "fn" and "functionName" are set on custom validator. Inline "fn" takes precedence; "functionName" is ignored.');
|
|
5833
|
+
}
|
|
5834
|
+
if (config.fn) {
|
|
5835
|
+
const inlineFn = config.fn;
|
|
5836
|
+
return (ctx) => inlineFn(ctx, config.params);
|
|
5837
|
+
}
|
|
5784
5838
|
const functionName = config.functionName;
|
|
5785
5839
|
if (!functionName) {
|
|
5786
5840
|
logger.warn('Custom validator missing functionName');
|
|
@@ -5841,10 +5895,24 @@ function createExpressionValidator(config) {
|
|
|
5841
5895
|
* - onError: Optional handler for resource errors
|
|
5842
5896
|
*/
|
|
5843
5897
|
function applyAsyncValidator(config, fieldPath) {
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5898
|
+
if (config.fn && config.functionName) {
|
|
5899
|
+
const logger = inject(DynamicFormLogger);
|
|
5900
|
+
logger.warn('Both "fn" and "functionName" are set on async validator. Inline "fn" takes precedence; "functionName" is ignored.');
|
|
5901
|
+
}
|
|
5902
|
+
let validatorConfig;
|
|
5903
|
+
if (config.fn) {
|
|
5904
|
+
validatorConfig = config.fn;
|
|
5905
|
+
}
|
|
5906
|
+
else if (config.functionName) {
|
|
5907
|
+
const registry = inject(FunctionRegistryService);
|
|
5908
|
+
validatorConfig = registry.getAsyncValidator(config.functionName);
|
|
5909
|
+
if (!validatorConfig) {
|
|
5910
|
+
throw new DynamicFormError(`Async validator "${config.functionName}" not found. Register it with customFnConfig.asyncValidators.`);
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
else {
|
|
5914
|
+
// XOR type rejects this at compile time; JSON-loaded configs can still reach here at runtime.
|
|
5915
|
+
throw new DynamicFormError('Async validator requires one of "functionName" or "fn".');
|
|
5848
5916
|
}
|
|
5849
5917
|
const whenLogic = createConditionalLogic(config.when);
|
|
5850
5918
|
const asyncOptions = {
|
|
@@ -5866,10 +5934,16 @@ function applyAsyncValidator(config, fieldPath) {
|
|
|
5866
5934
|
function applyUnifiedHttpValidator(config, fieldPath) {
|
|
5867
5935
|
if (isFunctionHttpValidator(config)) {
|
|
5868
5936
|
applyFunctionHttpValidator(config, fieldPath);
|
|
5937
|
+
return;
|
|
5869
5938
|
}
|
|
5870
|
-
|
|
5871
|
-
|
|
5939
|
+
// Declarative path requires both `http` and `responseMapping` — reject early
|
|
5940
|
+
// for JSON-loaded configs that have neither side fully populated. The
|
|
5941
|
+
// discriminated union forbids this at compile time.
|
|
5942
|
+
const declarative = config;
|
|
5943
|
+
if (!declarative.http || !declarative.responseMapping) {
|
|
5944
|
+
throw new DynamicFormError('HTTP validator requires one of "functionName" / "fn" (function-based) or "http" + "responseMapping" (declarative).');
|
|
5872
5945
|
}
|
|
5946
|
+
applyDeclarativeHttpValidator(config, fieldPath);
|
|
5873
5947
|
}
|
|
5874
5948
|
/**
|
|
5875
5949
|
* Apply function-based HTTP validator to field path using Angular's public validateHttp() API.
|
|
@@ -5880,10 +5954,24 @@ function applyUnifiedHttpValidator(config, fieldPath) {
|
|
|
5880
5954
|
* - onError: Optional handler for HTTP errors
|
|
5881
5955
|
*/
|
|
5882
5956
|
function applyFunctionHttpValidator(config, fieldPath) {
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5957
|
+
if (config.fn && config.functionName) {
|
|
5958
|
+
const logger = inject(DynamicFormLogger);
|
|
5959
|
+
logger.warn('Both "fn" and "functionName" are set on HTTP validator. Inline "fn" takes precedence; "functionName" is ignored.');
|
|
5960
|
+
}
|
|
5961
|
+
let httpValidatorConfig;
|
|
5962
|
+
if (config.fn) {
|
|
5963
|
+
httpValidatorConfig = config.fn;
|
|
5964
|
+
}
|
|
5965
|
+
else if (config.functionName) {
|
|
5966
|
+
const registry = inject(FunctionRegistryService);
|
|
5967
|
+
httpValidatorConfig = registry.getHttpValidator(config.functionName);
|
|
5968
|
+
if (!httpValidatorConfig) {
|
|
5969
|
+
throw new DynamicFormError(`HTTP validator "${config.functionName}" not found. Register it with customFnConfig.httpValidators.`);
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
else {
|
|
5973
|
+
// XOR type rejects this at compile time; JSON-loaded configs can still reach here at runtime.
|
|
5974
|
+
throw new DynamicFormError('HTTP validator requires one of "functionName" or "fn".');
|
|
5887
5975
|
}
|
|
5888
5976
|
const whenLogic = createConditionalLogic(config.when);
|
|
5889
5977
|
const httpOptions = {
|
|
@@ -8467,22 +8555,31 @@ const PLACEHOLDER_INDEX = '$';
|
|
|
8467
8555
|
/**
|
|
8468
8556
|
* Builds a property override store key for a field.
|
|
8469
8557
|
*
|
|
8470
|
-
* For non-array fields, the key is
|
|
8471
|
-
*
|
|
8472
|
-
*
|
|
8558
|
+
* For non-array fields, the key is the dot-joined group ancestors plus the
|
|
8559
|
+
* field's leaf key (e.g., `address.street` for `street` inside `address`).
|
|
8560
|
+
* For array item fields, the key includes the array path, either a concrete
|
|
8561
|
+
* index or the `$` placeholder, and any group ancestors INSIDE the array
|
|
8562
|
+
* item (e.g., `contacts.0.email`, `contacts.$.email`, or
|
|
8563
|
+
* `contacts.0.profile.email` for a group nested under each contact).
|
|
8564
|
+
*
|
|
8565
|
+
* `groupPath` matches the form-value path of nested groups (collector resets
|
|
8566
|
+
* it at array boundaries — see `property-derivation-collector.ts`), so the
|
|
8567
|
+
* resulting key always mirrors the field's location in `formValue`.
|
|
8473
8568
|
*
|
|
8474
8569
|
* @param arrayKey - The array field key (e.g., 'contacts'), or undefined for non-array fields
|
|
8475
8570
|
* @param index - The array item index, `PLACEHOLDER_INDEX` for the wildcard format, or undefined for non-array fields
|
|
8476
8571
|
* @param fieldKey - The leaf field key (e.g., 'email')
|
|
8477
|
-
* @
|
|
8572
|
+
* @param groupPath - Dot-joined group ancestors scoped to the current array item (or top-level), or undefined when not inside any group
|
|
8573
|
+
* @returns The store key (e.g., 'email', 'address.street', 'contacts.0.email', 'contacts.$.profile.email')
|
|
8478
8574
|
*
|
|
8479
8575
|
* @public
|
|
8480
8576
|
*/
|
|
8481
|
-
function buildPropertyOverrideKey(arrayKey, index, fieldKey) {
|
|
8577
|
+
function buildPropertyOverrideKey(arrayKey, index, fieldKey, groupPath) {
|
|
8578
|
+
const leafScope = groupPath ? `${groupPath}.${fieldKey}` : fieldKey;
|
|
8482
8579
|
if (arrayKey != null && index != null) {
|
|
8483
|
-
return `${arrayKey}.${index}.${
|
|
8580
|
+
return `${arrayKey}.${index}.${leafScope}`;
|
|
8484
8581
|
}
|
|
8485
|
-
return
|
|
8582
|
+
return leafScope;
|
|
8486
8583
|
}
|
|
8487
8584
|
|
|
8488
8585
|
/**
|
|
@@ -8563,6 +8660,25 @@ function applyIndexSuffix(inputs, index) {
|
|
|
8563
8660
|
key: `${key}_${index}`,
|
|
8564
8661
|
};
|
|
8565
8662
|
}
|
|
8663
|
+
/**
|
|
8664
|
+
* Prefixes the key with the underscored group ancestor path so that the same
|
|
8665
|
+
* leaf key inside different groups produces distinct DOM IDs (issue #401).
|
|
8666
|
+
* Form-schema keys remain clean — only the rendered `key` input is rewritten.
|
|
8667
|
+
*
|
|
8668
|
+
* Dots in the group path become underscores, then the original leaf key is
|
|
8669
|
+
* appended: `(groupPath='user.address', inputs.key='street')` →
|
|
8670
|
+
* `inputs.key = 'user_address_street'`.
|
|
8671
|
+
*/
|
|
8672
|
+
function applyGroupPrefix(inputs, groupPath) {
|
|
8673
|
+
const key = inputs['key'];
|
|
8674
|
+
if (typeof key !== 'string' || groupPath.length === 0) {
|
|
8675
|
+
return inputs;
|
|
8676
|
+
}
|
|
8677
|
+
return {
|
|
8678
|
+
...inputs,
|
|
8679
|
+
key: `${groupPath.replace(/\./g, '_')}_${key}`,
|
|
8680
|
+
};
|
|
8681
|
+
}
|
|
8566
8682
|
/**
|
|
8567
8683
|
* Main field mapper function that uses the field registry to get the appropriate mapper
|
|
8568
8684
|
* based on the field's type property.
|
|
@@ -8599,6 +8715,10 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
|
|
|
8599
8715
|
// Check if we're inside an array item context - if so, we need to suffix keys
|
|
8600
8716
|
// Optional because ARRAY_CONTEXT is only provided inside array item injectors
|
|
8601
8717
|
const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
|
|
8718
|
+
// Check if we're inside a group context - if so, prefix the key with the group path
|
|
8719
|
+
// so overlapping leaf keys across groups produce distinct DOM IDs (issue #401).
|
|
8720
|
+
// Optional because GROUP_CONTEXT is only provided by GroupFieldComponent.
|
|
8721
|
+
const groupContext = inject(GROUP_CONTEXT, { optional: true });
|
|
8602
8722
|
// Inject the property override store for property derivation overrides
|
|
8603
8723
|
// Always available — provided at the DynamicForm component level via provideDynamicFormDI
|
|
8604
8724
|
const store = inject(PROPERTY_OVERRIDE_STORE);
|
|
@@ -8609,6 +8729,7 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
|
|
|
8609
8729
|
// Check that arrayContext exists and has a valid index signal (guards against mock injectors)
|
|
8610
8730
|
const indexSignal = arrayContext?.index;
|
|
8611
8731
|
const hasArrayContext = indexSignal !== undefined && isSignal(indexSignal);
|
|
8732
|
+
const hasGroupContext = groupContext != null;
|
|
8612
8733
|
// Fast-path check for property overrides: only fields whose definition declares
|
|
8613
8734
|
// a property-derivation enter the computed() wrapper. We inspect the FieldDef
|
|
8614
8735
|
// directly rather than asking the store, because the store's registration
|
|
@@ -8617,7 +8738,7 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
|
|
|
8617
8738
|
// collector apply the same inclusion rule, so the answers always agree.
|
|
8618
8739
|
const hasOverrides = fieldHasPropertyDerivations(fieldDef);
|
|
8619
8740
|
// Fast path: no transformations needed
|
|
8620
|
-
if (!hasPropsForwarding && !hasArrayContext && !hasOverrides) {
|
|
8741
|
+
if (!hasPropsForwarding && !hasArrayContext && !hasGroupContext && !hasOverrides) {
|
|
8621
8742
|
return mapperResult;
|
|
8622
8743
|
}
|
|
8623
8744
|
// Wrap in computed to apply transformations
|
|
@@ -8627,18 +8748,34 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
|
|
|
8627
8748
|
if (hasPropsForwarding) {
|
|
8628
8749
|
inputs = mergeForwardedPropsToMeta(inputs, propsToMeta);
|
|
8629
8750
|
}
|
|
8751
|
+
// Apply group prefix BEFORE array suffix so the resulting key reads as
|
|
8752
|
+
// `{groupPath}_{key}_{index}` (e.g. `address_street_0`) — matching the
|
|
8753
|
+
// form-value path order: group → leaf → array index.
|
|
8754
|
+
if (hasGroupContext) {
|
|
8755
|
+
// Non-null after the hasGroupContext guard.
|
|
8756
|
+
inputs = applyGroupPrefix(inputs, groupContext.groupPath());
|
|
8757
|
+
}
|
|
8630
8758
|
// Apply index suffix for array items
|
|
8631
8759
|
if (hasArrayContext) {
|
|
8632
8760
|
const index = indexSignal();
|
|
8633
8761
|
inputs = applyIndexSuffix(inputs, index);
|
|
8634
8762
|
}
|
|
8635
|
-
// Apply property overrides from the store (AFTER all static transformations)
|
|
8763
|
+
// Apply property overrides from the store (AFTER all static transformations).
|
|
8764
|
+
//
|
|
8765
|
+
// INVARIANT: the store key MUST be built from `fieldDef.key` + the group/array
|
|
8766
|
+
// ancestry (via buildPropertyOverrideKey) — NEVER from the DOM-scoped
|
|
8767
|
+
// `inputs['key']` produced by applyGroupPrefix/applyIndexSuffix above. The
|
|
8768
|
+
// collector (property-derivation-collector.ts) registers using the same
|
|
8769
|
+
// shape; if these drift, overrides silently miss their target.
|
|
8636
8770
|
if (hasOverrides) {
|
|
8637
|
-
// Build
|
|
8638
|
-
//
|
|
8771
|
+
// Build inside computed() so the index/group signals establish reactive deps.
|
|
8772
|
+
// GROUP_CONTEXT resets at array boundaries (see create-array-item-injector.ts),
|
|
8773
|
+
// so groupPath() represents ancestors inside the current array item (or
|
|
8774
|
+
// top-level groups when not in an array) — matching the collector.
|
|
8775
|
+
const groupPath = hasGroupContext ? groupContext.groupPath() : undefined;
|
|
8639
8776
|
const key = hasArrayContext
|
|
8640
|
-
? buildPropertyOverrideKey(arrayContext.arrayKey, indexSignal(), fieldDef.key)
|
|
8641
|
-
: fieldDef.key;
|
|
8777
|
+
? buildPropertyOverrideKey(arrayContext.arrayKey, indexSignal(), fieldDef.key, groupPath || undefined)
|
|
8778
|
+
: buildPropertyOverrideKey(undefined, undefined, fieldDef.key, groupPath || undefined);
|
|
8642
8779
|
const overrides = store.getOverrides(key)();
|
|
8643
8780
|
inputs = applyPropertyOverrides(inputs, overrides);
|
|
8644
8781
|
}
|
|
@@ -9079,15 +9216,32 @@ function injectFieldRegistry() {
|
|
|
9079
9216
|
* Collects all field keys, types, and validates regex patterns from a field tree
|
|
9080
9217
|
* by recursively traversing containers (page, row, group, array).
|
|
9081
9218
|
*
|
|
9082
|
-
*
|
|
9083
|
-
*
|
|
9084
|
-
*
|
|
9085
|
-
*
|
|
9219
|
+
* Keys are scoped by their group ancestors (e.g. `address.city` instead of `city`),
|
|
9220
|
+
* so the same leaf key can appear inside different groups without conflict. Page and
|
|
9221
|
+
* row containers do not contribute to the path because they flatten their children
|
|
9222
|
+
* into the parent scope. Array items are excluded entirely from key collection
|
|
9223
|
+
* because they share template keys across items by design.
|
|
9224
|
+
*
|
|
9225
|
+
* Fields whose registered `valueHandling` is anything other than `include` (e.g.
|
|
9226
|
+
* buttons with `exclude`, page/row with `flatten`) are not collected: they don't
|
|
9227
|
+
* bind to a form-value path, so duplicate-key uniqueness isn't meaningful for them.
|
|
9228
|
+
*
|
|
9229
|
+
* @param collectKeys - When false, the entire subtree skips key collection (used
|
|
9230
|
+
* when descending into an array container).
|
|
9086
9231
|
*/
|
|
9087
|
-
function collectFieldData(fields, data, collectKeys = true) {
|
|
9232
|
+
function collectFieldData(fields, data, registry, pathPrefix, collectKeys = true) {
|
|
9088
9233
|
for (const field of fields) {
|
|
9089
|
-
|
|
9090
|
-
|
|
9234
|
+
// Defensively skip only known non-include modes — unexpected/missing
|
|
9235
|
+
// valueHandling defaults back to participating in duplicate detection so a
|
|
9236
|
+
// mis-registered field type doesn't silently drop from collision checks.
|
|
9237
|
+
const valueHandling = field.type ? getFieldValueHandling(field.type, registry) : 'include';
|
|
9238
|
+
const participatesInValue = valueHandling !== 'exclude' && valueHandling !== 'flatten';
|
|
9239
|
+
if (collectKeys && field.key && participatesInValue) {
|
|
9240
|
+
const scopedKey = pathPrefix ? `${pathPrefix}.${field.key}` : field.key;
|
|
9241
|
+
data.keys.push(scopedKey);
|
|
9242
|
+
// Project to the DOM-ID format (dots → underscores) so we can detect
|
|
9243
|
+
// collisions like top-level `'foo_bar'` vs group `'foo'` + leaf `'bar'`.
|
|
9244
|
+
data.domIds.push(scopedKey.replace(/\./g, '_'));
|
|
9091
9245
|
}
|
|
9092
9246
|
if (field.type) {
|
|
9093
9247
|
data.types.add(field.type);
|
|
@@ -9107,13 +9261,16 @@ function collectFieldData(fields, data, collectKeys = true) {
|
|
|
9107
9261
|
// Array item fields share template keys across items by design — stop collecting keys
|
|
9108
9262
|
// once inside an array. Types and regex patterns are still validated.
|
|
9109
9263
|
const childCollectKeys = collectKeys && field.type !== 'array';
|
|
9264
|
+
// Only group containers extend the scoping path — page/row flatten into the parent
|
|
9265
|
+
// scope, and array items use dynamic indices that aren't statically knowable.
|
|
9266
|
+
const childPrefix = isGroupField(field) && field.key ? (pathPrefix ? `${pathPrefix}.${field.key}` : field.key) : pathPrefix;
|
|
9110
9267
|
const children = normalizeFieldsArray(field.fields);
|
|
9111
9268
|
for (const child of children) {
|
|
9112
9269
|
if (Array.isArray(child)) {
|
|
9113
|
-
collectFieldData(child, data, childCollectKeys);
|
|
9270
|
+
collectFieldData(child, data, registry, childPrefix, childCollectKeys);
|
|
9114
9271
|
}
|
|
9115
9272
|
else {
|
|
9116
|
-
collectFieldData([child], data, childCollectKeys);
|
|
9273
|
+
collectFieldData([child], data, registry, childPrefix, childCollectKeys);
|
|
9117
9274
|
}
|
|
9118
9275
|
}
|
|
9119
9276
|
}
|
|
@@ -9149,7 +9306,38 @@ function validateNoDuplicateKeys(allKeys) {
|
|
|
9149
9306
|
const duplicateList = Array.from(duplicates)
|
|
9150
9307
|
.map((k) => `'${k}'`)
|
|
9151
9308
|
.join(', ');
|
|
9152
|
-
throw new DynamicFormError(`Duplicate field keys detected: ${duplicateList}.
|
|
9309
|
+
throw new DynamicFormError(`Duplicate field keys detected: ${duplicateList}. Field keys must be unique within their group scope (the same key may appear inside different groups).`);
|
|
9310
|
+
}
|
|
9311
|
+
}
|
|
9312
|
+
/**
|
|
9313
|
+
* Validates that the underscore-projected DOM-ID namespace has no collisions.
|
|
9314
|
+
*
|
|
9315
|
+
* DOM IDs are built by underscore-joining the group ancestor path with the
|
|
9316
|
+
* leaf key (e.g. `address_street`). Two scoped paths can land on the same
|
|
9317
|
+
* DOM ID even when their form-value paths differ — e.g. a top-level key
|
|
9318
|
+
* `'foo_bar'` and a leaf `'bar'` inside group `'foo'` both render as
|
|
9319
|
+
* `id="foo_bar"`. The form-value-path check accepts this (paths are
|
|
9320
|
+
* `'foo_bar'` vs `'foo.bar'` — distinct) but the rendered DOM would have
|
|
9321
|
+
* duplicate IDs, breaking aria associations and CSS selectors.
|
|
9322
|
+
*
|
|
9323
|
+
* @throws {DynamicFormError} When two scoped paths project to the same DOM ID
|
|
9324
|
+
*/
|
|
9325
|
+
function validateNoDomIdCollisions(domIds) {
|
|
9326
|
+
const seen = new Set();
|
|
9327
|
+
const duplicates = new Set();
|
|
9328
|
+
for (const id of domIds) {
|
|
9329
|
+
if (seen.has(id)) {
|
|
9330
|
+
duplicates.add(id);
|
|
9331
|
+
}
|
|
9332
|
+
seen.add(id);
|
|
9333
|
+
}
|
|
9334
|
+
if (duplicates.size > 0) {
|
|
9335
|
+
const duplicateList = Array.from(duplicates)
|
|
9336
|
+
.map((k) => `'${k}'`)
|
|
9337
|
+
.join(', ');
|
|
9338
|
+
throw new DynamicFormError(`DOM id collision detected for: ${duplicateList}. Group-nested keys are joined with '_' to form DOM ids ` +
|
|
9339
|
+
`(e.g. group 'foo' + leaf 'bar' renders as id='foo_bar'). A separate top-level key like 'foo_bar' or a different ` +
|
|
9340
|
+
`group/leaf pair that underscores to the same string will produce duplicate ids. Rename one of the colliding keys.`);
|
|
9153
9341
|
}
|
|
9154
9342
|
}
|
|
9155
9343
|
/**
|
|
@@ -9187,11 +9375,13 @@ function validateFieldTypesRegistered(allTypes, registry, logger) {
|
|
|
9187
9375
|
function validateFormConfig(fields, registry, logger) {
|
|
9188
9376
|
const data = {
|
|
9189
9377
|
keys: [],
|
|
9378
|
+
domIds: [],
|
|
9190
9379
|
types: new Set(),
|
|
9191
9380
|
regexErrors: [],
|
|
9192
9381
|
};
|
|
9193
|
-
collectFieldData(fields, data);
|
|
9382
|
+
collectFieldData(fields, data, registry, '');
|
|
9194
9383
|
validateNoDuplicateKeys(data.keys);
|
|
9384
|
+
validateNoDomIdCollisions(data.domIds);
|
|
9195
9385
|
validateFieldTypesRegistered(data.types, registry, logger);
|
|
9196
9386
|
if (data.regexErrors.length > 0) {
|
|
9197
9387
|
throw new DynamicFormError(data.regexErrors.length === 1
|
|
@@ -11036,15 +11226,18 @@ function computeValueFromEntry(entry, evalContext, options) {
|
|
|
11036
11226
|
if (entry.expression) {
|
|
11037
11227
|
return ExpressionParser.evaluate(entry.expression, evalContext);
|
|
11038
11228
|
}
|
|
11039
|
-
if (entry.functionName) {
|
|
11040
|
-
|
|
11229
|
+
if (entry.fn || entry.functionName) {
|
|
11230
|
+
if (entry.fn && entry.functionName) {
|
|
11231
|
+
evalContext.logger.warn(`Both "fn" and "functionName" are set on derivation for '${options.subject}'. Inline "fn" takes precedence; "functionName" is ignored.`);
|
|
11232
|
+
}
|
|
11233
|
+
const fn = entry.fn ?? (entry.functionName ? options.derivationFunctions?.[entry.functionName] : undefined);
|
|
11041
11234
|
if (!fn) {
|
|
11042
11235
|
const kind = options.functionKind ?? 'Derivation function';
|
|
11043
11236
|
throw new DynamicFormError(`${kind} '${entry.functionName}' not found in customFnConfig.derivations`);
|
|
11044
11237
|
}
|
|
11045
11238
|
return fn(evalContext);
|
|
11046
11239
|
}
|
|
11047
|
-
throw new DynamicFormError(`Derivation for ${options.subject} has no value source. Specify 'value', 'expression', or '
|
|
11240
|
+
throw new DynamicFormError(`Derivation for ${options.subject} has no value source. Specify 'value', 'expression', 'functionName', or 'fn'.`);
|
|
11048
11241
|
}
|
|
11049
11242
|
|
|
11050
11243
|
/**
|
|
@@ -11447,7 +11640,7 @@ function getEntriesForChangedFields$1(entries, changedFields) {
|
|
|
11447
11640
|
function tryApplyDerivation(entry, context, chainContext) {
|
|
11448
11641
|
const { derivationLogger } = context;
|
|
11449
11642
|
// HTTP and async entries are processed asynchronously in their own streams — skip in sync loop
|
|
11450
|
-
if (entry.http || entry.asyncFunctionName) {
|
|
11643
|
+
if (entry.http || entry.asyncFunctionName || entry.asyncFn) {
|
|
11451
11644
|
return { applied: false, fieldKey: entry.fieldKey };
|
|
11452
11645
|
}
|
|
11453
11646
|
// Check if this is an array field derivation (has '$' placeholder)
|
|
@@ -11754,9 +11947,11 @@ function formatDerivationError(entry, phase, message) {
|
|
|
11754
11947
|
? `expression: "${entry.expression.substring(0, 50)}${entry.expression.length > 50 ? '...' : ''}"`
|
|
11755
11948
|
: entry.functionName
|
|
11756
11949
|
? `function: "${entry.functionName}"`
|
|
11757
|
-
: entry.
|
|
11758
|
-
? '
|
|
11759
|
-
:
|
|
11950
|
+
: entry.fn
|
|
11951
|
+
? 'inline fn'
|
|
11952
|
+
: entry.value !== undefined
|
|
11953
|
+
? 'static value'
|
|
11954
|
+
: 'unknown source';
|
|
11760
11955
|
return `${ERROR_PREFIX$1} Failed to ${phase} derivation (field: ${entry.fieldKey}, ${source}): ${message}`;
|
|
11761
11956
|
}
|
|
11762
11957
|
/**
|
|
@@ -11825,7 +12020,7 @@ function shouldSkipForUserOverride(entry, resolvedFieldKey, context, chainContex
|
|
|
11825
12020
|
function applyDerivationsForTrigger(collection, trigger, context, changedFields) {
|
|
11826
12021
|
// Filter entries by trigger type, excluding HTTP and async entries (processed in async streams)
|
|
11827
12022
|
const filteredEntries = collection.entries.filter((entry) => {
|
|
11828
|
-
if (entry.http || entry.asyncFunctionName)
|
|
12023
|
+
if (entry.http || entry.asyncFunctionName || entry.asyncFn)
|
|
11829
12024
|
return false;
|
|
11830
12025
|
if (trigger === 'onChange') {
|
|
11831
12026
|
return !entry.trigger || entry.trigger === 'onChange';
|
|
@@ -11994,7 +12189,7 @@ function topologicalSort(entries) {
|
|
|
11994
12189
|
* Combines:
|
|
11995
12190
|
* - Explicit `dependsOn` (when provided, it takes precedence over auto-detection)
|
|
11996
12191
|
* - Expression-detected dependencies (only used when `dependsOn` is empty)
|
|
11997
|
-
* - `'*'` wildcard for `functionName` (only when `dependsOn` is empty)
|
|
12192
|
+
* - `'*'` wildcard for `functionName` / inline `fn` (only when `dependsOn` is empty)
|
|
11998
12193
|
* - Condition expression dependencies (always included)
|
|
11999
12194
|
*
|
|
12000
12195
|
* @internal
|
|
@@ -12008,7 +12203,9 @@ function extractDependenciesFromConfig(config) {
|
|
|
12008
12203
|
if (config.expression) {
|
|
12009
12204
|
extractStringDependencies(config.expression).forEach((dep) => deps.add(dep));
|
|
12010
12205
|
}
|
|
12011
|
-
|
|
12206
|
+
// Inline `fn` mirrors `functionName` semantics: without explicit dependsOn,
|
|
12207
|
+
// assume the function may depend on anything in the form.
|
|
12208
|
+
if (config.functionName || config.fn) {
|
|
12012
12209
|
deps.add('*');
|
|
12013
12210
|
}
|
|
12014
12211
|
}
|
|
@@ -12018,6 +12215,32 @@ function extractDependenciesFromConfig(config) {
|
|
|
12018
12215
|
return Array.from(deps);
|
|
12019
12216
|
}
|
|
12020
12217
|
|
|
12218
|
+
/**
|
|
12219
|
+
* Reads the optional inline `fn` property off a derivation config.
|
|
12220
|
+
*
|
|
12221
|
+
* `fn` only appears on the function-mode branch of the discriminated union, so
|
|
12222
|
+
* call sites that need to copy it through unconditionally must widen via cast.
|
|
12223
|
+
* This helper centralises the cast and keeps it read-only — no widening, no
|
|
12224
|
+
* mutation. Returns `undefined` for any non-function-mode variant.
|
|
12225
|
+
*
|
|
12226
|
+
* @internal
|
|
12227
|
+
*/
|
|
12228
|
+
function extractInlineFn(config) {
|
|
12229
|
+
return config.fn;
|
|
12230
|
+
}
|
|
12231
|
+
/**
|
|
12232
|
+
* Reads the optional inline `asyncFn` property off a derivation config.
|
|
12233
|
+
*
|
|
12234
|
+
* Same rationale as {@link extractInlineFn}: `asyncFn` only appears on the
|
|
12235
|
+
* `source: 'asyncFunction'` branch, so collectors that copy it through need a
|
|
12236
|
+
* cast. Read-only access, no widening.
|
|
12237
|
+
*
|
|
12238
|
+
* @internal
|
|
12239
|
+
*/
|
|
12240
|
+
function extractInlineAsyncFn(config) {
|
|
12241
|
+
return config.asyncFn;
|
|
12242
|
+
}
|
|
12243
|
+
|
|
12021
12244
|
/**
|
|
12022
12245
|
* Recursively walks a field-definition tree, invoking `visitor` for each field.
|
|
12023
12246
|
*
|
|
@@ -12360,9 +12583,11 @@ function createLogicEntry(fieldKey, config, context) {
|
|
|
12360
12583
|
value: config.value,
|
|
12361
12584
|
expression: config.expression,
|
|
12362
12585
|
functionName: config.functionName,
|
|
12586
|
+
fn: extractInlineFn(config),
|
|
12363
12587
|
http: config.http,
|
|
12364
12588
|
responseExpression: config.responseExpression,
|
|
12365
12589
|
asyncFunctionName: config.asyncFunctionName,
|
|
12590
|
+
asyncFn: extractInlineAsyncFn(config),
|
|
12366
12591
|
trigger,
|
|
12367
12592
|
debounceMs,
|
|
12368
12593
|
isShorthand: false,
|
|
@@ -12911,9 +13136,16 @@ function createAsyncDerivationStream(entry, formValue$, context) {
|
|
|
12911
13136
|
`Array async derivations will be supported in a future release.`);
|
|
12912
13137
|
return EMPTY;
|
|
12913
13138
|
}
|
|
12914
|
-
if (!entry.asyncFunctionName) {
|
|
13139
|
+
if (!entry.asyncFunctionName && !entry.asyncFn) {
|
|
13140
|
+
context.logger.warn(`${LOG_PREFIX} Derivation for '${entry.fieldKey}' has neither "asyncFunctionName" nor "asyncFn". ` +
|
|
13141
|
+
`One of the two is required for source: 'asyncFunction'.`);
|
|
12915
13142
|
return EMPTY;
|
|
12916
13143
|
}
|
|
13144
|
+
if (entry.asyncFn && entry.asyncFunctionName) {
|
|
13145
|
+
context.logger.warn(`${LOG_PREFIX} Both "asyncFn" and "asyncFunctionName" are set on derivation for '${entry.fieldKey}'. ` +
|
|
13146
|
+
`Inline "asyncFn" takes precedence; "asyncFunctionName" is ignored.`);
|
|
13147
|
+
}
|
|
13148
|
+
const inlineAsyncFn = entry.asyncFn;
|
|
12917
13149
|
const asyncFunctionName = entry.asyncFunctionName;
|
|
12918
13150
|
const debounceMs = entry.debounceMs ?? DEFAULT_ASYNC_DEBOUNCE_MS;
|
|
12919
13151
|
// startWith(null) → pairwise() causes all fields to appear "changed" on the
|
|
@@ -12959,8 +13191,8 @@ function createAsyncDerivationStream(entry, formValue$, context) {
|
|
|
12959
13191
|
const customFunctions = context.customFunctions?.();
|
|
12960
13192
|
const asyncDerivationFunctions = context.asyncDerivationFunctions?.();
|
|
12961
13193
|
const externalData = context.externalData?.();
|
|
12962
|
-
//
|
|
12963
|
-
const asyncFn = asyncDerivationFunctions?.[asyncFunctionName];
|
|
13194
|
+
// Inline `asyncFn` wins; otherwise look up the registered function.
|
|
13195
|
+
const asyncFn = inlineAsyncFn ?? (asyncFunctionName ? asyncDerivationFunctions?.[asyncFunctionName] : undefined);
|
|
12964
13196
|
if (!asyncFn) {
|
|
12965
13197
|
context.logger.warn(`${LOG_PREFIX} Async derivation function '${asyncFunctionName}' not found for field '${entry.fieldKey}'. ` +
|
|
12966
13198
|
`Register it in customFnConfig.asyncDerivations.`);
|
|
@@ -13388,7 +13620,7 @@ class DerivationOrchestrator {
|
|
|
13388
13620
|
toObservable(this.derivationCollection, { injector: this.injector })
|
|
13389
13621
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
13390
13622
|
.subscribe((collection) => {
|
|
13391
|
-
const asyncEntries = collection?.entries.filter((entry) => entry.asyncFunctionName) ?? [];
|
|
13623
|
+
const asyncEntries = collection?.entries.filter((entry) => entry.asyncFunctionName || entry.asyncFn) ?? [];
|
|
13392
13624
|
if (asyncEntries.length === 0) {
|
|
13393
13625
|
this.teardownAsyncFunctionStreams();
|
|
13394
13626
|
this.lastAsyncEntryKeys = null;
|
|
@@ -13435,9 +13667,14 @@ class DerivationOrchestrator {
|
|
|
13435
13667
|
* Used for smart teardown comparison.
|
|
13436
13668
|
*/
|
|
13437
13669
|
computeAsyncEntryKeys(entries) {
|
|
13670
|
+
// For inline `asyncFn` entries we tag the key with the field path (the real
|
|
13671
|
+
// identity, since derivations are self-targeting) rather than the array
|
|
13672
|
+
// index. Index-based tagging churned identities whenever topologicalSort
|
|
13673
|
+
// reordered entries, causing unnecessary teardown + re-subscribe.
|
|
13438
13674
|
return new Set(entries.map((entry) => {
|
|
13439
13675
|
const config = {
|
|
13440
13676
|
asyncFunctionName: entry.asyncFunctionName,
|
|
13677
|
+
asyncFnId: entry.asyncFn ? `inline:${entry.fieldKey}` : undefined,
|
|
13441
13678
|
dependsOn: entry.dependsOn,
|
|
13442
13679
|
debounceMs: entry.debounceMs,
|
|
13443
13680
|
stopOnUserOverride: entry.stopOnUserOverride,
|
|
@@ -13486,10 +13723,11 @@ function warnAboutWildcardDependencies$1(logger, entries, fieldCount) {
|
|
|
13486
13723
|
// HTTP and async entries require explicit dependsOn (validated at collection time)
|
|
13487
13724
|
const implicitWildcards = wildcardEntries.filter((entry) => !entry.http &&
|
|
13488
13725
|
!entry.asyncFunctionName &&
|
|
13489
|
-
entry.
|
|
13726
|
+
!entry.asyncFn &&
|
|
13727
|
+
(entry.functionName || entry.fn) &&
|
|
13490
13728
|
(!entry.originalConfig?.dependsOn || entry.originalConfig.dependsOn.length === 0));
|
|
13491
13729
|
if (implicitWildcards.length > 0) {
|
|
13492
|
-
const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey} (${e.functionName})`);
|
|
13730
|
+
const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey} (${e.functionName ?? '<inline fn>'})`);
|
|
13493
13731
|
logger.warn('Derivation: custom functions without explicit dependsOn detected. ' +
|
|
13494
13732
|
`These run on EVERY form change, which may impact performance (form has ${fieldCount} fields). ` +
|
|
13495
13733
|
'Consider specifying explicit dependsOn arrays for better performance.', derivationDescs);
|
|
@@ -13972,8 +14210,16 @@ function collectPropertyDerivations(fields, logger, tracker) {
|
|
|
13972
14210
|
const entries = [];
|
|
13973
14211
|
const context = { logger, tracker };
|
|
13974
14212
|
traverseFieldsWithContext(fields, context, (field, ctx) => collectFromField(field, entries, ctx), {
|
|
13975
|
-
|
|
13976
|
-
//
|
|
14213
|
+
// Array boundary: each item is its own model root. Reset groupPath so
|
|
14214
|
+
// ancestor groups OUTSIDE the array don't bleed into per-item keys.
|
|
14215
|
+
onArrayChild: (_, field) => ({ arrayPath: field.key, groupPath: undefined }),
|
|
14216
|
+
onGroupChild: (parent, field) => {
|
|
14217
|
+
// Keyless groups (rare) act like layout containers — pass through.
|
|
14218
|
+
if (!field.key)
|
|
14219
|
+
return {};
|
|
14220
|
+
return { groupPath: parent.groupPath ? `${parent.groupPath}.${field.key}` : field.key };
|
|
14221
|
+
},
|
|
14222
|
+
// Layout containers (page, row, container) leave context unchanged.
|
|
13977
14223
|
});
|
|
13978
14224
|
return { entries };
|
|
13979
14225
|
}
|
|
@@ -14005,7 +14251,7 @@ function collectFromField(field, entries, context) {
|
|
|
14005
14251
|
* @internal
|
|
14006
14252
|
*/
|
|
14007
14253
|
function createPropertyDerivationEntryFromDerivation(fieldKey, config, context) {
|
|
14008
|
-
const effectiveFieldKey = buildPropertyOverrideKey(context.arrayPath, context.arrayPath ? PLACEHOLDER_INDEX : undefined, fieldKey);
|
|
14254
|
+
const effectiveFieldKey = buildPropertyOverrideKey(context.arrayPath, context.arrayPath ? PLACEHOLDER_INDEX : undefined, fieldKey, context.groupPath);
|
|
14009
14255
|
const dependsOn = extractDependenciesFromConfig(config);
|
|
14010
14256
|
const condition = config.condition ?? true;
|
|
14011
14257
|
const trigger = config.trigger ?? 'onChange';
|
|
@@ -14018,6 +14264,7 @@ function createPropertyDerivationEntryFromDerivation(fieldKey, config, context)
|
|
|
14018
14264
|
value: config.value,
|
|
14019
14265
|
expression: config.expression,
|
|
14020
14266
|
functionName: config.functionName,
|
|
14267
|
+
fn: extractInlineFn(config),
|
|
14021
14268
|
trigger,
|
|
14022
14269
|
debounceMs,
|
|
14023
14270
|
debugName: config.debugName,
|
|
@@ -14172,10 +14419,10 @@ const PROPERTY_DERIVATION_ORCHESTRATOR = new InjectionToken('PROPERTY_DERIVATION
|
|
|
14172
14419
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14173
14420
|
function warnAboutWildcardDependencies(logger, entries, fieldCount) {
|
|
14174
14421
|
const implicitWildcards = entries.filter((entry) => entry.dependsOn.includes('*') &&
|
|
14175
|
-
entry.functionName &&
|
|
14422
|
+
(entry.functionName || entry.fn) &&
|
|
14176
14423
|
(!entry.originalConfig?.dependsOn || entry.originalConfig.dependsOn.length === 0));
|
|
14177
14424
|
if (implicitWildcards.length > 0) {
|
|
14178
|
-
const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey}.${e.targetProperty} (${e.functionName})`);
|
|
14425
|
+
const derivationDescs = implicitWildcards.map((e) => `${e.fieldKey}.${e.targetProperty} (${e.functionName ?? '<inline fn>'})`);
|
|
14179
14426
|
logger.warn('PropertyDerivation: custom functions without explicit dependsOn detected. ' +
|
|
14180
14427
|
`These run on EVERY form change, which may impact performance (form has ${fieldCount} fields). ` +
|
|
14181
14428
|
'Consider specifying explicit dependsOn arrays for better performance.', derivationDescs);
|
|
@@ -15268,31 +15515,31 @@ const BUILT_IN_FIELDS = [
|
|
|
15268
15515
|
// `row` is a virtual field type: it maps to ContainerFieldComponent, and
|
|
15269
15516
|
// the rowFieldMapper injects a `{ type: 'row' }` wrapper so the container
|
|
15270
15517
|
// renders the flex/grid layout via RowWrapperComponent.
|
|
15271
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-
|
|
15518
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-Pcxg5PQu.mjs'),
|
|
15272
15519
|
mapper: rowFieldMapper,
|
|
15273
15520
|
valueHandling: 'flatten',
|
|
15274
15521
|
},
|
|
15275
15522
|
{
|
|
15276
15523
|
name: 'group',
|
|
15277
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-
|
|
15524
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-fzKwPrQr.mjs'),
|
|
15278
15525
|
mapper: groupFieldMapper,
|
|
15279
15526
|
valueHandling: 'include',
|
|
15280
15527
|
},
|
|
15281
15528
|
{
|
|
15282
15529
|
name: 'array',
|
|
15283
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-
|
|
15530
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-DJ-LiLm_.mjs'),
|
|
15284
15531
|
mapper: arrayFieldMapper,
|
|
15285
15532
|
valueHandling: 'include',
|
|
15286
15533
|
},
|
|
15287
15534
|
{
|
|
15288
15535
|
name: 'page',
|
|
15289
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-
|
|
15536
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs'),
|
|
15290
15537
|
mapper: pageFieldMapper,
|
|
15291
15538
|
valueHandling: 'flatten',
|
|
15292
15539
|
},
|
|
15293
15540
|
{
|
|
15294
15541
|
name: 'text',
|
|
15295
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-
|
|
15542
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-B9SK8Vun.mjs'),
|
|
15296
15543
|
mapper: textFieldMapper,
|
|
15297
15544
|
valueHandling: 'exclude',
|
|
15298
15545
|
...DISPLAY_FIELD_TYPES_BASE,
|
|
@@ -15304,7 +15551,7 @@ const BUILT_IN_FIELDS = [
|
|
|
15304
15551
|
},
|
|
15305
15552
|
{
|
|
15306
15553
|
name: 'container',
|
|
15307
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-
|
|
15554
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-Pcxg5PQu.mjs'),
|
|
15308
15555
|
mapper: containerFieldMapper,
|
|
15309
15556
|
valueHandling: 'flatten',
|
|
15310
15557
|
},
|
|
@@ -15312,7 +15559,7 @@ const BUILT_IN_FIELDS = [
|
|
|
15312
15559
|
const BUILT_IN_WRAPPERS = [
|
|
15313
15560
|
{
|
|
15314
15561
|
wrapperName: 'css',
|
|
15315
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-
|
|
15562
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-CJACCIEK.mjs'),
|
|
15316
15563
|
},
|
|
15317
15564
|
{
|
|
15318
15565
|
wrapperName: 'row',
|
|
@@ -16386,5 +16633,5 @@ function extractErrorParams(error) {
|
|
|
16386
16633
|
* Generated bundle index. Do not edit.
|
|
16387
16634
|
*/
|
|
16388
16635
|
|
|
16389
|
-
export {
|
|
16390
|
-
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-
|
|
16636
|
+
export { ShiftArrayItemEvent as $, ARRAY_CONTEXT as A, BUILT_IN_FIELDS as B, CONTAINER_FIELD_PROCESSORS as C, DynamicFormLogger as D, EventBus as E, FIELD_SIGNAL_CONTEXT as F, GROUP_CONTEXT as G, DEFAULT_VALIDATION_MESSAGES as H, DynamicForm as I, DynamicFormError as J, EventDispatcher as K, FIELD_REGISTRY as L, FORM_OPTIONS as M, FormClearEvent as N, FormResetEvent as O, INITIALIZATION_TIMEOUT_MS as P, InsertArrayItemEvent as Q, RootFormRegistryService as R, MoveArrayItemEvent as S, NextPageEvent as T, NoopLogger as U, PageChangeEvent as V, WRAPPER_AUTO_ASSOCIATIONS as W, PopArrayItemEvent as X, PrependArrayItemEvent as Y, PreviousPageEvent as Z, RemoveAtIndexEvent as _, DEFAULT_WRAPPERS as a, SubmitEvent as a0, WRAPPER_REGISTRY as a1, applyMetaToElement as a2, applyValidator as a3, applyValidators as a4, arrayEvent as a5, arrayFieldMapper as a6, baseFieldMapper as a7, buildBaseInputs as a8, containerFieldMapper as a9, provideDynamicForm as aA, resolveNextButtonDisabled as aB, resolveNonFieldDisabled as aC, resolveNonFieldHidden as aD, resolveSubmitButtonDisabled as aE, resolveTokens as aF, rowFieldMapper as aG, textFieldMapper as aH, toReadonlyFieldTree as aI, withEventFormValue as aJ, withLoggerConfig as aK, withPreviousValue as aL, withValidationExecutionDefaults as aM, withValueExclusionDefaults as aN, wrapperProps as aO, createField as aa, createWrappers as ab, evaluateNonFieldDisabled as ac, evaluateNonFieldHidden as ad, field as ae, formConfig as af, getArrayLength as ag, groupFieldMapper as ah, hasFormValue as ai, interpolateParams as aj, isArrayField as ak, isCheckedField as al, isContainerField as am, isContainerTypedField as an, isDisplayOnlyField as ao, isFormStateCondition as ap, isLeafField as aq, isPageField as ar, isRowField as as, isSimplifiedArrayField as at, isValueBearingField as au, isValueField as av, isWrapperTypeDefinition as aw, isWrappersBundle as ax, omit as ay, pageFieldMapper as az, isSameWrapperChain as b, createFieldResolutionPipe as c, derivedFromDeferred as d, createWrapperChainController as e, DfFieldOutlet as f, isEqual as g, createSchemaFromFields as h, injectFieldRegistry as i, createRenderReadySignal as j, ARRAY_TEMPLATE_REGISTRY as k, ARRAY_ITEM_ID_GENERATOR as l, mapFieldToInputs as m, getNormalizedArrayMetadata as n, getFieldValueHandling as o, getFieldDefaultValue as p, isGroupField as q, resolveWrappers as r, createArrayItemIdGenerator as s, ComponentInitializedEvent as t, DynamicTextPipe as u, validatePageNesting as v, dynamicTextToObservable as w, AppendArrayItemEvent as x, ConsoleLogger as y, DEFAULT_PROPS as z };
|
|
16637
|
+
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-o3rJX4vG.mjs.map
|