@ng-forge/dynamic-forms 0.8.0-next.4 → 0.8.0-next.5

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 (28) hide show
  1. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-CJ_OclbR.mjs → ng-forge-dynamic-forms-array-field.component-DWhbvZUO.mjs} +3 -3
  2. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-CJ_OclbR.mjs.map → ng-forge-dynamic-forms-array-field.component-DWhbvZUO.mjs.map} +1 -1
  3. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-DQ_OQLtx.mjs → ng-forge-dynamic-forms-container-field.component-ByKgY4-b.mjs} +4 -9
  4. package/fesm2022/ng-forge-dynamic-forms-container-field.component-ByKgY4-b.mjs.map +1 -0
  5. package/fesm2022/{ng-forge-dynamic-forms-container-utils-zQSTe7Jk.mjs → ng-forge-dynamic-forms-container-utils-DXRcEEkE.mjs} +4 -5
  6. package/fesm2022/ng-forge-dynamic-forms-container-utils-DXRcEEkE.mjs.map +1 -0
  7. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-2fdmbtL7.mjs → ng-forge-dynamic-forms-css-wrapper.component-ClFdxZV4.mjs} +2 -2
  8. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-2fdmbtL7.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-ClFdxZV4.mjs.map} +1 -1
  9. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-pCNwBRRm.mjs → ng-forge-dynamic-forms-emit-initialization-CTpgJ-02.mjs} +3 -3
  10. package/fesm2022/ng-forge-dynamic-forms-emit-initialization-CTpgJ-02.mjs.map +1 -0
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BdLdSEHj.mjs → ng-forge-dynamic-forms-group-field.component-clrQsUyY.mjs} +3 -3
  12. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BdLdSEHj.mjs.map → ng-forge-dynamic-forms-group-field.component-clrQsUyY.mjs.map} +1 -1
  13. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +3 -4
  14. package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
  15. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-BdMxsqmk.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-Bw9Bt0Ev.mjs} +492 -394
  16. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-Bw9Bt0Ev.mjs.map +1 -0
  17. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-C5rUo80O.mjs → ng-forge-dynamic-forms-page-field.component-XtyjFlJi.mjs} +3 -3
  18. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-C5rUo80O.mjs.map → ng-forge-dynamic-forms-page-field.component-XtyjFlJi.mjs.map} +1 -1
  19. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-lvuyTM1r.mjs → ng-forge-dynamic-forms-text-field.component-CJsRWH2r.mjs} +2 -2
  20. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-lvuyTM1r.mjs.map → ng-forge-dynamic-forms-text-field.component-CJsRWH2r.mjs.map} +1 -1
  21. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  22. package/package.json +1 -1
  23. package/types/ng-forge-dynamic-forms-integration.d.ts +3 -4
  24. package/types/ng-forge-dynamic-forms.d.ts +68 -27
  25. package/fesm2022/ng-forge-dynamic-forms-container-field.component-DQ_OQLtx.mjs.map +0 -1
  26. package/fesm2022/ng-forge-dynamic-forms-container-utils-zQSTe7Jk.mjs.map +0 -1
  27. package/fesm2022/ng-forge-dynamic-forms-emit-initialization-pCNwBRRm.mjs.map +0 -1
  28. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BdMxsqmk.mjs.map +0 -1
@@ -1318,13 +1318,11 @@ function setupInitializationTracking(options) {
1318
1318
  }
1319
1319
 
1320
1320
  /**
1321
- * Type guard to check if a field is a hidden field.
1322
- *
1323
- * @param field - The field to check
1324
- * @returns True if the field is a HiddenField
1321
+ * Type guard for RowField with proper type narrowing
1325
1322
  */
