@ng-forge/dynamic-forms 0.9.0-next.5 → 0.9.0-next.7

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 (22) hide show
  1. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-DYPTXG9B.mjs → ng-forge-dynamic-forms-array-field.component-CltYGHg9.mjs} +3 -3
  2. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-DYPTXG9B.mjs.map → ng-forge-dynamic-forms-array-field.component-CltYGHg9.mjs.map} +1 -1
  3. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-BMDGh8bN.mjs → ng-forge-dynamic-forms-container-field.component-D4Fpr2ri.mjs} +3 -3
  4. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-BMDGh8bN.mjs.map → ng-forge-dynamic-forms-container-field.component-D4Fpr2ri.mjs.map} +1 -1
  5. package/fesm2022/{ng-forge-dynamic-forms-container-utils-C5r1wwP9.mjs → ng-forge-dynamic-forms-container-utils-BTiqZ8Np.mjs} +2 -2
  6. package/fesm2022/{ng-forge-dynamic-forms-container-utils-C5r1wwP9.mjs.map → ng-forge-dynamic-forms-container-utils-BTiqZ8Np.mjs.map} +1 -1
  7. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-hRiX9Jmf.mjs → ng-forge-dynamic-forms-css-wrapper.component-mdfySFjX.mjs} +2 -2
  8. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-hRiX9Jmf.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-mdfySFjX.mjs.map} +1 -1
  9. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-CYUYq_qs.mjs → ng-forge-dynamic-forms-emit-initialization-Bt3kvlOY.mjs} +2 -2
  10. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-CYUYq_qs.mjs.map → ng-forge-dynamic-forms-emit-initialization-Bt3kvlOY.mjs.map} +1 -1
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BR7nCNmT.mjs → ng-forge-dynamic-forms-group-field.component-BxswrhOc.mjs} +3 -3
  12. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-BR7nCNmT.mjs.map → ng-forge-dynamic-forms-group-field.component-BxswrhOc.mjs.map} +1 -1
  13. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-DRYdwQcd.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-QdV2vcCx.mjs} +362 -86
  14. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-QdV2vcCx.mjs.map +1 -0
  15. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-DBWSe936.mjs → ng-forge-dynamic-forms-page-field.component-BHNVryIq.mjs} +3 -3
  16. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-DBWSe936.mjs.map → ng-forge-dynamic-forms-page-field.component-BHNVryIq.mjs.map} +1 -1
  17. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CCGX7m_z.mjs → ng-forge-dynamic-forms-text-field.component-Cf1mEKVW.mjs} +2 -2
  18. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CCGX7m_z.mjs.map → ng-forge-dynamic-forms-text-field.component-Cf1mEKVW.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 +104 -6
  22. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-DRYdwQcd.mjs.map +0 -1
@@ -4141,9 +4141,9 @@ class PageOrchestratorComponent {
4141
4141
  }
4142
4142
  }
4143
4143
  }
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-DBWSe936.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-DBWSe936.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-BHNVryIq.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-BHNVryIq.mjs').then(m => m.default)]] });
4145
4145
  }
4146
- i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-DBWSe936.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-BHNVryIq.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
4147
4147
  type: Component,
4148
4148
  args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
4149
4149
  @for (pageField of pageFields(); track pageField.key; let i = $index) {
@@ -6187,6 +6187,109 @@ function createSchemaFunction(schema) {
6187
6187
  };
6188
6188
  }
6189
6189
 
