@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.
Files changed (24) hide show
  1. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs → ng-forge-dynamic-forms-array-field.component-DJ-LiLm_.mjs} +17 -4
  2. package/fesm2022/ng-forge-dynamic-forms-array-field.component-DJ-LiLm_.mjs.map +1 -0
  3. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs → ng-forge-dynamic-forms-container-field.component-Pcxg5PQu.mjs} +3 -3
  4. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs.map → ng-forge-dynamic-forms-container-field.component-Pcxg5PQu.mjs.map} +1 -1
  5. package/fesm2022/{ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs → ng-forge-dynamic-forms-container-utils-a4RstR2Z.mjs} +2 -2
  6. package/fesm2022/{ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs.map → ng-forge-dynamic-forms-container-utils-a4RstR2Z.mjs.map} +1 -1
  7. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs → ng-forge-dynamic-forms-css-wrapper.component-CJACCIEK.mjs} +2 -2
  8. 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
  9. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs → ng-forge-dynamic-forms-emit-initialization-Bjf_I0D_.mjs} +2 -2
  10. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs.map → ng-forge-dynamic-forms-emit-initialization-Bjf_I0D_.mjs.map} +1 -1
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs → ng-forge-dynamic-forms-group-field.component-fzKwPrQr.mjs} +24 -4
  12. package/fesm2022/ng-forge-dynamic-forms-group-field.component-fzKwPrQr.mjs.map +1 -0
  13. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-o3rJX4vG.mjs} +328 -81
  14. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-o3rJX4vG.mjs.map +1 -0
  15. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs → ng-forge-dynamic-forms-page-field.component-BX4R9GGa.mjs} +3 -3
  16. 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
  17. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs → ng-forge-dynamic-forms-text-field.component-B9SK8Vun.mjs} +2 -2
  18. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs.map → ng-forge-dynamic-forms-text-field.component-B9SK8Vun.mjs.map} +1 -1
  19. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  20. package/package.json +1 -1
  21. package/types/ng-forge-dynamic-forms.d.ts +1827 -1608
  22. package/fesm2022/ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs.map +0 -1
  23. package/fesm2022/ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs.map +0 -1
  24. 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
- const customFn = context.customFunctions?.[expression.functionName];
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:', expression.functionName);
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:', expression.functionName, error);
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('[Dynamic Forms] HTTP conditions are resolved asynchronously via createHttpConditionLogicFunction(). ' +
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-B9v_1shD.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs').then(m => m.default)]] });
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-B9v_1shD.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
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
- return 'functionName' in config && !!config.functionName;
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
- // Check function cache
5051
- const cacheKey = stableStringify(condition);
5052
- const cached = cacheService.asyncConditionFunctionCache.get(cacheKey);
5053
- if (cached) {
5054
- return cached;
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
- // Look up the async function at call time (fresh reference)
5080
- const asyncFn = functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName);
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
- cacheService.asyncConditionFunctionCache.set(cacheKey, fn);
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 either "expression" or "functionName"');
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
- const registry = inject(FunctionRegistryService);
5845
- const validatorConfig = registry.getAsyncValidator(config.functionName);
5846
- if (!validatorConfig) {
5847
- throw new DynamicFormError(`Async validator "${config.functionName}" not found. Register it with customFnConfig.asyncValidators.`);
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
- else {
5871
- applyDeclarativeHttpValidator(config, fieldPath);
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
- const registry = inject(FunctionRegistryService);
5884
- const httpValidatorConfig = registry.getHttpValidator(config.functionName);
5885
- if (!httpValidatorConfig) {
5886
- throw new DynamicFormError(`HTTP validator "${config.functionName}" not found. Register it with customFnConfig.httpValidators.`);
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 just the field's key.
8471
- * For array item fields, the key includes the array path and either a concrete
8472
- * index (e.g., `contacts.0.email`) or the `$` placeholder (e.g., `contacts.$.email`).
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
- * @returns The store key (e.g., 'email', 'contacts.0.email', or 'contacts.$.email')
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}.${fieldKey}`;
8580
+ return `${arrayKey}.${index}.${leafScope}`;
8484
8581
  }
8485
- return fieldKey;
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 the store key inside computed() so index signal read establishes reactive dependency
8638
- // Safe to access arrayContext/indexSignal directly — hasArrayContext already confirmed they exist
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
- * @param collectKeys - Whether to add field keys to data.keys for duplicate detection.
9083
- * Set to false when inside an array container: array item fields share template keys
9084
- * across items by design (e.g. every item has 'name', 'email'), so they must not
9085
- * participate in global duplicate-key checking.
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
- if (collectKeys && field.key) {
9090
- data.keys.push(field.key);
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}. Each field key must be unique within a form config.`);
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
- const fn = options.derivationFunctions?.[entry.functionName];
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 'functionName'.`);
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.value !== undefined
11758
- ? 'static value'
11759
- : 'unknown source';
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
- if (config.functionName) {
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
- // Look up the async function
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.functionName &&
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
- onArrayChild: (_, field) => ({ arrayPath: field.key }),
13976
- // Group and layout containers do not affect property-derivation paths.
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-CMhqFGJZ.mjs'),
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-BhYFIDA1.mjs'),
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-DbWes42V.mjs'),
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-B9v_1shD.mjs'),
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-B4LnTnIA.mjs'),
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-CMhqFGJZ.mjs'),
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-htvP_o95.mjs'),
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 { SubmitEvent 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, DEFAULT_VALIDATION_MESSAGES as G, DynamicForm as H, DynamicFormError as I, EventDispatcher as J, FIELD_REGISTRY as K, FORM_OPTIONS as L, FormClearEvent as M, FormResetEvent as N, INITIALIZATION_TIMEOUT_MS as O, InsertArrayItemEvent as P, MoveArrayItemEvent as Q, RootFormRegistryService as R, NextPageEvent as S, NoopLogger as T, PageChangeEvent as U, PopArrayItemEvent as V, WRAPPER_AUTO_ASSOCIATIONS as W, PrependArrayItemEvent as X, PreviousPageEvent as Y, RemoveAtIndexEvent as Z, ShiftArrayItemEvent as _, DEFAULT_WRAPPERS as a, WRAPPER_REGISTRY as a0, applyMetaToElement as a1, applyValidator as a2, applyValidators as a3, arrayEvent as a4, arrayFieldMapper as a5, baseFieldMapper as a6, buildBaseInputs as a7, containerFieldMapper as a8, createField as a9, resolveNextButtonDisabled as aA, resolveNonFieldDisabled as aB, resolveNonFieldHidden as aC, resolveSubmitButtonDisabled as aD, resolveTokens as aE, rowFieldMapper as aF, textFieldMapper as aG, toReadonlyFieldTree as aH, withEventFormValue as aI, withLoggerConfig as aJ, withPreviousValue as aK, withValidationExecutionDefaults as aL, withValueExclusionDefaults as aM, wrapperProps as aN, createWrappers as aa, evaluateNonFieldDisabled as ab, evaluateNonFieldHidden as ac, field as ad, formConfig as ae, getArrayLength as af, groupFieldMapper as ag, hasFormValue as ah, interpolateParams as ai, isArrayField as aj, isCheckedField as ak, isContainerField as al, isContainerTypedField as am, isDisplayOnlyField as an, isFormStateCondition as ao, isLeafField as ap, isPageField as aq, isRowField as ar, isSimplifiedArrayField as as, isValueBearingField as at, isValueField as au, isWrapperTypeDefinition as av, isWrappersBundle as aw, omit as ax, pageFieldMapper as ay, provideDynamicForm 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 };
16390
- //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs.map
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