1326
- function isHiddenField(field) {
1327
- return field.type === 'hidden';
1323
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type
1324
+ function isRowField(field) {
1325
+ return field.type === 'row' && 'fields' in field && Array.isArray(field.fields);
1328
1326
  }
1329
1327
 
1330
1328
  /**
@@ -1336,41 +1334,20 @@ function isGroupField(field) {
1336
1334
  }
1337
1335
 
1338
1336
  /**
1339
- * Type guard for RowField with proper type narrowing
1340
- */
1341
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type
1342
- function isRowField(field) {
1343
- return field.type === 'row' && 'fields' in field && Array.isArray(field.fields);
1344
- }
1345
- /**
1346
- * Validates that a row field doesn't contain hidden fields.
1347
- * Hidden fields are not allowed in rows because rows are for horizontal layouts,
1348
- * and hidden fields don't render anything.
1337
+ * Type guard for ContainerField with proper type narrowing.
1349
1338
  *
1350
- * @param rowField The row field to validate
1351
- * @returns true if valid (no hidden fields), false otherwise
1352
- */
1353
- function validateRowNesting(rowField) {
1354
- return !hasHiddenFields(rowField.fields);
1355
- }
1356
- /**
1357
- * Recursively checks if fields contain any hidden fields
1358
- * @param fields Array of field definitions to check
1359
- * @returns true if hidden fields found, false otherwise
1339
+ * `wrappers` is part of the type but optional in practice — many configs use
1340
+ * containers purely as flex/layout wrappers without any wrapper components.
1341
+ * Match on `type === 'container'` and the `fields` shape only, mirroring
1342
+ * `isPageField` / `isRowField`.
1360
1343
  */
1361
- function hasHiddenFields(fields) {
1362
- for (const field of fields) {
1363
- if (isHiddenField(field)) {
1364
- return true;
1365
- }
1366
- // Check group fields for nested hidden fields
1367
- if (isGroupField(field)) {
1368
- if (hasHiddenFields(field.fields)) {
1369
- return true;
1370
- }
1371
- }
1372
- }
1373
- return false;
1344
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type
1345
+ function isContainerTypedField(field) {
1346
+ return (field.type === 'container' &&
1347
+ 'wrappers' in field &&
1348
+ 'fields' in field &&
1349
+ Array.isArray(field.wrappers) &&
1350
+ Array.isArray(field.fields));
1374
1351
  }
1375
1352
 
1376
1353
  /**
@@ -1391,7 +1368,9 @@ function validatePageNesting(pageField) {
1391
1368
  * Type guard to check if a field is a container with fields property
1392
1369
  */
1393
1370
  function isContainerWithFields$1(field) {
1394
- return (isRowField(field) || isGroupField(field)) && 'fields' in field && Array.isArray(field.fields);
1371
+ return ((isRowField(field) || isGroupField(field) || isContainerTypedField(field)) &&
1372
+ 'fields' in field &&
1373
+ Array.isArray(field.fields));
1395
1374
  }
1396
1375
  /**
1397
1376
  * Recursively checks if fields contain any nested page fields
@@ -1431,14 +1410,6 @@ function isSimplifiedArrayField(field) {
1431
1410
  return field.type === 'array' && 'template' in field;
1432
1411
  }
1433
1412
 
1434
- /**
1435
- * Type guard for ContainerField with proper type narrowing
1436
- */
1437
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type
1438
- function isContainerTypedField(field) {
1439
- return field.type === 'container' && 'fields' in field && 'wrappers' in field;
1440
- }
1441
-
1442
1413
  /**
1443
1414
  * Type guard to check if a field definition has child fields.
1444
1415
  * This is a looser check that works with FieldDef<unknown> without requiring RegisteredFieldTypes.
@@ -4170,9 +4141,9 @@ class PageOrchestratorComponent {
4170
4141
  }
4171
4142
  }
4172
4143
  }
4173
- `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-C5rUo80O.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-C5rUo80O.mjs').then(m => m.default)]] });
4144
+ `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-XtyjFlJi.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-XtyjFlJi.mjs').then(m => m.default)]] });
4174
4145
  }
4175
- i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-C5rUo80O.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
4146
+ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-XtyjFlJi.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
4176
4147
  type: Component,
4177
4148
  args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
4178
4149
  @for (pageField of pageFields(); track pageField.key; let i = $index) {
@@ -4558,7 +4529,7 @@ function hasAnyPageFields(fields) {
4558
4529
  return true;
4559
4530
  }
4560
4531
  // Check nested fields in container types
4561
- if (isContainerWithFields(field) && (field.type === 'row' || field.type === 'group')) {
4532
+ if (!isPageField(field) && isContainerWithFields(field)) {
4562
4533
  if (hasAnyPageFields(field.fields)) {
4563
4534
  return true;
4564
4535
  }
@@ -4581,7 +4552,7 @@ function hasNestedPageFields(fields) {
4581
4552
  }
4582
4553
  }
4583
4554
  // Check other container types for nested pages
4584
- if (isContainerWithFields(field) && (field.type === 'row' || field.type === 'group')) {
4555
+ if (!isPageField(field) && isContainerWithFields(field)) {
4585
4556
  if (hasNestedPageFields(field.fields)) {
4586
4557
  return true;
4587
4558
  }
@@ -6318,12 +6289,22 @@ function mapFieldToForm(fieldDef, fieldPath) {
6318
6289
  }
6319
6290
  /**
6320
6291
  * Maps children of a container field (page, row, group) to the form schema.
6292
+ *
6293
+ * Layout containers (page/row/container) flatten into `parentPath` and are
6294
+ * recursed through without consuming a key — same contract as `mapFieldToForm`'s
6295
+ * top-level layout branch, so any depth of layout-container nesting reaches its
6296
+ * leaf descendants. Group/array fields consume their key and look up their own
6297
+ * child path on `parentPath`.
6321
6298
  */
6322
6299
  function mapContainerChildren(fields, parentPath) {
6323
6300
  if (!fields)
6324
6301
  return;
6325
6302
  const pathRecord = parentPath;
6326
6303
  for (const field of fields) {
6304
+ if (isPageField(field) || isRowField(field) || isContainerTypedField(field)) {
6305
+ mapContainerChildren(field.fields, parentPath);
6306
+ continue;
6307
+ }
6327
6308
  if (!field.key)
6328
6309
  continue;
6329
6310
  const childPath = pathRecord[field.key];
@@ -6419,9 +6400,21 @@ function mapArrayFieldToForm(arrayField, fieldPath) {
6419
6400
  maxLength(fieldPath, arrayField.maxLength);
6420
6401
  }
6421
6402
  // Fields can be either FieldDef (primitive) or FieldDef[] (object)
6422
- const itemDefinitions = arrayField.fields;
6423
- // Empty array is valid - items will be added via buttons with their own templates
6424
- // Create a minimal schema that just accepts any value
6403
+ let itemDefinitions = arrayField.fields;
6404
+ // Simplified arrays initialized without `value` have empty `fields`; their
6405
+ // item shape only lives in Symbol metadata. Fall back to that template so
6406
+ // validators and logic declared inside it still apply when items are
6407
+ // materialized from a programmatically-set form value.
6408
+ if (!itemDefinitions || itemDefinitions.length === 0) {
6409
+ const metadataTemplate = getNormalizedArrayMetadata(arrayField)?.template;
6410
+ if (metadataTemplate) {
6411
+ itemDefinitions = [
6412
+ Array.isArray(metadataTemplate) ? [...metadataTemplate] : metadataTemplate,
6413
+ ];
6414
+ }
6415
+ }
6416
+ // Empty array with no template metadata - items will be added via buttons with
6417
+ // their own templates. Create a minimal schema that just accepts any value.
6425
6418
  if (!itemDefinitions || itemDefinitions.length === 0) {
6426
6419
  const emptyItemSchema = schema(() => {
6427
6420
  // No fields to map - items will be added dynamically via buttons
@@ -6433,14 +6426,22 @@ function mapArrayFieldToForm(arrayField, fieldPath) {
6433
6426
  let hasPrimitiveItems = false;
6434
6427
  const allObjectFields = [];
6435
6428
  for (const itemDef of itemDefinitions) {
6436
- if (!Array.isArray(itemDef)) {
6437
- // Primitive item: single FieldDef
6438
- hasPrimitiveItems = true;
6439
- }
6440
- else {
6429
+ if (Array.isArray(itemDef)) {
6441
6430
  // Object item: collect fields for superset schema
6442
6431
  collectFieldsFromObjectItem(itemDef, allObjectFields);
6443
6432
  }
6433
+ else if (isPageField(itemDef) ||
6434
+ isRowField(itemDef) ||
6435
+ isContainerTypedField(itemDef)) {
6436
+ // Layout container as a single-template item: its descendants describe
6437
+ // the item's object shape. Hand it to the object-item collector wrapped
6438
+ // as a 1-element array so the structured-item mapping path runs.
6439
+ collectFieldsFromObjectItem([itemDef], allObjectFields);
6440
+ }
6441
+ else {
6442
+ // Primitive item: single FieldDef
6443
+ hasPrimitiveItems = true;
6444
+ }
6444
6445
  }
6445
6446
  // For pure primitive arrays, use a simple schema
6446
6447
  if (hasPrimitiveItems && allObjectFields.length === 0) {
@@ -6580,7 +6581,7 @@ function getFieldDefaultValue(field, registry) {
6580
6581
  if (valueHandling === 'exclude') {
6581
6582
  return undefined;
6582
6583
  }
6583
- // Flatten fields (row/page) return flattened object of children's values
6584
+ // Flatten fields (row/page/container) return flattened object of children's values
6584
6585
  // This is used by array fields to get the template structure
6585
6586
  // Note: fieldsToDefaultValues will skip these at top-level since they have keys and return objects
6586
6587
  if (valueHandling === 'flatten' && 'fields' in field && field.fields) {
@@ -6588,11 +6589,18 @@ function getFieldDefaultValue(field, registry) {
6588
6589
  // Collect only fields that contribute values (exclude buttons, text, etc.)
6589
6590
  const flattenedValues = {};
6590
6591
  for (const childField of childFields) {
6591
- if ('key' in childField && childField.key) {
6592
- const childValue = getFieldDefaultValue(childField, registry);
6593
- if (childValue !== undefined) {
6594
- flattenedValues[childField.key] = childValue;
6595
- }
6592
+ const childValueHandling = getFieldValueHandling(childField.type, registry);
6593
+ if (childValueHandling === 'exclude')
6594
+ continue;
6595
+ const childValue = getFieldDefaultValue(childField, registry);
6596
+ if (childValue === undefined)
6597
+ continue;
6598
+ if (childValueHandling === 'flatten' && childValue && typeof childValue === 'object' && !Array.isArray(childValue)) {
6599
+ // Nested flatten field — its value is already flat; spread it up.
6600
+ Object.assign(flattenedValues, childValue);
6601
+ }
6602
+ else if ('key' in childField && childField.key) {
6603
+ flattenedValues[childField.key] = childValue;
6596
6604
  }
6597
6605
  }
6598
6606
  // Return flattened object if there are any values, otherwise undefined
@@ -6607,20 +6615,17 @@ function getFieldDefaultValue(field, registry) {
6607
6615
  const groupDefaults = {};
6608
6616
  for (const childField of fields) {
6609
6617
  const childValueHandling = getFieldValueHandling(childField.type, registry);
6610
- // Flatten row/page fields into the group (they are presentational containers)
6611
- if (childValueHandling === 'flatten' && 'fields' in childField && childField.fields) {
6612
- const nestedFields = childField.fields;
6613
- for (const nestedField of nestedFields) {
6614
- if ('key' in nestedField && nestedField.key) {
6615
- const nestedValue = getFieldDefaultValue(nestedField, registry);
6616
- if (nestedValue !== undefined) {
6617
- groupDefaults[nestedField.key] = nestedValue;
6618
- }
6619
- }
6618
+ if (childValueHandling === 'exclude')
6619
+ continue;
6620
+ const childValue = getFieldDefaultValue(childField, registry);
6621
+ if (childValueHandling === 'flatten') {
6622
+ // Flatten container/row/page values into the group (they are presentational containers).
6623
+ // The recursive call already returns a flat object — spread it directly.
6624
+ if (childValue && typeof childValue === 'object' && !Array.isArray(childValue)) {
6625
+ Object.assign(groupDefaults, childValue);
6620
6626
  }
6621
6627
  }
6622
6628
  else if ('key' in childField && childField.key) {
6623
- const childValue = getFieldDefaultValue(childField, registry);
6624
6629
  if (childValue !== undefined) {
6625
6630
  groupDefaults[childField.key] = childValue;
6626
6631
  }
@@ -7438,10 +7443,10 @@ function createEmptyCollection() {
7438
7443
  /** Traverses field definitions and collects cross-field entries (validators, logic, schemas). */
7439
7444
  function collectCrossFieldEntries(fields) {
7440
7445
  const collection = createEmptyCollection();
7441
- traverseFields$2(fields, collection, '');
7446
+ traverseFields(fields, collection, '');
7442
7447
  return collection;
7443
7448
  }
7444
- function traverseFields$2(fields, collection, pathPrefix) {
7449
+ function traverseFields(fields, collection, pathPrefix) {
7445
7450
  for (const field of fields) {
7446
7451
  collectFromField$2(field, collection, pathPrefix);
7447
7452
  // Recursively process container fields (page, row, group, array)
@@ -7453,7 +7458,7 @@ function traverseFields$2(fields, collection, pathPrefix) {
7453
7458
  // be built for them.
7454
7459
  const groupKey = isGroupField(field) ? field.key : undefined;
7455
7460
  const childPrefix = groupKey ? (pathPrefix ? `${pathPrefix}.${groupKey}` : groupKey) : pathPrefix;
7456
- traverseFields$2(normalizeFieldsArray(field.fields), collection, childPrefix);
7461
+ traverseFields(normalizeFieldsArray(field.fields), collection, childPrefix);
7457
7462
  }
7458
7463
  }
7459
7464
  }
@@ -8168,6 +8173,29 @@ function buildPropertyOverrideKey(arrayKey, index, fieldKey) {
8168
8173
  return fieldKey;
8169
8174
  }
8170
8175
 
8176
+ /**
8177
+ * Returns true if the field has any property-derivation logic entries
8178
+ * (type: 'derivation' with a non-empty targetProperty). Mirrors the inclusion
8179
+ * rule used by `collectPropertyDerivations`, so the mapper and orchestrator
8180
+ * agree on which fields participate in property overrides — without depending
8181
+ * on whether the orchestrator's registration effect has run yet (which it
8182
+ * hasn't on the resolveFieldSync path with a warm COMPONENT_CACHE).
8183
+ */
8184
+ function fieldHasPropertyDerivations(fieldDef) {
8185
+ // Match the collector's keyless guard (property-derivation-collector.ts):
8186
+ // keyless fields are never registered in the store, so the mapper must skip
8187
+ // them too — otherwise getOverrides() would be called with an undefined key.
8188
+ if (!fieldDef.key)
8189
+ return false;
8190
+ const logic = fieldDef.logic;
8191
+ if (!logic || logic.length === 0)
8192
+ return false;
8193
+ for (const entry of logic) {
8194
+ if (isDerivationLogicConfig(entry) && hasTargetProperty(entry))
8195
+ return true;
8196
+ }
8197
+ return false;
8198
+ }
8171
8199
  /**
8172
8200
  * Merges forwarded props into meta, with meta taking precedence.
8173
8201
  *
@@ -8269,11 +8297,13 @@ function mapFieldToInputs(fieldDef, fieldRegistry) {
8269
8297
  // Check that arrayContext exists and has a valid index signal (guards against mock injectors)
8270
8298
  const indexSignal = arrayContext?.index;
8271
8299
  const hasArrayContext = indexSignal !== undefined && isSignal(indexSignal);
8272
- // Fast-path check for property overrides: only fields with registered derivations
8273
- // enter the computed() wrapper for overrides. hasField() is a non-reactive Map.has() — O(1).
8274
- // Uses PLACEHOLDER_INDEX to produce the wildcard format (e.g., 'items.$.endDate') matching
8275
- // what the collector/orchestrator registers. The computed block below uses a concrete index instead.
8276
- const hasOverrides = store?.hasField(buildPropertyOverrideKey(arrayContext?.arrayKey, arrayContext ? PLACEHOLDER_INDEX : undefined, fieldDef.key)) ?? false;
8300
+ // Fast-path check for property overrides: only fields whose definition declares
8301
+ // a property-derivation enter the computed() wrapper. We inspect the FieldDef
8302
+ // directly rather than asking the store, because the store's registration
8303
+ // effect may not have fired yet when the mapper runs in the resolveFieldSync
8304
+ // path (warm COMPONENT_CACHE). Both the mapper and the orchestrator's
8305
+ // collector apply the same inclusion rule, so the answers always agree.
8306
+ const hasOverrides = fieldHasPropertyDerivations(fieldDef);
8277
8307
  // Fast path: no transformations needed
8278
8308
  if (!hasPropsForwarding && !hasArrayContext && !hasOverrides) {
8279
8309
  return mapperResult;
@@ -8427,8 +8457,7 @@ function createFieldResolutionPipe(getContext) {
8427
8457
  * Comprehensive form configuration validator that checks:
8428
8458
  * 1. Form mode consistency (paged vs non-paged)
8429
8459
  * 2. Page nesting rules
8430
- * 3. Row nesting rules (no hidden fields in rows)
8431
- * 4. Field placement constraints
8460
+ * 3. Field placement constraints
8432
8461
  */
8433
8462
  class FormModeValidator {
8434
8463
  /**
@@ -8495,57 +8524,6 @@ class FormModeValidator {
8495
8524
  }
8496
8525
  }
8497
8526
  }
8498
- // Warn about hidden fields in rows (they work but don't render, which may be confusing)
8499
- const rowWarnings = this.collectRowHiddenFieldWarnings(fields);
8500
- warnings.push(...rowWarnings);
8501
- return warnings;
8502
- }
8503
- /**
8504
- * Recursively collects warnings for hidden fields inside rows
8505
- * @param fields The form field definitions to check
8506
- * @param path Current path for warning messages
8507
- * @returns Array of warning messages
8508
- */
8509
- static collectRowHiddenFieldWarnings(fields, path = '') {
8510
- if (!fields) {
8511
- return [];
8512
- }
8513
- const warnings = [];
8514
- for (let i = 0; i < fields.length; i++) {
8515
- const field = fields[i];
8516
- const fieldPath = path ? `${path}.fields[${i}]` : `fields[${i}]`;
8517
- const fieldKey = field.key || 'unknown';
8518
- if (isRowField(field)) {
8519
- if (!validateRowNesting(field)) {
8520
- warnings.push(`Row field at ${fieldPath} (key: "${fieldKey}") contains hidden fields. ` +
8521
- `Hidden fields in rows don't render anything - consider placing them outside the row.`);
8522
- }
8523
- // Continue checking nested rows within the row's children
8524
- warnings.push(...this.collectRowHiddenFieldWarnings(field.fields, fieldPath));
8525
- }
8526
- else if (isPageField(field)) {
8527
- // Check rows within pages
8528
- warnings.push(...this.collectRowHiddenFieldWarnings(field.fields, fieldPath));
8529
- }
8530
- else if (isGroupField(field)) {
8531
- // Check rows within groups
8532
- warnings.push(...this.collectRowHiddenFieldWarnings(field.fields, fieldPath));
8533
- }
8534
- else if (isSimplifiedArrayField(field)) {
8535
- // Simplified arrays are normalized later; check template fields if present
8536
- const templateFields = Array.isArray(field.template) ? [...field.template] : [field.template];
8537
- warnings.push(...this.collectRowHiddenFieldWarnings(templateFields, fieldPath));
8538
- }
8539
- else if (isArrayField(field)) {
8540
- // Check rows within array templates — items may be single FieldDef (primitive) or FieldDef[] (object)
8541
- const itemTemplates = field.fields;
8542
- for (let j = 0; j < itemTemplates.length; j++) {
8543
- const itemFields = itemTemplates[j];
8544
- const fieldsArray = Array.isArray(itemFields) ? [...itemFields] : [itemFields];
8545
- warnings.push(...this.collectRowHiddenFieldWarnings(fieldsArray, `${fieldPath}.fields[${j}]`));
8546
- }
8547
- }
8548
- }
8549
8527
  return warnings;
8550
8528
  }
8551
8529
  }
@@ -10532,6 +10510,33 @@ function parseDerivationKey(key) {
10532
10510
  return { fieldKey: key };
10533
10511
  }
10534
10512
 
10513
+ /**
10514
+ * Single dispatch over `value` / `expression` / `functionName` shared by both
10515
+ * the value-derivation and property-derivation applicators.
10516
+ *
10517
+ * Throws {@link DynamicFormError} on missing function or missing value source
10518
+ * so both pipelines surface the library's standard error type.
10519
+ *
10520
+ * @internal
10521
+ */
10522
+ function computeValueFromEntry(entry, evalContext, options) {
10523
+ if (entry.value !== undefined) {
10524
+ return entry.value;
10525
+ }
10526
+ if (entry.expression) {
10527
+ return ExpressionParser.evaluate(entry.expression, evalContext);
10528
+ }
10529
+ if (entry.functionName) {
10530
+ const fn = options.derivationFunctions?.[entry.functionName];
10531
+ if (!fn) {
10532
+ const kind = options.functionKind ?? 'Derivation function';
10533
+ throw new DynamicFormError(`${kind} '${entry.functionName}' not found in customFnConfig.derivations`);
10534
+ }
10535
+ return fn(evalContext);
10536
+ }
10537
+ throw new DynamicFormError(`Derivation for ${options.subject} has no value source. Specify 'value', 'expression', or 'functionName'.`);
10538
+ }
10539
+
10535
10540
  /**
10536
10541
  * Centralized constants for the derivation system.
10537
10542
  *
@@ -11101,10 +11106,15 @@ function createEvaluationContext$1(entry, formValue, context, chainContext) {
11101
11106
  // Create field state snapshot for this specific field (non-reactive)
11102
11107
  const fieldAccessor = context.rootForm[entry.fieldKey];
11103
11108
  const fieldState = fieldAccessor ? readFieldStateInfo(fieldAccessor, false) : undefined;
11109
+ // Parent group value: the absolute path with the trailing field segment
11110
+ // dropped. `undefined` when the field is at form root.
11111
+ const parentPath = getParentPathInScope(entry.fieldKey);
11112
+ const groupValue = parentPath ? getNestedValue(formValue, parentPath) : undefined;
11104
11113
  return {
11105
11114
  fieldValue,
11106
11115
  formValue,
11107
11116
  fieldPath: entry.fieldKey,
11117
+ groupValue,
11108
11118
  customFunctions: context.customFunctions,
11109
11119
  externalData: context.externalData,
11110
11120
  logger: context.logger,
@@ -11112,6 +11122,25 @@ function createEvaluationContext$1(entry, formValue, context, chainContext) {
11112
11122
  formFieldState: chainContext.formFieldState,
11113
11123
  };
11114
11124
  }
11125
+ /**
11126
+ * Returns the path to the parent of the given field path, expressed in the
11127
+ * scope appropriate for the entry. Drops the last `.`-delimited segment.
11128
+ * Returns `undefined` when the field has no parent in scope (e.g., a
11129
+ * single-segment root key, or `'items.$.x'` inside the array-item scope
11130
+ * where `x` is a direct child of the item).
11131
+ *
11132
+ * For non-array entries: `'address.state'` → `'address'`, `'state'` → undefined.
11133
+ * For array entries inside `createEvaluationContext` (root scope): unused —
11134
+ * `createArrayItemEvaluationContext` handles the array-scope case.
11135
+ *
11136
+ * @internal
11137
+ */
11138
+ function getParentPathInScope(fieldKey) {
11139
+ const lastDot = fieldKey.lastIndexOf('.');
11140
+ if (lastDot <= 0)
11141
+ return undefined;
11142
+ return fieldKey.slice(0, lastDot);
11143
+ }
11115
11144
  /**
11116
11145
  * Creates an evaluation context scoped to a specific array item.
11117
11146
  *
@@ -11143,10 +11172,23 @@ function createArrayItemEvaluationContext$1(entry, arrayItem, rootFormValue, ite
11143
11172
  }
11144
11173
  }
11145
11174
  }
11175
+ // groupValue inside an array item:
11176
+ // - Field directly under the array item (no inner group): the array item
11177
+ // itself is the "nearest parent group".
11178
+ // - Field inside an inner group within the item: the inner group's value.
11179
+ // The relative path within the item is everything after `'.$.'`.
11180
+ const relativePath = pathInfo.isArrayPath ? (pathInfo.relativePath ?? '') : '';
11181
+ const innerParentPath = relativePath.includes('.') ? relativePath.slice(0, relativePath.lastIndexOf('.')) : '';
11182
+ const groupValue = innerParentPath ? getNestedValue(arrayItem, innerParentPath) : arrayItem;
11183
+ // Resolve fieldValue to the leaf value addressed by the entry's relative path
11184
+ // within the array item. Without this, $self / fieldValue derivations inside
11185
+ // arrays receive the whole array item instead of the field's own value.
11186
+ const fieldValue = relativePath ? getNestedValue(arrayItem, relativePath) : arrayItem;
11146
11187
  return {
11147
- fieldValue: arrayItem,
11188
+ fieldValue,
11148
11189
  formValue: arrayItem,
11149
- fieldPath: `${arrayPath}.${itemIndex}`,
11190
+ fieldPath: relativePath ? `${arrayPath}.${itemIndex}.${relativePath}` : `${arrayPath}.${itemIndex}`,
11191
+ groupValue,
11150
11192
  customFunctions: context.customFunctions,
11151
11193
  externalData: context.externalData,
11152
11194
  logger: context.logger,
@@ -11175,24 +11217,10 @@ function evaluateDerivationCondition(condition, context) {
11175
11217
  * @internal
11176
11218
  */
11177
11219
  function computeDerivedValue(entry, evalContext, applicatorContext) {
11178
- // Static value
11179
- if (entry.value !== undefined) {
11180
- return entry.value;
11181
- }
11182
- // Expression
11183
- if (entry.expression) {
11184
- return ExpressionParser.evaluate(entry.expression, evalContext);
11185
- }
11186
- // Custom function
11187
- if (entry.functionName) {
11188
- const fn = applicatorContext.derivationFunctions?.[entry.functionName];
11189
- if (!fn) {
11190
- throw new Error(`Derivation function '${entry.functionName}' not found in customFnConfig.derivations`);
11191
- }
11192
- return fn(evalContext);
11193
- }
11194
- // No value source specified
11195
- throw new Error(`Derivation for ${entry.fieldKey} has no value source. ` + `Specify 'value', 'expression', or 'functionName'.`);
11220
+ return computeValueFromEntry(entry, evalContext, {
11221
+ derivationFunctions: applicatorContext.derivationFunctions,
11222
+ subject: entry.fieldKey,
11223
+ });
11196
11224
  }
11197
11225
  /**
11198
11226
  * Error message prefix for derivation-related errors.
@@ -11442,6 +11470,134 @@ function topologicalSort(entries) {
11442
11470
  return sorted;
11443
11471
  }
11444
11472
 
11473
+ /**
11474
+ * Extracts the union of field dependencies declared by a derivation config.
11475
+ *
11476
+ * Combines:
11477
+ * - Explicit `dependsOn` (when provided, it takes precedence over auto-detection)
11478
+ * - Expression-detected dependencies (only used when `dependsOn` is empty)
11479
+ * - `'*'` wildcard for `functionName` (only when `dependsOn` is empty)
11480
+ * - Condition expression dependencies (always included)
11481
+ *
11482
+ * @internal
11483
+ */
11484
+ function extractDependenciesFromConfig(config) {
11485
+ const deps = new Set();
11486
+ if (config.dependsOn && config.dependsOn.length > 0) {
11487
+ config.dependsOn.forEach((dep) => deps.add(dep));
11488
+ }
11489
+ else {
11490
+ if (config.expression) {
11491
+ extractStringDependencies(config.expression).forEach((dep) => deps.add(dep));
11492
+ }
11493
+ if (config.functionName) {
11494
+ deps.add('*');
11495
+ }
11496
+ }
11497
+ if (config.condition && config.condition !== true) {
11498
+ extractExpressionDependencies(config.condition).forEach((dep) => deps.add(dep));
11499
+ }
11500
+ return Array.from(deps);
11501
+ }
11502
+
11503
+ /**
11504
+ * Recursively walks a field-definition tree, invoking `visitor` for each field.
11505
+ *
11506
+ * Owns the subtleties shared across derivation collectors:
11507
+ * - Detects container fields via `hasChildFields`.
11508
+ * - Falls back to the simplified-array Symbol-metadata template when an array
11509
+ * field has empty `fields` (the case for arrays initialized without a
11510
+ * starting `value` — only the metadata template carries their item shape).
11511
+ * - Flattens primitive (FieldDef) and object (FieldDef[]) array items into a
11512
+ * single child list before recursing.
11513
+ *
11514
+ * Callers customize per-boundary context via {@link FieldTraversalHooks}: e.g.,
11515
+ * the value-derivation collector tracks an array path AND a group path (with
11516
+ * the group path reset at array boundaries), while the property-derivation
11517
+ * collector tracks only the array path.
11518
+ *
11519
+ * @internal
11520
+ */
11521
+ function traverseFieldsWithContext(fields, context, visitor, hooks) {
11522
+ for (const field of fields) {
11523
+ visitor(field, context);
11524
+ if (!hasChildFields(field))
11525
+ continue;
11526
+ if (field.type === 'array') {
11527
+ const childContext = mergeContext(context, hooks?.onArrayChild?.(context, field));
11528
+ const arrayChildren = collectArrayChildren(field);
11529
+ traverseFieldsWithContext(arrayChildren, childContext, visitor, hooks);
11530
+ continue;
11531
+ }
11532
+ const overrides = field.type === 'group' ? hooks?.onGroupChild?.(context, field) : hooks?.onLayoutChild?.(context, field);
11533
+ const childContext = mergeContext(context, overrides);
11534
+ traverseFieldsWithContext(normalizeFieldsArray(field.fields), childContext, visitor, hooks);
11535
+ }
11536
+ }
11537
+ /**
11538
+ * Returns the flattened list of children for an array field, falling back
11539
+ * to the Symbol metadata template when `fields` is empty.
11540
+ *
11541
+ * Caller must ensure `field` is a container (passes `hasChildFields`); this
11542
+ * function reads `field.fields` directly and would error otherwise.
11543
+ *
11544
+ * @internal
11545
+ */
11546
+ function collectArrayChildren(field) {
11547
+ let arrayItems = normalizeFieldsArray(field.fields);
11548
+ if (arrayItems.length === 0) {
11549
+ const metadataTemplate = getNormalizedArrayMetadata(field)?.template;
11550
+ if (metadataTemplate) {
11551
+ arrayItems = [
11552
+ Array.isArray(metadataTemplate) ? [...metadataTemplate] : metadataTemplate,
11553
+ ];
11554
+ }
11555
+ }
11556
+ const flattened = [];
11557
+ for (const item of arrayItems) {
11558
+ if (Array.isArray(item)) {
11559
+ flattened.push(...item);
11560
+ }
11561
+ else {
11562
+ flattened.push(item);
11563
+ }
11564
+ }
11565
+ return flattened;
11566
+ }
11567
+ /** @internal */
11568
+ function mergeContext(base, overrides) {
11569
+ return overrides ? { ...base, ...overrides } : { ...base };
11570
+ }
11571
+
11572
+ /**
11573
+ * Token in `dependsOn` arrays that resolves to the current field's own
11574
+ * absolute path at collection time. Useful for self-derivations on fields
11575
+ * nested under groups, where the absolute path depends on ancestor keys
11576
+ * the config author may not know (e.g. factory-built field shapes).
11577
+ *
11578
+ * @public
11579
+ */
11580
+ const SELF_DEPENDENCY_TOKEN = '$self';
11581
+ /**
11582
+ * Token in `dependsOn` arrays that resolves to the absolute path of the
11583
+ * field's nearest parent container (group or array) at collection time.
11584
+ *
11585
+ * Useful when a derivation should fire whenever any sibling under the
11586
+ * same parent container changes (e.g. "compute when anything in the
11587
+ * address group is edited") without enumerating each sibling explicitly.
11588
+ *
11589
+ * Resolution rules:
11590
+ * - Inside group(s) only: resolves to the dotted ancestor group path
11591
+ * (e.g. `'address'` or `'org.address'`).
11592
+ * - Inside an array (no group between): resolves to the array key
11593
+ * (e.g. `'items'`). Fires whenever any item changes.
11594
+ * - Inside group(s) inside an array: resolves to the array placeholder
11595
+ * form including the inner groups (e.g. `'items.$.address'`).
11596
+ * - At form root: throws `DynamicFormError` (no parent to target).
11597
+ *
11598
+ * @public
11599
+ */
11600
+ const GROUP_DEPENDENCY_TOKEN = '$group';
11445
11601
  /**
11446
11602
  * Collects all derivation entries from field definitions.
11447
11603
  *
@@ -11479,8 +11635,21 @@ function topologicalSort(entries) {
11479
11635
  */
11480
11636
  function collectDerivations(fields) {
11481
11637
  const entries = [];
11482
- const context = {};
11483
- traverseFields$1(fields, entries, context);
11638
+ traverseFieldsWithContext(fields, {}, (field, context) => collectFromField$1(field, entries, context), {
11639
+ // Array boundary: the array placeholder path takes over and any
11640
+ // ancestor groupPath is reset for descendants (entries inside
11641
+ // array items use `arrayPath.$.fieldKey` form).
11642
+ onArrayChild: (_, field) => ({ arrayPath: field.key, groupPath: undefined }),
11643
+ onGroupChild: (parent, field) => {
11644
+ // Group establishes a model boundary; descend with the field's
11645
+ // key appended to the ancestor groupPath. Groups without a key
11646
+ // (rare) act like layout containers — leave context unchanged.
11647
+ if (!field.key)
11648
+ return {};
11649
+ return { groupPath: parent.groupPath ? `${parent.groupPath}.${field.key}` : field.key };
11650
+ },
11651
+ // Layout containers (page, row, container) leave context unchanged.
11652
+ });
11484
11653
  // Sort entries in topological order for efficient processing.
11485
11654
  // This ensures derivations are processed in dependency order,
11486
11655
  // reducing the number of iterations needed in the applicator.
@@ -11488,40 +11657,83 @@ function collectDerivations(fields) {
11488
11657
  return { entries: sortedEntries };
11489
11658
  }
11490
11659
  /**
11491
- * Recursively traverses field definitions to collect derivations.
11660
+ * Builds the absolute field path for a derivation entry from the field's
11661
+ * key and the current collection context.
11662
+ *
11663
+ * - Inside an array AND inside one or more groups (groups nested under
11664
+ * the array item): combines both as `${arrayPath}.$.${groupPath}.${fieldKey}`.
11665
+ * - Inside an array only: returns `${arrayPath}.$.${fieldKey}`.
11666
+ * - Inside one or more groups: returns `${groupPath}.${fieldKey}`.
11667
+ * - Otherwise returns `fieldKey` unchanged.
11668
+ *
11669
+ * `groupPath` is reset when entering an array (each array item is its own
11670
+ * model root); ancestor groups OUTSIDE the array are not part of the
11671
+ * entry path. Groups INSIDE the array are tracked via `groupPath` and
11672
+ * combined here.
11492
11673
  *
11493
11674
  * @internal
11494
11675
  */
11495
- function traverseFields$1(fields, entries, context) {
11496
- for (const field of fields) {
11497
- collectFromField$1(field, entries, context);
11498
- // Recursively process container fields (page, row, group, array)
11499
- if (hasChildFields(field)) {
11500
- const childContext = { ...context };
11501
- // Update array path context if this is an array field
11502
- if (field.type === 'array') {
11503
- childContext.arrayPath = field.key;
11504
- // Array fields have items that can be either FieldDef (primitive) or FieldDef[] (object).
11505
- // Normalize all items to arrays and flatten for traversal.
11506
- const arrayItems = normalizeFieldsArray(field.fields);
11507
- const normalizedChildren = [];
11508
- for (const item of arrayItems) {
11509
- if (Array.isArray(item)) {
11510
- // Object item: FieldDef[] - add each field
11511
- normalizedChildren.push(...item);
11512
- }
11513
- else {
11514
- // Primitive item: single FieldDef - add directly
11515
- normalizedChildren.push(item);
11516
- }
11517
- }
11518
- traverseFields$1(normalizedChildren, entries, childContext);
11519
- }
11520
- else {
11521
- traverseFields$1(normalizeFieldsArray(field.fields), entries, childContext);
11676
+ function buildEffectiveFieldKey(fieldKey, context) {
11677
+ if (context.arrayPath && context.groupPath) {
11678
+ return `${context.arrayPath}.$.${context.groupPath}.${fieldKey}`;
11679
+ }
11680
+ if (context.arrayPath) {
11681
+ return `${context.arrayPath}.$.${fieldKey}`;
11682
+ }
11683
+ if (context.groupPath) {
11684
+ return `${context.groupPath}.${fieldKey}`;
11685
+ }
11686
+ return fieldKey;
11687
+ }
11688
+ /**
11689
+ * Resolves the absolute path of the field's nearest parent container
11690
+ * (group or array). Returns `undefined` when the field is at the form
11691
+ * root with no parent container.
11692
+ *
11693
+ * @internal
11694
+ */
11695
+ function buildNearestGroupPath(context) {
11696
+ if (context.arrayPath && context.groupPath) {
11697
+ return `${context.arrayPath}.$.${context.groupPath}`;
11698
+ }
11699
+ return context.arrayPath ?? context.groupPath;
11700
+ }
11701
+ /**
11702
+ * Substitutes special tokens in a `dependsOn` array with their resolved
11703
+ * absolute paths. Currently supports:
11704
+ *
11705
+ * - `SELF_DEPENDENCY_TOKEN` ('$self') → the field's own absolute path
11706
+ * - `'$self.X'` → the field's own path joined with `.X` (e.g. for object-valued
11707
+ * fields where you want to depend on a sub-property)
11708
+ * - `GROUP_DEPENDENCY_TOKEN` ('$group') → the field's nearest parent
11709
+ * container path; throws if the field has no parent group/array.
11710
+ * - `'$group.sibling'` → the parent path joined with `.sibling`, useful to
11711
+ * target a specific sibling without naming the parent group key.
11712
+ *
11713
+ * Non-token strings are passed through unchanged.
11714
+ *
11715
+ * @internal
11716
+ */
11717
+ function resolveTokenDependencies(deps, effectiveFieldKey, context) {
11718
+ return deps.map((dep) => {
11719
+ if (dep === SELF_DEPENDENCY_TOKEN) {
11720
+ return effectiveFieldKey;
11721
+ }
11722
+ if (dep.startsWith(`${SELF_DEPENDENCY_TOKEN}.`)) {
11723
+ return effectiveFieldKey + dep.slice(SELF_DEPENDENCY_TOKEN.length);
11724
+ }
11725
+ if (dep === GROUP_DEPENDENCY_TOKEN || dep.startsWith(`${GROUP_DEPENDENCY_TOKEN}.`)) {
11726
+ const groupPath = buildNearestGroupPath(context);
11727
+ if (!groupPath) {
11728
+ throw new DynamicFormError(`Derivation for '${effectiveFieldKey}' uses '${dep}' but the field has no parent group or array. ` +
11729
+ `'${GROUP_DEPENDENCY_TOKEN}' is only valid for fields nested inside a group or array container.`);
11522
11730
  }
11731
+ if (dep === GROUP_DEPENDENCY_TOKEN)
11732
+ return groupPath;
11733
+ return groupPath + dep.slice(GROUP_DEPENDENCY_TOKEN.length);
11523
11734
  }
11524
- }
11735
+ return dep;
11736
+ });
11525
11737
  }
11526
11738
  /**
11527
11739
  * Collects derivation entries from a single field.
@@ -11561,8 +11773,7 @@ function collectFromField$1(field, entries, context) {
11561
11773
  * @internal
11562
11774
  */
11563
11775
  function createShorthandEntry(fieldKey, expression, context) {
11564
- // Build the effective field key, including array path if in array context
11565
- const effectiveFieldKey = context.arrayPath ? `${context.arrayPath}.$.${fieldKey}` : fieldKey;
11776
+ const effectiveFieldKey = buildEffectiveFieldKey(fieldKey, context);
11566
11777
  return {
11567
11778
  fieldKey: effectiveFieldKey,
11568
11779
  dependsOn: extractStringDependencies(expression),
@@ -11588,11 +11799,16 @@ function createShorthandEntry(fieldKey, expression, context) {
11588
11799
  * @internal
11589
11800
  */
11590
11801
  function createLogicEntry(fieldKey, config, context) {
11591
- // Build the effective field key, including array path if in array context
11592
- const effectiveFieldKey = context.arrayPath ? `${context.arrayPath}.$.${fieldKey}` : fieldKey;
11802
+ const effectiveFieldKey = buildEffectiveFieldKey(fieldKey, context);
11593
11803
  // Runtime guards for HTTP and async derivations.
11594
11804
  // Mutual exclusivity and required fields are now enforced by TypeScript (via `source` discriminant).
11595
11805
  // The wildcard and empty-array checks below are not expressible in the type system.
11806
+ //
11807
+ // Note: `$group` / `$group.X` tokens are intentionally allowed for HTTP and async sources even
11808
+ // though they resolve to the parent path and therefore fire on any sibling change inside that
11809
+ // group/array. Unlike `*`, the token is structurally scoped (collection throws if the field has
11810
+ // no parent container) and an explicit opt-in. Consumers that want to bound request frequency
11811
+ // can use `trigger: 'debounced'` here, or rely on debouncing inside their async function.
11596
11812
  if (config.source === 'http') {
11597
11813
  if (config.dependsOn.length === 0) {
11598
11814
  throw new DynamicFormError(`HTTP derivation for '${effectiveFieldKey}' requires explicit 'dependsOn'. ` +
@@ -11613,7 +11829,7 @@ function createLogicEntry(fieldKey, config, context) {
11613
11829
  `Wildcards would trigger async functions on every form change. Specify explicit field dependencies instead.`);
11614
11830
  }
11615
11831
  }
11616
- const dependsOn = extractDependencies$1(config);
11832
+ const dependsOn = resolveTokenDependencies(extractDependenciesFromConfig(config), effectiveFieldKey, context);
11617
11833
  const condition = config.condition ?? true;
11618
11834
  const trigger = config.trigger ?? 'onChange';
11619
11835
  // Extract debounceMs from debounced configs
@@ -11638,43 +11854,6 @@ function createLogicEntry(fieldKey, config, context) {
11638
11854
  reEngageOnDependencyChange: config.reEngageOnDependencyChange,
11639
11855
  };
11640
11856
  }
11641
- /**
11642
- * Extracts all field dependencies from a derivation config.
11643
- *
11644
- * Combines dependencies from:
11645
- * - Explicit `dependsOn` array (if provided, takes precedence)
11646
- * - Condition expression
11647
- * - Value expression
11648
- * - Function name (defaults to '*' if no explicit dependsOn)
11649
- *
11650
- * @internal
11651
- */
11652
- function extractDependencies$1(config) {
11653
- const deps = new Set();
11654
- // If explicit dependsOn is provided, use it as the primary source
11655
- // This allows users to override automatic detection and control
11656
- // when derivations are triggered
11657
- if (config.dependsOn && config.dependsOn.length > 0) {
11658
- config.dependsOn.forEach((dep) => deps.add(dep));
11659
- }
11660
- else {
11661
- // Extract from expression (automatic dependency detection)
11662
- if (config.expression) {
11663
- const exprDeps = extractStringDependencies(config.expression);
11664
- exprDeps.forEach((dep) => deps.add(dep));
11665
- }
11666
- // Custom functions assume full form access if no explicit dependsOn
11667
- if (config.functionName) {
11668
- deps.add('*');
11669
- }
11670
- }
11671
- // Always extract from condition (these are additional runtime guards)
11672
- if (config.condition && config.condition !== true) {
11673
- const conditionDeps = extractExpressionDependencies(config.condition);
11674
- conditionDeps.forEach((dep) => deps.add(dep));
11675
- }
11676
- return Array.from(deps);
11677
- }
11678
11857
 
11679
11858
  /**
11680
11859
  * Detects cycles in the derivation dependency graph.
@@ -12361,6 +12540,58 @@ function createAsyncDerivationStream(entry, formValue$, context) {
12361
12540
  }));
12362
12541
  }
12363
12542
 
12543
+ /**
12544
+ * Returns the unique set of debounce periods declared by `entries` whose
12545
+ * `trigger === 'debounced'`. Entries without an explicit `debounceMs` use
12546
+ * {@link DEFAULT_DEBOUNCE_MS}.
12547
+ *
12548
+ * @internal
12549
+ */
12550
+ function getDebouncePeriods(entries) {
12551
+ const periods = new Set();
12552
+ for (const entry of entries) {
12553
+ if (entry.trigger === 'debounced') {
12554
+ periods.add(entry.debounceMs ?? DEFAULT_DEBOUNCE_MS);
12555
+ }
12556
+ }
12557
+ return Array.from(periods);
12558
+ }
12559
+ /**
12560
+ * Filters `entries` down to those whose `trigger === 'debounced'` and whose
12561
+ * effective `debounceMs` (defaulting to {@link DEFAULT_DEBOUNCE_MS}) equals `period`.
12562
+ *
12563
+ * @internal
12564
+ */
12565
+ function filterEntriesByDebouncePeriod(entries, period) {
12566
+ return entries.filter((entry) => entry.trigger === 'debounced' && (entry.debounceMs ?? DEFAULT_DEBOUNCE_MS) === period);
12567
+ }
12568
+
12569
+ /**
12570
+ * Resolves a record of external-data signals to their current values without
12571
+ * establishing reactive dependencies.
12572
+ *
12573
+ * The orchestrators read external data per derivation cycle; using `untracked`
12574
+ * here ensures the orchestrator's own computed/effect graph isn't tied to the
12575
+ * external-data signal identities.
12576
+ *
12577
+ * @internal
12578
+ */
12579
+ function resolveExternalData(externalDataSignal) {
12580
+ const externalDataRecord = untracked(() => externalDataSignal?.());
12581
+ if (!externalDataRecord)
12582
+ return undefined;
12583
+ const resolved = {};
12584
+ for (const [key, value] of Object.entries(externalDataRecord)) {
12585
+ if (isSignal(value)) {
12586
+ resolved[key] = untracked(() => value());
12587
+ }
12588
+ else {
12589
+ resolved[key] = value;
12590
+ }
12591
+ }
12592
+ return resolved;
12593
+ }
12594
+
12364
12595
  /**
12365
12596
  * Orchestrates derivation processing for a dynamic form.
12366
12597
  *
@@ -12477,7 +12708,7 @@ class DerivationOrchestrator {
12477
12708
  if (!collection || !formAccessor)
12478
12709
  return of(null);
12479
12710
  // Get unique debounce periods from entries with trigger 'debounced'
12480
- const debouncePeriods = this.getDebouncePeriods(collection.entries);
12711
+ const debouncePeriods = getDebouncePeriods(collection.entries);
12481
12712
  if (debouncePeriods.length === 0)
12482
12713
  return of(null);
12483
12714
  // merge: Process multiple debounce periods concurrently.
@@ -12510,7 +12741,7 @@ class DerivationOrchestrator {
12510
12741
  rootForm: formAccessor,
12511
12742
  derivationFunctions: this.functionRegistry.getDerivationFunctions(),
12512
12743
  customFunctions: this.functionRegistry.getCustomFunctions(),
12513
- externalData: this.resolveExternalData(),
12744
+ externalData: resolveExternalData(this.config.externalData),
12514
12745
  logger: this.logger,
12515
12746
  warningTracker: this.warningTracker,
12516
12747
  derivationLogger: untracked(() => this.config.derivationLogger()),
@@ -12538,7 +12769,7 @@ class DerivationOrchestrator {
12538
12769
  rootForm: formAccessor,
12539
12770
  derivationFunctions: this.functionRegistry.getDerivationFunctions(),
12540
12771
  customFunctions: this.functionRegistry.getCustomFunctions(),
12541
- externalData: this.resolveExternalData(),
12772
+ externalData: resolveExternalData(this.config.externalData),
12542
12773
  logger: this.logger,
12543
12774
  warningTracker: this.warningTracker,
12544
12775
  derivationLogger: untracked(() => this.config.derivationLogger()),
@@ -12588,7 +12819,7 @@ class DerivationOrchestrator {
12588
12819
  logger: this.logger,
12589
12820
  derivationLogger: this.config.derivationLogger,
12590
12821
  customFunctions: () => this.functionRegistry.getCustomFunctions(),
12591
- externalData: () => this.resolveExternalData(),
12822
+ externalData: () => resolveExternalData(this.config.externalData),
12592
12823
  warningTracker: this.warningTracker,
12593
12824
  guard$: this.streamGuard.guard$,
12594
12825
  };
@@ -12662,7 +12893,7 @@ class DerivationOrchestrator {
12662
12893
  derivationLogger: this.config.derivationLogger,
12663
12894
  customFunctions: () => this.functionRegistry.getCustomFunctions(),
12664
12895
  asyncDerivationFunctions: () => this.functionRegistry.getAsyncDerivationFunctions(),
12665
- externalData: () => this.resolveExternalData(),
12896
+ externalData: () => resolveExternalData(this.config.externalData),
12666
12897
  warningTracker: this.warningTracker,
12667
12898
  };
12668
12899
  const stream = createAsyncDerivationStream(entry, formValue$, context).pipe(takeUntilDestroyed(this.destroyRef));
@@ -12697,42 +12928,6 @@ class DerivationOrchestrator {
12697
12928
  return `${entry.fieldKey}:${JSON.stringify(config, Object.keys(config).sort())}`;
12698
12929
  }));
12699
12930
  }
12700
- /**
12701
- * Gets all unique debounce periods from entries with trigger 'debounced'.
12702
- */
12703
- getDebouncePeriods(entries) {
12704
- const periods = new Set();
12705
- for (const entry of entries) {
12706
- if (entry.trigger === 'debounced') {
12707
- periods.add(entry.debounceMs ?? DEFAULT_DEBOUNCE_MS);
12708
- }
12709
- }
12710
- return Array.from(periods);
12711
- }
12712
- /**
12713
- * Resolves external data signals to their current values without creating dependencies.
12714
- *
12715
- * Uses `untracked()` to read signals without establishing reactive dependencies,
12716
- * which is important for derivations that shouldn't re-trigger on every external data change.
12717
- *
12718
- * @returns Record of resolved external data values, or undefined if no external data.
12719
- */
12720
- resolveExternalData() {
12721
- const externalDataRecord = untracked(() => this.config.externalData?.());
12722
- if (!externalDataRecord) {
12723
- return undefined;
12724
- }
12725
- const resolved = {};
12726
- for (const [key, signal] of Object.entries(externalDataRecord)) {
12727
- if (isSignal(signal)) {
12728
- resolved[key] = untracked(() => signal());
12729
- }
12730
- else {
12731
- resolved[key] = signal;
12732
- }
12733
- }
12734
- return resolved;
12735
- }
12736
12931
  }
12737
12932
  /**
12738
12933
  * Creates a DerivationOrchestrator for a dynamic form.
@@ -13219,24 +13414,11 @@ function evaluatePropertyCondition(condition, context) {
13219
13414
  * @internal
13220
13415
  */
13221
13416
  function computePropertyValue(entry, evalContext, applicatorContext) {
13222
- // Static value
13223
- if (entry.value !== undefined) {
13224
- return entry.value;
13225
- }
13226
- // Expression
13227
- if (entry.expression) {
13228
- return ExpressionParser.evaluate(entry.expression, evalContext);
13229
- }
13230
- // Custom function
13231
- if (entry.functionName) {
13232
- const fn = applicatorContext.derivationFunctions?.[entry.functionName];
13233
- if (!fn) {
13234
- throw new DynamicFormError(`Property derivation function '${entry.functionName}' not found in customFnConfig.derivations`);
13235
- }
13236
- return fn(evalContext);
13237
- }
13238
- throw new DynamicFormError(`Property derivation for ${entry.fieldKey}.${entry.targetProperty} has no value source. ` +
13239
- `Specify 'value', 'expression', or 'functionName'.`);
13417
+ return computeValueFromEntry(entry, evalContext, {
13418
+ derivationFunctions: applicatorContext.derivationFunctions,
13419
+ subject: `${entry.fieldKey}.${entry.targetProperty}`,
13420
+ functionKind: 'Property derivation function',
13421
+ });
13240
13422
  }
13241
13423
 
13242
13424
  /**
@@ -13256,40 +13438,12 @@ function computePropertyValue(entry, evalContext, applicatorContext) {
13256
13438
  function collectPropertyDerivations(fields, logger, tracker) {
13257
13439
  const entries = [];
13258
13440
  const context = { logger, tracker };
13259
- traverseFields(fields, entries, context);
13441
+ traverseFieldsWithContext(fields, context, (field, ctx) => collectFromField(field, entries, ctx), {
13442
+ onArrayChild: (_, field) => ({ arrayPath: field.key }),
13443
+ // Group and layout containers do not affect property-derivation paths.
13444
+ });
13260
13445
  return { entries };
13261
13446
  }
13262
- /**
13263
- * Recursively traverses field definitions to collect property derivations.
13264
- *
13265
- * @internal
13266
- */
13267
- function traverseFields(fields, entries, context) {
13268
- for (const field of fields) {
13269
- collectFromField(field, entries, context);
13270
- // Recursively process container fields (page, row, group, array)
13271
- if (hasChildFields(field)) {
13272
- const childContext = { ...context };
13273
- if (field.type === 'array') {
13274
- childContext.arrayPath = field.key;
13275
- const arrayItems = normalizeFieldsArray(field.fields);
13276
- const normalizedChildren = [];
13277
- for (const item of arrayItems) {
13278
- if (Array.isArray(item)) {
13279
- normalizedChildren.push(...item);
13280
- }
13281
- else {
13282
- normalizedChildren.push(item);
13283
- }
13284
- }
13285
- traverseFields(normalizedChildren, entries, childContext);
13286
- }
13287
- else {
13288
- traverseFields(normalizeFieldsArray(field.fields), entries, childContext);
13289
- }
13290
- }
13291
- }
13292
- }
13293
13447
  /**
13294
13448
  * Collects property derivation entries from a single field.
13295
13449
  *
@@ -13319,7 +13473,7 @@ function collectFromField(field, entries, context) {
13319
13473
  */
13320
13474
  function createPropertyDerivationEntryFromDerivation(fieldKey, config, context) {
13321
13475
  const effectiveFieldKey = buildPropertyOverrideKey(context.arrayPath, context.arrayPath ? PLACEHOLDER_INDEX : undefined, fieldKey);
13322
- const dependsOn = extractDependencies(config);
13476
+ const dependsOn = extractDependenciesFromConfig(config);
13323
13477
  const condition = config.condition ?? true;
13324
13478
  const trigger = config.trigger ?? 'onChange';
13325
13479
  const debounceMs = trigger === 'debounced' ? config.debounceMs : undefined;
@@ -13337,32 +13491,6 @@ function createPropertyDerivationEntryFromDerivation(fieldKey, config, context)
13337
13491
  originalConfig: config,
13338
13492
  };
13339
13493
  }
13340
- /**
13341
- * Extracts all field dependencies from a property derivation config.
13342
- *
13343
- * @internal
13344
- */
13345
- function extractDependencies(config) {
13346
- const deps = new Set();
13347
- if (config.dependsOn && config.dependsOn.length > 0) {
13348
- config.dependsOn.forEach((dep) => deps.add(dep));
13349
- }
13350
- else {
13351
- if (config.expression) {
13352
- const exprDeps = extractStringDependencies(config.expression);
13353
- exprDeps.forEach((dep) => deps.add(dep));
13354
- }
13355
- if (config.functionName) {
13356
- deps.add('*');
13357
- }
13358
- }
13359
- // Always extract from condition
13360
- if (config.condition && config.condition !== true) {
13361
- const conditionDeps = extractExpressionDependencies(config.condition);
13362
- conditionDeps.forEach((dep) => deps.add(dep));
13363
- }
13364
- return Array.from(deps);
13365
- }
13366
13494
 
13367
13495
  /**
13368
13496
  * Orchestrates property derivation processing for a dynamic form.
@@ -13440,7 +13568,7 @@ class PropertyDerivationOrchestrator {
13440
13568
  const collection = untracked(() => this.propertyDerivationCollection());
13441
13569
  if (!collection)
13442
13570
  return of(null);
13443
- const debouncePeriods = this.getDebouncePeriods(collection.entries);
13571
+ const debouncePeriods = getDebouncePeriods(collection.entries);
13444
13572
  if (debouncePeriods.length === 0)
13445
13573
  return of(null);
13446
13574
  const periodStreams = debouncePeriods.map((debounceMs) => this.createPeriodStream(debounceMs, collection, changedFields));
@@ -13465,7 +13593,7 @@ class PropertyDerivationOrchestrator {
13465
13593
  store: this.config.store,
13466
13594
  derivationFunctions: this.functionRegistry.getDerivationFunctions(),
13467
13595
  customFunctions: this.functionRegistry.getCustomFunctions(),
13468
- externalData: this.resolveExternalData(),
13596
+ externalData: resolveExternalData(this.config.externalData),
13469
13597
  logger: this.logger,
13470
13598
  warningTracker: this.warningTracker,
13471
13599
  };
@@ -13481,45 +13609,12 @@ class PropertyDerivationOrchestrator {
13481
13609
  store: this.config.store,
13482
13610
  derivationFunctions: this.functionRegistry.getDerivationFunctions(),
13483
13611
  customFunctions: this.functionRegistry.getCustomFunctions(),
13484
- externalData: this.resolveExternalData(),
13612
+ externalData: resolveExternalData(this.config.externalData),
13485
13613
  logger: this.logger,
13486
13614
  warningTracker: this.warningTracker,
13487
13615
  };
13488
13616
  applyPropertyDerivationsForTrigger(filteredCollection, 'debounced', applicatorContext, changedFields);
13489
13617
  }
13490
- getDebouncePeriods(entries) {
13491
- const periods = new Set();
13492
- for (const entry of entries) {
13493
- if (entry.trigger === 'debounced') {
13494
- periods.add(entry.debounceMs ?? DEFAULT_DEBOUNCE_MS);
13495
- }
13496
- }
13497
- return Array.from(periods);
13498
- }
13499
- /**
13500
- * Resolves external data signals to their current values.
13501
- *
13502
- * Called on every onChange and debounced application. Each call iterates all
13503
- * external data entries and resolves signals via untracked(). For forms with
13504
- * many external data entries, this could be optimized by caching the resolved
13505
- * record and only invalidating when external data signal values change.
13506
- * In practice, external data entries are few, so the linear scan is acceptable.
13507
- */
13508
- resolveExternalData() {
13509
- const externalDataRecord = untracked(() => this.config.externalData?.());
13510
- if (!externalDataRecord)
13511
- return undefined;
13512
- const resolved = {};
13513
- for (const [key, signalValue] of Object.entries(externalDataRecord)) {
13514
- if (isSignal(signalValue)) {
13515
- resolved[key] = untracked(() => signalValue());
13516
- }
13517
- else {
13518
- resolved[key] = signalValue;
13519
- }
13520
- }
13521
- return resolved;
13522
- }
13523
13618
  }
13524
13619
  /**
13525
13620
  * Creates a PropertyDerivationOrchestrator for a dynamic form.
@@ -14491,7 +14586,10 @@ function rowFieldMapper(fieldDef) {
14491
14586
  // Rebuilt each emission so logic-driven updates to fieldDef (e.g. `disabled`)
14492
14587
  // flow through. Shallow spread is safe while RowField has no nested
14493
14588
  // mutable props; add a deep-clone if that stops being true.
14494
- const containerField = { ...fieldDef, wrappers: ROW_WRAPPERS };
14589
+ // `type` is rewritten to 'container' because `row` is a synthetic field
14590
+ // type — the runtime renders it as a ContainerField with a synthesized
14591
+ // `{ type: 'row' }` wrapper.
14592
+ const containerField = { ...fieldDef, type: 'container', wrappers: ROW_WRAPPERS };
14495
14593
  const inputs = {
14496
14594
  key: fieldDef.key,
14497
14595
  field: containerField,
@@ -14637,31 +14735,31 @@ const BUILT_IN_FIELDS = [
14637
14735
  // `row` is a virtual field type: it maps to ContainerFieldComponent, and
14638
14736
  // the rowFieldMapper injects a `{ type: 'row' }` wrapper so the container
14639
14737
  // renders the flex/grid layout via RowWrapperComponent.
14640
- loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-DQ_OQLtx.mjs'),
14738
+ loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-ByKgY4-b.mjs'),
14641
14739
  mapper: rowFieldMapper,
14642
14740
  valueHandling: 'flatten',
14643
14741
  },
14644
14742
  {
14645
14743
  name: 'group',
14646
- loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-BdLdSEHj.mjs'),
14744
+ loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-clrQsUyY.mjs'),
14647
14745
  mapper: groupFieldMapper,
14648
14746
  valueHandling: 'include',
14649
14747
  },
14650
14748
  {
14651
14749
  name: 'array',
14652
- loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-CJ_OclbR.mjs'),
14750
+ loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-DWhbvZUO.mjs'),
14653
14751
  mapper: arrayFieldMapper,
14654
14752
  valueHandling: 'include',
14655
14753
  },
14656
14754
  {
14657
14755
  name: 'page',
14658
- loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-C5rUo80O.mjs'),
14756
+ loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-XtyjFlJi.mjs'),
14659
14757
  mapper: pageFieldMapper,
14660
14758
  valueHandling: 'flatten',
14661
14759
  },
14662
14760
  {
14663
14761
  name: 'text',
14664
- loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-lvuyTM1r.mjs'),
14762
+ loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-CJsRWH2r.mjs'),
14665
14763
  mapper: textFieldMapper,
14666
14764
  valueHandling: 'exclude',
14667
14765
  ...DISPLAY_FIELD_TYPES_BASE,
@@ -14673,7 +14771,7 @@ const BUILT_IN_FIELDS = [
14673
14771
  },
14674
14772
  {
14675
14773
  name: 'container',
14676
- loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-DQ_OQLtx.mjs'),
14774
+ loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-ByKgY4-b.mjs'),
14677
14775
  mapper: containerFieldMapper,
14678
14776
  valueHandling: 'flatten',
14679
14777
  },
@@ -14681,7 +14779,7 @@ const BUILT_IN_FIELDS = [
14681
14779
  const BUILT_IN_WRAPPERS = [
14682
14780
  {
14683
14781
  wrapperName: 'css',
14684
- loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-2fdmbtL7.mjs'),
14782
+ loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-ClFdxZV4.mjs'),
14685
14783
  },
14686
14784
  {
14687
14785
  wrapperName: 'row',
@@ -15714,4 +15812,4 @@ function extractErrorParams(error) {
15714
15812
  */
15715
15813
 
15716
15814
  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, withValueExclusionDefaults as aL, wrapperProps as aM, 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 };
15717
- //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-BdMxsqmk.mjs.map
15815
+ //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-Bw9Bt0Ev.mjs.map