6190
+ /**
6191
+ * Combines the inherited "is any ancestor hidden" check into a single LogicFn.
6192
+ * Returns `undefined` when no ancestor has dynamic hidden logic.
6193
+ *
6194
+ * @internal
6195
+ */
6196
+ function combineAncestorHiddenLogics(logics) {
6197
+ if (logics.length === 0)
6198
+ return undefined;
6199
+ if (logics.length === 1)
6200
+ return logics[0];
6201
+ return (ctx) => logics.some((fn) => fn(ctx));
6202
+ }
6203
+ /**
6204
+ * Returns the `FieldTreeMappingContext` to use **for the field itself** — i.e.
6205
+ * for gating its own validators. Only applies overrides that affect the field
6206
+ * (currently `validateWhenHidden`). The field's own hidden state is NOT folded
6207
+ * into `ancestorAlwaysHidden`/`ancestorHiddenLogics` here — Angular Signal
6208
+ * Forms already exposes that through `ctx.state.hidden()`, which the gate
6209
+ * checks separately.
6210
+ *
6211
+ * Returns the parent context unchanged when the field defines no overrides —
6212
+ * saves an allocation on every leaf in the common case.
6213
+ *
6214
+ * @internal
6215
+ */
6216
+ function resolveFieldOwnContext(fieldDef, parentContext) {
6217
+ if (fieldDef.validateWhenHidden === undefined)
6218
+ return parentContext;
6219
+ if (fieldDef.validateWhenHidden === parentContext.validateWhenHidden)
6220
+ return parentContext;
6221
+ return {
6222
+ ...parentContext,
6223
+ validateWhenHidden: fieldDef.validateWhenHidden,
6224
+ };
6225
+ }
6226
+ /**
6227
+ * Returns the `FieldTreeMappingContext` to use **for the field's descendants**.
6228
+ * Builds on top of `ownContext` by folding in the field's own hidden state so
6229
+ * children can correctly skip validation when this field hides them. Used for
6230
+ * groups, arrays, and layout containers that recurse into children.
6231
+ *
6232
+ * Behavior:
6233
+ * - `hidden: true` (static) → sets `ancestorAlwaysHidden`.
6234
+ * - `logic: [{type: 'hidden', condition: true}]` → sets `ancestorAlwaysHidden`.
6235
+ * - `logic: [{type: 'hidden', condition: false}]` → no effect.
6236
+ * - `logic: [{type: 'hidden', condition: <expr>}]` → appends a reactive
6237
+ * `LogicFn` to `ancestorHiddenLogics`.
6238
+ *
6239
+ * @internal
6240
+ */
6241
+ function resolveDescendantContext(fieldDef, ownContext) {
6242
+ let ancestorAlwaysHidden = ownContext.ancestorAlwaysHidden;
6243
+ let ancestorHiddenLogics = ownContext.ancestorHiddenLogics;
6244
+ let mutated = false;
6245
+ if (fieldDef.hidden === true && !ancestorAlwaysHidden) {
6246
+ ancestorAlwaysHidden = true;
6247
+ mutated = true;
6248
+ }
6249
+ if (fieldDef.logic) {
6250
+ for (const logic of fieldDef.logic) {
6251
+ if (!isStateLogicConfig(logic) || logic.type !== 'hidden')
6252
+ continue;
6253
+ const condition = logic.condition;
6254
+ if (condition === true) {
6255
+ if (!ancestorAlwaysHidden) {
6256
+ ancestorAlwaysHidden = true;
6257
+ mutated = true;
6258
+ }
6259
+ continue;
6260
+ }
6261
+ if (condition === false)
6262
+ continue;
6263
+ // FormStateCondition strings ('formInvalid', etc.) are intended for buttons —
6264
+ // they don't make sense as a "field hidden" trigger, so we skip them here.
6265
+ if (typeof condition === 'string')
6266
+ continue;
6267
+ // Dynamic — we need a reactive logic function. createLogicFunction
6268
+ // requires an injection context, which the schema build provides.
6269
+ const fn = createLogicFunction(condition);
6270
+ ancestorHiddenLogics = ancestorHiddenLogics.length === 0 ? [fn] : [...ancestorHiddenLogics, fn];
6271
+ mutated = true;
6272
+ }
6273
+ }
6274
+ if (!mutated)
6275
+ return ownContext;
6276
+ return {
6277
+ validateWhenHidden: ownContext.validateWhenHidden,
6278
+ ancestorAlwaysHidden,
6279
+ ancestorHiddenLogics,
6280
+ };
6281
+ }
6282
+
6283
+ /**
6284
+ * Default {@link FieldTreeMappingContext} used when no inherited context is provided.
6285
+ * Mirrors the global token defaults so callers that bypass `createSchemaFromFields`
6286
+ * (e.g. nested group forms) still get sensible behavior.
6287
+ */
6288
+ const DEFAULT_FIELD_TREE_MAPPING_CONTEXT = {
6289
+ validateWhenHidden: false,
6290
+ ancestorAlwaysHidden: false,
6291
+ ancestorHiddenLogics: [],
6292
+ };
6190
6293
  /**
6191
6294
  * Applies string-specific validators to a schema path.
6192
6295
  * This function is properly typed - no casts needed internally.
@@ -6267,25 +6370,42 @@ function getNumberValidationConfig(fieldDef) {
6267
6370
  * which uses RootFormRegistryService.
6268
6371
  *
6269
6372
  * Cross-field validators are skipped at field level and applied at form level via validateTree.
6270
- */
6271
- function mapFieldToForm(fieldDef, fieldPath) {
6272
- // Layout containers (page, row, container) - flatten children to current level
6373
+ *
6374
+ * @param fieldDef Field definition to map
6375
+ * @param fieldPath Schema path for this field
6376
+ * @param inheritedContext Cascading mapping context from the parent. Field-level
6377
+ * overrides on `fieldDef` are merged on top and passed down to descendants. Defaults
6378
+ * to {@link DEFAULT_FIELD_TREE_MAPPING_CONTEXT} when omitted.
6379
+ */
6380
+ function mapFieldToForm(fieldDef, fieldPath, inheritedContext = DEFAULT_FIELD_TREE_MAPPING_CONTEXT) {
6381
+ const ownContext = resolveFieldOwnContext(fieldDef, inheritedContext);
6382
+ // Layout containers (page, row, container) - flatten children to current level.
6383
+ // Layout containers have no schema path of their own, so any hidden state on them
6384
+ // must be propagated to descendants through the cascade.
6273
6385
  if (isPageField(fieldDef) || isRowField(fieldDef) || isContainerTypedField(fieldDef)) {
6274
- mapContainerChildren(fieldDef.fields, fieldPath);
6386
+ const descendantContext = resolveDescendantContext(fieldDef, ownContext);
6387
+ mapContainerChildren(fieldDef.fields, fieldPath, descendantContext);
6275
6388
  return;
6276
6389
  }
6277
- // Group fields - map children under the group's path
6390
+ // Group fields - map children under the group's path. Groups have a schema path,
6391
+ // so Angular Signal Forms can propagate `hidden(path, ...)` calls to descendants —
6392
+ // but groups don't currently apply their own hidden state, so we still propagate
6393
+ // through the cascade as a belt-and-suspenders fallback.
6278
6394
  if (isGroupField(fieldDef)) {
6279
- mapContainerChildren(fieldDef.fields, fieldPath);
6395
+ const descendantContext = resolveDescendantContext(fieldDef, ownContext);
6396
+ mapContainerChildren(fieldDef.fields, fieldPath, descendantContext);
6280
6397
  return;
6281
6398
  }
6282
- // Array fields - use applyEach for item schema
6399
+ // Array fields - use applyEach for item schema.
6283
6400
  if (isArrayField(fieldDef)) {
6284
- mapArrayFieldToForm(fieldDef, fieldPath);
6401
+ const descendantContext = resolveDescendantContext(fieldDef, ownContext);
6402
+ mapArrayFieldToForm(fieldDef, fieldPath, descendantContext);
6285
6403
  return;
6286
6404
  }
6287
- // Leaf field - apply validation, logic, and configuration
6288
- mapLeafField(fieldDef, fieldPath);
6405
+ // Leaf field - apply validation, logic, and configuration. Use the field's OWN
6406
+ // context (no descendant propagation) because the leaf's own hidden state is
6407
+ // already exposed via `ctx.state.hidden()`.
6408
+ mapLeafField(fieldDef, fieldPath, ownContext);
6289
6409
  }
6290
6410
  /**
6291
6411
  * Maps children of a container field (page, row, group) to the form schema.
@@ -6296,50 +6416,100 @@ function mapFieldToForm(fieldDef, fieldPath) {
6296
6416
  * leaf descendants. Group/array fields consume their key and look up their own
6297
6417
  * child path on `parentPath`.
6298
6418
  */
6299
- function mapContainerChildren(fields, parentPath) {
6419
+ function mapContainerChildren(fields, parentPath, inheritedContext) {
6300
6420
  if (!fields)
6301
6421
  return;
6302
6422
  const pathRecord = parentPath;
6303
6423
  for (const field of fields) {
6304
6424
  if (isPageField(field) || isRowField(field) || isContainerTypedField(field)) {
6305
- mapContainerChildren(field.fields, parentPath);
6425
+ // Layout containers can still carry their own cascading overrides that flow
6426
+ // down to their flattened descendants.
6427
+ const layoutOwn = resolveFieldOwnContext(field, inheritedContext);
6428
+ const layoutDescendant = resolveDescendantContext(field, layoutOwn);
6429
+ mapContainerChildren(field.fields, parentPath, layoutDescendant);
6306
6430
  continue;
6307
6431
  }
6308
6432
  if (!field.key)
6309
6433
  continue;
6310
6434
  const childPath = pathRecord[field.key];
6311
6435
  if (childPath) {
6312
- mapFieldToForm(field, childPath);
6436
+ mapFieldToForm(field, childPath, inheritedContext);
6313
6437
  }
6314
6438
  }
6315
6439
  }
6440
+ /**
6441
+ * Returns true for state logic that contributes validation (currently only `required`).
6442
+ * The other state logic types (`hidden`, `disabled`, `readonly`) must always apply,
6443
+ * even on hidden fields, so they're not gated by `validateWhenHidden`.
6444
+ */
6445
+ function isValidationStateLogic(config) {
6446
+ return isStateLogicConfig(config) && config.type === 'required';
6447
+ }
6316
6448
  /**
6317
6449
  * Maps a leaf field (value-bearing field) to the form schema.
6450
+ *
6451
+ * @param context Resolved cascading context for this field. When
6452
+ * `context.validateWhenHidden` is `false`, the field's validation block is wrapped
6453
+ * in `applyWhen` keyed off `!ctx.state.hidden()`.
6318
6454
  */
6319
- function mapLeafField(fieldDef, fieldPath) {
6455
+ function mapLeafField(fieldDef, fieldPath, context) {
6320
6456
  const validationField = fieldDef;
6321
6457
  const path = fieldPath;
6322
- // Apply simple validation rules from field properties
6323
- applySimpleValidationRules(validationField, path);
6324
- if (validationField.validators) {
6325
- for (const config of validationField.validators) {
6326
- applyValidator(config, fieldPath);
6327
- }
6328
- }
6329
- // Apply logic rules
6458
+ // Always-applied: state logic (hidden/disabled/readonly), schemas, and value derivations.
6459
+ // These must run regardless of hidden state — particularly the `hidden` logic itself,
6460
+ // which establishes the very signal `applyWhen` reads below.
6461
+ applyFieldState(fieldDef, path);
6330
6462
  if (validationField.logic) {
6331
6463
  for (const config of validationField.logic) {
6332
- applyLogic(config, fieldPath);
6464
+ if (!isValidationStateLogic(config)) {
6465
+ applyLogic(config, fieldPath);
6466
+ }
6333
6467
  }
6334
6468
  }
6335
- // Apply schemas
6336
6469
  if (validationField.schemas) {
6337
6470
  for (const config of validationField.schemas) {
6338
6471
  applySchema(config, fieldPath);
6339
6472
  }
6340
6473
  }
6341
- // Apply field state configuration
6342
- applyFieldState(fieldDef, path);
6474
+ // Validation block — gated by `validateWhenHidden` when hidden.
6475
+ const applyValidation = (validationPath) => {
6476
+ applySimpleValidationRules(validationField, validationPath);
6477
+ if (validationField.validators) {
6478
+ for (const config of validationField.validators) {
6479
+ applyValidator(config, validationPath);
6480
+ }
6481
+ }
6482
+ if (validationField.logic) {
6483
+ for (const config of validationField.logic) {
6484
+ if (isValidationStateLogic(config)) {
6485
+ applyLogic(config, validationPath);
6486
+ }
6487
+ }
6488
+ }
6489
+ };
6490
+ if (context.validateWhenHidden) {
6491
+ applyValidation(path);
6492
+ return;
6493
+ }
6494
+ // An ancestor is unconditionally hidden — there is no reactive case for the
6495
+ // validators to ever fire, so we simply do not apply them.
6496
+ if (context.ancestorAlwaysHidden) {
6497
+ return;
6498
+ }
6499
+ // applyWhen conditionally applies a sub-schema. The gate combines:
6500
+ // - the field's own `state.hidden()` (set by Angular Signal Forms whenever
6501
+ // `hidden(path, ...)` was called for this field or any ancestor with a
6502
+ // path); and
6503
+ // - any ancestor dynamic hidden logic that does NOT live on a schema path
6504
+ // (layout containers — see field-tree-mapping-context for details).
6505
+ const ancestorHiddenLogic = combineAncestorHiddenLogics(context.ancestorHiddenLogics);
6506
+ applyWhen(path, (ctx) => {
6507
+ if (ctx.state.hidden())
6508
+ return false;
6509
+ if (ancestorHiddenLogic && ancestorHiddenLogic(ctx))
6510
+ return false;
6511
+ return true;
6512
+ }, schema((subPath) => applyValidation(subPath)));
6343
6513
  }
6344
6514
  /**
6345
6515
  * Applies simple validation rules from field properties.
@@ -6388,7 +6558,7 @@ function applyFieldState(fieldDef, path) {
6388
6558
  * - Heterogeneous arrays - mixed primitives and objects
6389
6559
  * - Container templates (row, group, page) that wrap children
6390
6560
  */
6391
- function mapArrayFieldToForm(arrayField, fieldPath) {
6561
+ function mapArrayFieldToForm(arrayField, fieldPath, context) {
6392
6562
  if (!isArrayField(arrayField)) {
6393
6563
  return;
6394
6564
  }
@@ -6459,20 +6629,24 @@ function mapArrayFieldToForm(arrayField, fieldPath) {
6459
6629
  for (const templateField of allObjectFields) {
6460
6630
  if (isRowField(templateField) || isPageField(templateField) || isContainerTypedField(templateField)) {
6461
6631
  // Row/page/container templates flatten their children
6462
- mapContainerChildren(templateField.fields, itemPath);
6632
+ const layoutOwn = resolveFieldOwnContext(templateField, context);
6633
+ const layoutDescendant = resolveDescendantContext(templateField, layoutOwn);
6634
+ mapContainerChildren(templateField.fields, itemPath, layoutDescendant);
6463
6635
  }
6464
6636
  else if (isGroupField(templateField)) {
6465
6637
  // Group template - access group's path first
6466
6638
  const groupKey = templateField.key;
6639
+ const groupOwn = resolveFieldOwnContext(templateField, context);
6640
+ const groupDescendant = resolveDescendantContext(templateField, groupOwn);
6467
6641
  if (groupKey) {
6468
6642
  const groupPath = pathRecord[groupKey];
6469
6643
  if (groupPath) {
6470
- mapContainerChildren(templateField.fields, groupPath);
6644
+ mapContainerChildren(templateField.fields, groupPath, groupDescendant);
6471
6645
  }
6472
6646
  }
6473
6647
  else {
6474
6648
  // No group key - apply children directly (edge case)
6475
- mapContainerChildren(templateField.fields, itemPath);
6649
+ mapContainerChildren(templateField.fields, itemPath, groupDescendant);
6476
6650
  }
6477
6651
  }
6478
6652
  else {
@@ -6481,7 +6655,7 @@ function mapArrayFieldToForm(arrayField, fieldPath) {
6481
6655
  if (fieldKey) {
6482
6656
  const fieldPathForKey = pathRecord[fieldKey];
6483
6657
  if (fieldPathForKey) {
6484
- mapFieldToForm(templateField, fieldPathForKey);
6658
+ mapFieldToForm(templateField, fieldPathForKey, context);
6485
6659
  }
6486
6660
  }
6487
6661
  }
@@ -7185,6 +7359,27 @@ function createFormLevelSchema(formLevelSchema) {
7185
7359
  });
7186
7360
  }
7187
7361
 
7362
+ /**
7363
+ * Injection token for global validation execution defaults.
7364
+ *
7365
+ * Controls whether validators run on hidden fields. By default, validation is
7366
+ * skipped when a field is hidden (`validateWhenHidden: false`).
7367
+ *
7368
+ * Configured via `withValidationExecutionDefaults()`. Per-form overrides via
7369
+ * `FormOptions.validateWhenHidden`. Per-field overrides via `FieldDef.validateWhenHidden`,
7370
+ * which inherits down the field tree.
7371
+ *
7372
+ * **Resolution order:** Field (with parent inheritance) > Form > Global.
7373
+ *
7374
+ * @internal
7375
+ */
7376
+ const VALIDATION_EXECUTION_DEFAULTS = new InjectionToken('VALIDATION_EXECUTION_DEFAULTS', {
7377
+ providedIn: 'root',
7378
+ factory: () => ({
7379
+ validateWhenHidden: false,
7380
+ }),
7381
+ });
7382
+
7188
7383
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
7189
7384
  /**
7190
7385
  * Gets a FieldTree by key, supporting nested paths with dot notation.
@@ -7229,11 +7424,17 @@ function createSchemaFromFields(fields, registry, optionsOrValidators) {
7229
7424
  // These will be available because createSchemaFromFields is called within runInInjectionContext
7230
7425
  const functionRegistry = inject(FunctionRegistryService);
7231
7426
  const logger = inject(DynamicFormLogger);
7427
+ const validationExecutionDefaults = inject(VALIDATION_EXECUTION_DEFAULTS);
7232
7428
  // Normalize options - support both old array signature and new options object
7233
7429
  const options = Array.isArray(optionsOrValidators)
7234
7430
  ? { crossFieldValidators: optionsOrValidators }
7235
7431
  : (optionsOrValidators ?? {});
7236
7432
  const { crossFieldValidators, formLevelSchema } = options;
7433
+ const rootContext = {
7434
+ validateWhenHidden: options.validateWhenHidden ?? validationExecutionDefaults.validateWhenHidden,
7435
+ ancestorAlwaysHidden: false,
7436
+ ancestorHiddenLogics: [],
7437
+ };
7237
7438
  return schema((path) => {
7238
7439
  for (const fieldDef of fields) {
7239
7440
  const valueHandling = getFieldValueHandling(fieldDef.type, registry);
@@ -7243,12 +7444,17 @@ function createSchemaFromFields(fields, registry, optionsOrValidators) {
7243
7444
  continue;
7244
7445
  }
7245
7446
  if (valueHandling === 'flatten' && hasChildFields(fieldDef)) {
7447
+ // A flatten container can still set its own cascading overrides that flow
7448
+ // down to its children — including its own hidden state which has no
7449
+ // schema path of its own.
7450
+ const flattenedOwn = resolveFieldOwnContext(fieldDef, rootContext);
7451
+ const flattenedContext = resolveDescendantContext(fieldDef, flattenedOwn);
7246
7452
  for (const childField of normalizeFieldsArray(fieldDef.fields)) {
7247
7453
  if (!childField.key)
7248
7454
  continue;
7249
7455
  const childPath = path[childField.key];
7250
7456
  if (childPath) {
7251
- mapFieldToForm(childField, childPath);
7457
+ mapFieldToForm(childField, childPath, flattenedContext);
7252
7458
  }
7253
7459
  }
7254
7460
  continue;
@@ -7261,7 +7467,7 @@ function createSchemaFromFields(fields, registry, optionsOrValidators) {
7261
7467
  // Use the new modular form mapping function
7262
7468
  // This will progressively apply validators, logic, and schemas
7263
7469
  // Cross-field logic is handled automatically via RootFormRegistryService
7264
- mapFieldToForm(fieldDef, fieldPath);
7470
+ mapFieldToForm(fieldDef, fieldPath, rootContext);
7265
7471
  }
7266
7472
  // Apply cross-field validators using validateTree
7267
7473
  if (crossFieldValidators && crossFieldValidators.length > 0) {
@@ -7869,49 +8075,54 @@ function flattenFields(fields, registry, options = {}) {
7869
8075
  * These functions are used by both `FormStateManager` and `GroupFieldComponent`
7870
8076
  * to compute flattened fields, field lookups, and default values from field definitions.
7871
8077
  *
8078
+ * Caches are keyed on the input array's reference identity (via WeakMap) rather than
8079
+ * a derived content key. Different array instances — even ones with the same field
8080
+ * keys/types — must produce fresh results so per-field content changes (e.g. updated
8081
+ * select options) propagate through the form pipeline.
8082
+ *
7872
8083
  * @returns Object containing memoized processing functions
7873
8084
  */
7874
8085
  function createContainerFieldProcessors() {
7875
- const createFieldsResolver = (preserveRows = false) => (fields, registry) => {
7876
- let key = '';
7877
- for (const f of fields) {
7878
- key += (f.key ?? '') + ':' + (f.type ?? '') + '|';
8086
+ const flattenCache = new WeakMap();
8087
+ const memoizedFlattenFields = (fields, registry) => {
8088
+ const cached = flattenCache.get(fields);
8089
+ if (cached !== undefined) {
8090
+ return cached;
7879
8091
  }
7880
- return key + registry.size + '|' + preserveRows;
8092
+ const result = flattenFields([...fields], registry);
8093
+ flattenCache.set(fields, result);
8094
+ return result;
8095
+ };
8096
+ const flattenForRenderingCache = new WeakMap();
8097
+ const memoizedFlattenFieldsForRendering = (fields, registry) => {
8098
+ const cached = flattenForRenderingCache.get(fields);
8099
+ if (cached !== undefined) {
8100
+ return cached;
8101
+ }
8102
+ const result = flattenFields([...fields], registry, { preserveRows: true });
8103
+ flattenForRenderingCache.set(fields, result);
8104
+ return result;
8105
+ };
8106
+ const keyByCache = new WeakMap();
8107
+ const memoizedKeyBy = (fields) => {
8108
+ const cached = keyByCache.get(fields);
8109
+ if (cached !== undefined) {
8110
+ return cached;
8111
+ }
8112
+ const result = keyBy(fields, 'key');
8113
+ keyByCache.set(fields, result);
8114
+ return result;
8115
+ };
8116
+ const defaultValuesCache = new WeakMap();
8117
+ const memoizedDefaultValues = (fieldsById, registry) => {
8118
+ const cached = defaultValuesCache.get(fieldsById);
8119
+ if (cached !== undefined) {
8120
+ return cached;
8121
+ }
8122
+ const result = mapValues(fieldsById, (field) => getFieldDefaultValue(field, registry));
8123
+ defaultValuesCache.set(fieldsById, result);
8124
+ return result;
7881
8125
  };
7882
- const memoizedFlattenFields = memoize((fields, registry) => flattenFields([...fields], registry), {
7883
- // registry.size is a valid cache key proxy because the field registry is populated
7884
- // once at bootstrap and never mutated at runtime. If the registry were mutable,
7885
- // we would need a content-based hash instead.
7886
- resolver: createFieldsResolver(),
7887
- maxSize: 10,
7888
- });
7889
- const memoizedFlattenFieldsForRendering = memoize((fields, registry) => flattenFields([...fields], registry, { preserveRows: true }), {
7890
- // registry.size is a valid cache key proxy because the field registry is populated
7891
- // once at bootstrap and never mutated at runtime. If the registry were mutable,
7892
- // we would need a content-based hash instead.
7893
- resolver: createFieldsResolver(true),
7894
- maxSize: 10,
7895
- });
7896
- const memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), {
7897
- resolver: (fields) => {
7898
- let key = '';
7899
- for (const f of fields) {
7900
- key += f.key + '|';
7901
- }
7902
- return key;
7903
- },
7904
- maxSize: 10,
7905
- });
7906
- const memoizedDefaultValues = memoize((fieldsById, registry) => mapValues(fieldsById, (field) => getFieldDefaultValue(field, registry)), {
7907
- // registry.size is a valid cache key proxy because the field registry is populated
7908
- // once at bootstrap and never mutated at runtime.
7909
- resolver: (fieldsById, registry) => {
7910
- const keys = Object.keys(fieldsById).sort();
7911
- return keys.join('|') + '|' + registry.size;
7912
- },
7913
- maxSize: 10,
7914
- });
7915
8126
  return { memoizedFlattenFields, memoizedFlattenFieldsForRendering, memoizedKeyBy, memoizedDefaultValues };
7916
8127
  }
7917
8128
  /**
@@ -8520,6 +8731,11 @@ function resolveFieldSync(fieldDef, context) {
8520
8731
  * Reconciles previous and current resolved fields to preserve injector instances
8521
8732
  * for fields that haven't changed type, preventing unnecessary component recreation.
8522
8733
  *
8734
+ * When a field's key, component, and injector all match the previous render but the
8735
+ * underlying `fieldDef` has changed (e.g. updated options/props), the existing
8736
+ * component/injector are preserved while the new `inputs` and `renderReady` signals
8737
+ * are adopted so prop updates flow through to the rendered component.
8738
+ *
8523
8739
  * @param prev - Previous resolved fields array
8524
8740
  * @param curr - Current resolved fields array
8525
8741
  * @returns Reconciled fields with preserved injectors where applicable
@@ -8529,8 +8745,20 @@ function reconcileFields(prev, curr) {
8529
8745
  return curr.map((field) => {
8530
8746
  const existing = prevMap.get(field.key);
8531
8747
  if (existing && existing.component === field.component && existing.injector === field.injector) {
8532
- // Truly unchanged - preserve object identity for signal stability
8533
- return existing;
8748
+ if (existing.fieldDef === field.fieldDef) {
8749
+ // Truly unchanged - preserve object identity for signal stability
8750
+ return existing;
8751
+ }
8752
+ // Same component instance + injector, but the field definition changed.
8753
+ // Preserve component identity (so ngComponentOutlet keeps the same instance)
8754
+ // while swapping in the freshly-mapped inputs/renderReady signals so prop
8755
+ // updates (e.g. select options) reach the component.
8756
+ return {
8757
+ ...existing,
8758
+ fieldDef: field.fieldDef,
8759
+ inputs: field.inputs,
8760
+ renderReady: field.renderReady,
8761
+ };
8534
8762
  }
8535
8763
  // New field, type changed, or context changed (new injector) - use new field
8536
8764
  return field;
@@ -9794,6 +10022,7 @@ class FormStateManager {
9794
10022
  return createSchemaFromFields(setup.schemaFields, setup.registry, {
9795
10023
  crossFieldValidators: crossFieldCollection.validators,
9796
10024
  formLevelSchema: config.schema,
10025
+ validateWhenHidden: this.effectiveFormOptions().validateWhenHidden,
9797
10026
  });
9798
10027
  }
9799
10028
  if (config.schema) {
@@ -9897,7 +10126,12 @@ class FormStateManager {
9897
10126
  /**
9898
10127
  * Phase-aware field source that coordinates component lifecycle with form updates.
9899
10128
  * Teardown/Applying → intersection of old/new fields; Restoring → all new fields.
9900
- * Uses key+type equality to avoid spurious emissions during rapid transitions.
10129
+ *
10130
+ * Uses per-field reference equality to suppress spurious emissions when the same
10131
+ * field definitions are returned (e.g. memoized intersections during transitions)
10132
+ * while still triggering re-resolution when the underlying field definition
10133
+ * references change (e.g. a config swap that updates options/props for a
10134
+ * same-keyed field).
9901
10135
  */
9902
10136
  fieldsSource = computed(() => {
9903
10137
  const state = this.machine.state();
@@ -9918,7 +10152,7 @@ class FormStateManager {
9918
10152
  return true;
9919
10153
  if (a.length !== b.length)
9920
10154
  return false;
9921
- return a.every((field, i) => field.key === b[i].key && field.type === b[i].type);
10155
+ return a.every((field, i) => field === b[i]);
9922
10156
  } });
9923
10157
  /**
9924
10158
  * Injector for field components with FIELD_SIGNAL_CONTEXT.
@@ -14858,31 +15092,31 @@ const BUILT_IN_FIELDS = [
14858
15092
  // `row` is a virtual field type: it maps to ContainerFieldComponent, and
14859
15093
  // the rowFieldMapper injects a `{ type: 'row' }` wrapper so the container
14860
15094
  // renders the flex/grid layout via RowWrapperComponent.
14861
- loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-BMDGh8bN.mjs'),
15095
+ loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-D4Fpr2ri.mjs'),
14862
15096
  mapper: rowFieldMapper,
14863
15097
  valueHandling: 'flatten',
14864
15098
  },
14865
15099
  {
14866
15100
  name: 'group',
14867
- loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-BR7nCNmT.mjs'),
15101
+ loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-BxswrhOc.mjs'),
14868
15102
  mapper: groupFieldMapper,
14869
15103
  valueHandling: 'include',
14870
15104
  },
14871
15105
  {
14872
15106
  name: 'array',
14873
- loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-DYPTXG9B.mjs'),
15107
+ loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-CltYGHg9.mjs'),
14874
15108
  mapper: arrayFieldMapper,
14875
15109
  valueHandling: 'include',
14876
15110
  },
14877
15111
  {
14878
15112
  name: 'page',
14879
- loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-DBWSe936.mjs'),
15113
+ loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-BHNVryIq.mjs'),
14880
15114
  mapper: pageFieldMapper,
14881
15115
  valueHandling: 'flatten',
14882
15116
  },
14883
15117
  {
14884
15118
  name: 'text',
14885
- loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-CCGX7m_z.mjs'),
15119
+ loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-Cf1mEKVW.mjs'),
14886
15120
  mapper: textFieldMapper,
14887
15121
  valueHandling: 'exclude',
14888
15122
  ...DISPLAY_FIELD_TYPES_BASE,
@@ -14894,7 +15128,7 @@ const BUILT_IN_FIELDS = [
14894
15128
  },
14895
15129
  {
14896
15130
  name: 'container',
14897
- loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-BMDGh8bN.mjs'),
15131
+ loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-D4Fpr2ri.mjs'),
14898
15132
  mapper: containerFieldMapper,
14899
15133
  valueHandling: 'flatten',
14900
15134
  },
@@ -14902,7 +15136,7 @@ const BUILT_IN_FIELDS = [
14902
15136
  const BUILT_IN_WRAPPERS = [
14903
15137
  {
14904
15138
  wrapperName: 'css',
14905
- loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-hRiX9Jmf.mjs'),
15139
+ loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-mdfySFjX.mjs'),
14906
15140
  },
14907
15141
  {
14908
15142
  wrapperName: 'row',
@@ -15212,6 +15446,48 @@ function withValueExclusionDefaults(config) {
15212
15446
  return createFeature('value-exclusion', [{ provide: VALUE_EXCLUSION_DEFAULTS, useValue: resolved }]);
15213
15447
  }
15214
15448
 
15449
+ /**
15450
+ * Configures global validation execution defaults — when validators should run
15451
+ * relative to a field's reactive state.
15452
+ *
15453
+ * By default, validators are **skipped** for hidden fields (`validateWhenHidden: false`),
15454
+ * matching what most users expect: a hidden field is inert. Use this feature to
15455
+ * restore the legacy behavior where hidden fields still validate.
15456
+ *
15457
+ * @example Default behavior — no feature needed
15458
+ * ```typescript
15459
+ * provideDynamicForm(...withMaterialFields())
15460
+ * // hidden fields skip validation
15461
+ * ```
15462
+ *
15463
+ * @example Restore legacy validation-on-hidden behavior
15464
+ * ```typescript
15465
+ * provideDynamicForm(
15466
+ * ...withMaterialFields(),
15467
+ * withValidationExecutionDefaults({ validateWhenHidden: true })
15468
+ * )
15469
+ * ```
15470
+ *
15471
+ * @remarks
15472
+ * **Precedence rules:**
15473
+ * 1. Per-field `FieldDef.validateWhenHidden` — wins for that field; inherits down to descendants
15474
+ * that don't override.
15475
+ * 2. Per-form `FormOptions.validateWhenHidden` — root inherited value for the form.
15476
+ * 3. Global `withValidationExecutionDefaults()` — fallback when no form/field setting.
15477
+ * 4. No feature configured — uses token default (`validateWhenHidden: false`).
15478
+ *
15479
+ * @param config - Partial override. Unspecified properties keep their defaults.
15480
+ * @returns A `DynamicFormFeature` that configures validation execution defaults.
15481
+ *
15482
+ * @public
15483
+ */
15484
+ function withValidationExecutionDefaults(config) {
15485
+ const resolved = {
15486
+ validateWhenHidden: config?.validateWhenHidden ?? false,
15487
+ };
15488
+ return createFeature('validation-execution', [{ provide: VALIDATION_EXECUTION_DEFAULTS, useValue: resolved }]);
15489
+ }
15490
+
15215
15491
  /**
15216
15492
  * Zero-cost type carrier used in wrapper registrations.
15217
15493
  *
@@ -15934,5 +16210,5 @@ function extractErrorParams(error) {
15934
16210
  * Generated bundle index. Do not edit.
15935
16211
  */
15936
16212
 
15937
- 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 };
15938
- //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-DRYdwQcd.mjs.map
16213
+ 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 };
16214
+ //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-QdV2vcCx.mjs.map