@ng-forge/dynamic-forms 0.8.0 → 0.9.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-CWxCWVdz.mjs → ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-CWxCWVdz.mjs.map → ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-PozYj2JC.mjs → ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-PozYj2JC.mjs.map → ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-DW_P7w-b.mjs → ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-utils-DW_P7w-b.mjs.map → ng-forge-dynamic-forms-container-utils-D_nNzKo1.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-DrtHSaVV.mjs → ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs} +2 -2
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-CSx0gwFc.mjs → ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-CSx0gwFc.mjs.map → ng-forge-dynamic-forms-emit-initialization-DtkxP7pE.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-2yRmHAUp.mjs → ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-2yRmHAUp.mjs.map → ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +629 -54
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-Ll4W2Z_n.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs} +662 -110
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-B_CxX0T-.mjs → ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-B_CxX0T-.mjs.map → ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-BIvEUoia.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-rJMtKX2X.mjs → ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-text-field.component-rJMtKX2X.mjs.map → ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +2 -2
- package/types/ng-forge-dynamic-forms-integration.d.ts +380 -8
- package/types/ng-forge-dynamic-forms.d.ts +106 -8
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-DrtHSaVV.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-Ll4W2Z_n.mjs.map +0 -1
|
@@ -324,8 +324,7 @@ function getDeclaredInputs(componentType) {
|
|
|
324
324
|
* avoiding the reflection scan on each call is meaningful under heavy typing.
|
|
325
325
|
*/
|
|
326
326
|
function setInputIfDeclared(ref, inputName, value) {
|
|
327
|
-
|
|
328
|
-
if (declared.has(inputName)) {
|
|
327
|
+
if (getDeclaredInputs(ref.componentType).has(inputName)) {
|
|
329
328
|
ref.setInput(inputName, value);
|
|
330
329
|
}
|
|
331
330
|
}
|
|
@@ -545,6 +544,9 @@ function createWrapperChainController(opts) {
|
|
|
545
544
|
const state = createStateSignal(opts);
|
|
546
545
|
const mounted = { value: null };
|
|
547
546
|
let refs = [];
|
|
547
|
+
// INVARIANT: toObservable schedules emissions as microtasks, so `renderInnermost`
|
|
548
|
+
// runs outside any parent CD pass. Don't swap for sync signal wiring without
|
|
549
|
+
// restoring df-field-outlet's detectChanges — NG0950 would re-surface.
|
|
548
550
|
buildEmissionStream(state, deps)
|
|
549
551
|
.pipe(takeUntilDestroyed(deps.destroyRef))
|
|
550
552
|
.subscribe((emission) => {
|
|
@@ -841,6 +843,8 @@ class DfFieldOutlet {
|
|
|
841
843
|
injector: createWrapperAwareInjector(resolved.injector, slot.injector),
|
|
842
844
|
});
|
|
843
845
|
this.fieldSlot = slot;
|
|
846
|
+
// setInput dirties the signal synchronously; the next CD pass reads
|
|
847
|
+
// it. Safe outside CD — see wrapper-chain-controller scheduling.
|
|
844
848
|
this.pushRawInputs(this.fieldRef, this.rawInputs());
|
|
845
849
|
},
|
|
846
850
|
});
|
|
@@ -904,12 +908,12 @@ class DfFieldOutlet {
|
|
|
904
908
|
}
|
|
905
909
|
}
|
|
906
910
|
pushRawInputs(ref, rawInputs) {
|
|
907
|
-
//
|
|
911
|
+
// setInput is lenient in Angular 21 — unknown keys silently drop (not NG0303). reflectComponentType does NOT include hostDirective forwarded inputs (angular/angular#49734), so a declared-input filter here would skip every value flowing through NgForgeFieldShell / NgForgeField / NgForgeAction. Mapper-as-contract is enforced by the *_INPUTS lockstep type assertions, not at runtime here.
|
|
908
912
|
const last = this.lastPushedInputs;
|
|
909
913
|
for (const [key, value] of Object.entries(rawInputs)) {
|
|
910
914
|
if (last && last[key] === value)
|
|
911
915
|
continue;
|
|
912
|
-
|
|
916
|
+
ref.setInput(key, value);
|
|
913
917
|
}
|
|
914
918
|
this.lastPushedInputs = rawInputs;
|
|
915
919
|
}
|
|
@@ -4141,9 +4145,9 @@ class PageOrchestratorComponent {
|
|
|
4141
4145
|
}
|
|
4142
4146
|
}
|
|
4143
4147
|
}
|
|
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-
|
|
4148
|
+
`, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs').then(m => m.default)]] });
|
|
4145
4149
|
}
|
|
4146
|
-
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-
|
|
4150
|
+
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.8", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
|
|
4147
4151
|
type: Component,
|
|
4148
4152
|
args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
|
|
4149
4153
|
@for (pageField of pageFields(); track pageField.key; let i = $index) {
|
|
@@ -6187,6 +6191,109 @@ function createSchemaFunction(schema) {
|
|
|
6187
6191
|
};
|
|
6188
6192
|
}
|
|
6189
6193
|
|
|
6194
|
+
/**
|
|
6195
|
+
* Combines the inherited "is any ancestor hidden" check into a single LogicFn.
|
|
6196
|
+
* Returns `undefined` when no ancestor has dynamic hidden logic.
|
|
6197
|
+
*
|
|
6198
|
+
* @internal
|
|
6199
|
+
*/
|
|
6200
|
+
function combineAncestorHiddenLogics(logics) {
|
|
6201
|
+
if (logics.length === 0)
|
|
6202
|
+
return undefined;
|
|
6203
|
+
if (logics.length === 1)
|
|
6204
|
+
return logics[0];
|
|
6205
|
+
return (ctx) => logics.some((fn) => fn(ctx));
|
|
6206
|
+
}
|
|
6207
|
+
/**
|
|
6208
|
+
* Returns the `FieldTreeMappingContext` to use **for the field itself** — i.e.
|
|
6209
|
+
* for gating its own validators. Only applies overrides that affect the field
|
|
6210
|
+
* (currently `validateWhenHidden`). The field's own hidden state is NOT folded
|
|
6211
|
+
* into `ancestorAlwaysHidden`/`ancestorHiddenLogics` here — Angular Signal
|
|
6212
|
+
* Forms already exposes that through `ctx.state.hidden()`, which the gate
|
|
6213
|
+
* checks separately.
|
|
6214
|
+
*
|
|
6215
|
+
* Returns the parent context unchanged when the field defines no overrides —
|
|
6216
|
+
* saves an allocation on every leaf in the common case.
|
|
6217
|
+
*
|
|
6218
|
+
* @internal
|
|
6219
|
+
*/
|
|
6220
|
+
function resolveFieldOwnContext(fieldDef, parentContext) {
|
|
6221
|
+
if (fieldDef.validateWhenHidden === undefined)
|
|
6222
|
+
return parentContext;
|
|
6223
|
+
if (fieldDef.validateWhenHidden === parentContext.validateWhenHidden)
|
|
6224
|
+
return parentContext;
|
|
6225
|
+
return {
|
|
6226
|
+
...parentContext,
|
|
6227
|
+
validateWhenHidden: fieldDef.validateWhenHidden,
|
|
6228
|
+
};
|
|
6229
|
+
}
|
|
6230
|
+
/**
|
|
6231
|
+
* Returns the `FieldTreeMappingContext` to use **for the field's descendants**.
|
|
6232
|
+
* Builds on top of `ownContext` by folding in the field's own hidden state so
|
|
6233
|
+
* children can correctly skip validation when this field hides them. Used for
|
|
6234
|
+
* groups, arrays, and layout containers that recurse into children.
|
|
6235
|
+
*
|
|
6236
|
+
* Behavior:
|
|
6237
|
+
* - `hidden: true` (static) → sets `ancestorAlwaysHidden`.
|
|
6238
|
+
* - `logic: [{type: 'hidden', condition: true}]` → sets `ancestorAlwaysHidden`.
|
|
6239
|
+
* - `logic: [{type: 'hidden', condition: false}]` → no effect.
|
|
6240
|
+
* - `logic: [{type: 'hidden', condition: <expr>}]` → appends a reactive
|
|
6241
|
+
* `LogicFn` to `ancestorHiddenLogics`.
|
|
6242
|
+
*
|
|
6243
|
+
* @internal
|
|
6244
|
+
*/
|
|
6245
|
+
function resolveDescendantContext(fieldDef, ownContext) {
|
|
6246
|
+
let ancestorAlwaysHidden = ownContext.ancestorAlwaysHidden;
|
|
6247
|
+
let ancestorHiddenLogics = ownContext.ancestorHiddenLogics;
|
|
6248
|
+
let mutated = false;
|
|
6249
|
+
if (fieldDef.hidden === true && !ancestorAlwaysHidden) {
|
|
6250
|
+
ancestorAlwaysHidden = true;
|
|
6251
|
+
mutated = true;
|
|
6252
|
+
}
|
|
6253
|
+
if (fieldDef.logic) {
|
|
6254
|
+
for (const logic of fieldDef.logic) {
|
|
6255
|
+
if (!isStateLogicConfig(logic) || logic.type !== 'hidden')
|
|
6256
|
+
continue;
|
|
6257
|
+
const condition = logic.condition;
|
|
6258
|
+
if (condition === true) {
|
|
6259
|
+
if (!ancestorAlwaysHidden) {
|
|
6260
|
+
ancestorAlwaysHidden = true;
|
|
6261
|
+
mutated = true;
|
|
6262
|
+
}
|
|
6263
|
+
continue;
|
|
6264
|
+
}
|
|
6265
|
+
if (condition === false)
|
|
6266
|
+
continue;
|
|
6267
|
+
// FormStateCondition strings ('formInvalid', etc.) are intended for buttons —
|
|
6268
|
+
// they don't make sense as a "field hidden" trigger, so we skip them here.
|
|
6269
|
+
if (typeof condition === 'string')
|
|
6270
|
+
continue;
|
|
6271
|
+
// Dynamic — we need a reactive logic function. createLogicFunction
|
|
6272
|
+
// requires an injection context, which the schema build provides.
|
|
6273
|
+
const fn = createLogicFunction(condition);
|
|
6274
|
+
ancestorHiddenLogics = ancestorHiddenLogics.length === 0 ? [fn] : [...ancestorHiddenLogics, fn];
|
|
6275
|
+
mutated = true;
|
|
6276
|
+
}
|
|
6277
|
+
}
|
|
6278
|
+
if (!mutated)
|
|
6279
|
+
return ownContext;
|
|
6280
|
+
return {
|
|
6281
|
+
validateWhenHidden: ownContext.validateWhenHidden,
|
|
6282
|
+
ancestorAlwaysHidden,
|
|
6283
|
+
ancestorHiddenLogics,
|
|
6284
|
+
};
|
|
6285
|
+
}
|
|
6286
|
+
|
|
6287
|
+
/**
|
|
6288
|
+
* Default {@link FieldTreeMappingContext} used when no inherited context is provided.
|
|
6289
|
+
* Mirrors the global token defaults so callers that bypass `createSchemaFromFields`
|
|
6290
|
+
* (e.g. nested group forms) still get sensible behavior.
|
|
6291
|
+
*/
|
|
6292
|
+
const DEFAULT_FIELD_TREE_MAPPING_CONTEXT = {
|
|
6293
|
+
validateWhenHidden: false,
|
|
6294
|
+
ancestorAlwaysHidden: false,
|
|
6295
|
+
ancestorHiddenLogics: [],
|
|
6296
|
+
};
|
|
6190
6297
|
/**
|
|
6191
6298
|
* Applies string-specific validators to a schema path.
|
|
6192
6299
|
* This function is properly typed - no casts needed internally.
|
|
@@ -6267,25 +6374,42 @@ function getNumberValidationConfig(fieldDef) {
|
|
|
6267
6374
|
* which uses RootFormRegistryService.
|
|
6268
6375
|
*
|
|
6269
6376
|
* Cross-field validators are skipped at field level and applied at form level via validateTree.
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6377
|
+
*
|
|
6378
|
+
* @param fieldDef Field definition to map
|
|
6379
|
+
* @param fieldPath Schema path for this field
|
|
6380
|
+
* @param inheritedContext Cascading mapping context from the parent. Field-level
|
|
6381
|
+
* overrides on `fieldDef` are merged on top and passed down to descendants. Defaults
|
|
6382
|
+
* to {@link DEFAULT_FIELD_TREE_MAPPING_CONTEXT} when omitted.
|
|
6383
|
+
*/
|
|
6384
|
+
function mapFieldToForm(fieldDef, fieldPath, inheritedContext = DEFAULT_FIELD_TREE_MAPPING_CONTEXT) {
|
|
6385
|
+
const ownContext = resolveFieldOwnContext(fieldDef, inheritedContext);
|
|
6386
|
+
// Layout containers (page, row, container) - flatten children to current level.
|
|
6387
|
+
// Layout containers have no schema path of their own, so any hidden state on them
|
|
6388
|
+
// must be propagated to descendants through the cascade.
|
|
6273
6389
|
if (isPageField(fieldDef) || isRowField(fieldDef) || isContainerTypedField(fieldDef)) {
|
|
6274
|
-
|
|
6390
|
+
const descendantContext = resolveDescendantContext(fieldDef, ownContext);
|
|
6391
|
+
mapContainerChildren(fieldDef.fields, fieldPath, descendantContext);
|
|
6275
6392
|
return;
|
|
6276
6393
|
}
|
|
6277
|
-
// Group fields - map children under the group's path
|
|
6394
|
+
// Group fields - map children under the group's path. Groups have a schema path,
|
|
6395
|
+
// so Angular Signal Forms can propagate `hidden(path, ...)` calls to descendants —
|
|
6396
|
+
// but groups don't currently apply their own hidden state, so we still propagate
|
|
6397
|
+
// through the cascade as a belt-and-suspenders fallback.
|
|
6278
6398
|
if (isGroupField(fieldDef)) {
|
|
6279
|
-
|
|
6399
|
+
const descendantContext = resolveDescendantContext(fieldDef, ownContext);
|
|
6400
|
+
mapContainerChildren(fieldDef.fields, fieldPath, descendantContext);
|
|
6280
6401
|
return;
|
|
6281
6402
|
}
|
|
6282
|
-
// Array fields - use applyEach for item schema
|
|
6403
|
+
// Array fields - use applyEach for item schema.
|
|
6283
6404
|
if (isArrayField(fieldDef)) {
|
|
6284
|
-
|
|
6405
|
+
const descendantContext = resolveDescendantContext(fieldDef, ownContext);
|
|
6406
|
+
mapArrayFieldToForm(fieldDef, fieldPath, descendantContext);
|
|
6285
6407
|
return;
|
|
6286
6408
|
}
|
|
6287
|
-
// Leaf field - apply validation, logic, and configuration
|
|
6288
|
-
|
|
6409
|
+
// Leaf field - apply validation, logic, and configuration. Use the field's OWN
|
|
6410
|
+
// context (no descendant propagation) because the leaf's own hidden state is
|
|
6411
|
+
// already exposed via `ctx.state.hidden()`.
|
|
6412
|
+
mapLeafField(fieldDef, fieldPath, ownContext);
|
|
6289
6413
|
}
|
|
6290
6414
|
/**
|
|
6291
6415
|
* Maps children of a container field (page, row, group) to the form schema.
|
|
@@ -6296,50 +6420,100 @@ function mapFieldToForm(fieldDef, fieldPath) {
|
|
|
6296
6420
|
* leaf descendants. Group/array fields consume their key and look up their own
|
|
6297
6421
|
* child path on `parentPath`.
|
|
6298
6422
|
*/
|
|
6299
|
-
function mapContainerChildren(fields, parentPath) {
|
|
6423
|
+
function mapContainerChildren(fields, parentPath, inheritedContext) {
|
|
6300
6424
|
if (!fields)
|
|
6301
6425
|
return;
|
|
6302
6426
|
const pathRecord = parentPath;
|
|
6303
6427
|
for (const field of fields) {
|
|
6304
6428
|
if (isPageField(field) || isRowField(field) || isContainerTypedField(field)) {
|
|
6305
|
-
|
|
6429
|
+
// Layout containers can still carry their own cascading overrides that flow
|
|
6430
|
+
// down to their flattened descendants.
|
|
6431
|
+
const layoutOwn = resolveFieldOwnContext(field, inheritedContext);
|
|
6432
|
+
const layoutDescendant = resolveDescendantContext(field, layoutOwn);
|
|
6433
|
+
mapContainerChildren(field.fields, parentPath, layoutDescendant);
|
|
6306
6434
|
continue;
|
|
6307
6435
|
}
|
|
6308
6436
|
if (!field.key)
|
|
6309
6437
|
continue;
|
|
6310
6438
|
const childPath = pathRecord[field.key];
|
|
6311
6439
|
if (childPath) {
|
|
6312
|
-
mapFieldToForm(field, childPath);
|
|
6440
|
+
mapFieldToForm(field, childPath, inheritedContext);
|
|
6313
6441
|
}
|
|
6314
6442
|
}
|
|
6315
6443
|
}
|
|
6444
|
+
/**
|
|
6445
|
+
* Returns true for state logic that contributes validation (currently only `required`).
|
|
6446
|
+
* The other state logic types (`hidden`, `disabled`, `readonly`) must always apply,
|
|
6447
|
+
* even on hidden fields, so they're not gated by `validateWhenHidden`.
|
|
6448
|
+
*/
|
|
6449
|
+
function isValidationStateLogic(config) {
|
|
6450
|
+
return isStateLogicConfig(config) && config.type === 'required';
|
|
6451
|
+
}
|
|
6316
6452
|
/**
|
|
6317
6453
|
* Maps a leaf field (value-bearing field) to the form schema.
|
|
6454
|
+
*
|
|
6455
|
+
* @param context Resolved cascading context for this field. When
|
|
6456
|
+
* `context.validateWhenHidden` is `false`, the field's validation block is wrapped
|
|
6457
|
+
* in `applyWhen` keyed off `!ctx.state.hidden()`.
|
|
6318
6458
|
*/
|
|
6319
|
-
function mapLeafField(fieldDef, fieldPath) {
|
|
6459
|
+
function mapLeafField(fieldDef, fieldPath, context) {
|
|
6320
6460
|
const validationField = fieldDef;
|
|
6321
6461
|
const path = fieldPath;
|
|
6322
|
-
//
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
applyValidator(config, fieldPath);
|
|
6327
|
-
}
|
|
6328
|
-
}
|
|
6329
|
-
// Apply logic rules
|
|
6462
|
+
// Always-applied: state logic (hidden/disabled/readonly), schemas, and value derivations.
|
|
6463
|
+
// These must run regardless of hidden state — particularly the `hidden` logic itself,
|
|
6464
|
+
// which establishes the very signal `applyWhen` reads below.
|
|
6465
|
+
applyFieldState(fieldDef, path);
|
|
6330
6466
|
if (validationField.logic) {
|
|
6331
6467
|
for (const config of validationField.logic) {
|
|
6332
|
-
|
|
6468
|
+
if (!isValidationStateLogic(config)) {
|
|
6469
|
+
applyLogic(config, fieldPath);
|
|
6470
|
+
}
|
|
6333
6471
|
}
|
|
6334
6472
|
}
|
|
6335
|
-
// Apply schemas
|
|
6336
6473
|
if (validationField.schemas) {
|
|
6337
6474
|
for (const config of validationField.schemas) {
|
|
6338
6475
|
applySchema(config, fieldPath);
|
|
6339
6476
|
}
|
|
6340
6477
|
}
|
|
6341
|
-
//
|
|
6342
|
-
|
|
6478
|
+
// Validation block — gated by `validateWhenHidden` when hidden.
|
|
6479
|
+
const applyValidation = (validationPath) => {
|
|
6480
|
+
applySimpleValidationRules(validationField, validationPath);
|
|
6481
|
+
if (validationField.validators) {
|
|
6482
|
+
for (const config of validationField.validators) {
|
|
6483
|
+
applyValidator(config, validationPath);
|
|
6484
|
+
}
|
|
6485
|
+
}
|
|
6486
|
+
if (validationField.logic) {
|
|
6487
|
+
for (const config of validationField.logic) {
|
|
6488
|
+
if (isValidationStateLogic(config)) {
|
|
6489
|
+
applyLogic(config, validationPath);
|
|
6490
|
+
}
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6493
|
+
};
|
|
6494
|
+
if (context.validateWhenHidden) {
|
|
6495
|
+
applyValidation(path);
|
|
6496
|
+
return;
|
|
6497
|
+
}
|
|
6498
|
+
// An ancestor is unconditionally hidden — there is no reactive case for the
|
|
6499
|
+
// validators to ever fire, so we simply do not apply them.
|
|
6500
|
+
if (context.ancestorAlwaysHidden) {
|
|
6501
|
+
return;
|
|
6502
|
+
}
|
|
6503
|
+
// applyWhen conditionally applies a sub-schema. The gate combines:
|
|
6504
|
+
// - the field's own `state.hidden()` (set by Angular Signal Forms whenever
|
|
6505
|
+
// `hidden(path, ...)` was called for this field or any ancestor with a
|
|
6506
|
+
// path); and
|
|
6507
|
+
// - any ancestor dynamic hidden logic that does NOT live on a schema path
|
|
6508
|
+
// (layout containers — see field-tree-mapping-context for details).
|
|
6509
|
+
const ancestorHiddenLogic = combineAncestorHiddenLogics(context.ancestorHiddenLogics);
|
|
6510
|
+
applyWhen(path, (ctx) => {
|
|
6511
|
+
if (ctx.state.hidden())
|
|
6512
|
+
return false;
|
|
6513
|
+
if (ancestorHiddenLogic && ancestorHiddenLogic(ctx))
|
|
6514
|
+
return false;
|
|
6515
|
+
return true;
|
|
6516
|
+
}, schema((subPath) => applyValidation(subPath)));
|
|
6343
6517
|
}
|
|
6344
6518
|
/**
|
|
6345
6519
|
* Applies simple validation rules from field properties.
|
|
@@ -6388,7 +6562,7 @@ function applyFieldState(fieldDef, path) {
|
|
|
6388
6562
|
* - Heterogeneous arrays - mixed primitives and objects
|
|
6389
6563
|
* - Container templates (row, group, page) that wrap children
|
|
6390
6564
|
*/
|
|
6391
|
-
function mapArrayFieldToForm(arrayField, fieldPath) {
|
|
6565
|
+
function mapArrayFieldToForm(arrayField, fieldPath, context) {
|
|
6392
6566
|
if (!isArrayField(arrayField)) {
|
|
6393
6567
|
return;
|
|
6394
6568
|
}
|
|
@@ -6459,20 +6633,24 @@ function mapArrayFieldToForm(arrayField, fieldPath) {
|
|
|
6459
6633
|
for (const templateField of allObjectFields) {
|
|
6460
6634
|
if (isRowField(templateField) || isPageField(templateField) || isContainerTypedField(templateField)) {
|
|
6461
6635
|
// Row/page/container templates flatten their children
|
|
6462
|
-
|
|
6636
|
+
const layoutOwn = resolveFieldOwnContext(templateField, context);
|
|
6637
|
+
const layoutDescendant = resolveDescendantContext(templateField, layoutOwn);
|
|
6638
|
+
mapContainerChildren(templateField.fields, itemPath, layoutDescendant);
|
|
6463
6639
|
}
|
|
6464
6640
|
else if (isGroupField(templateField)) {
|
|
6465
6641
|
// Group template - access group's path first
|
|
6466
6642
|
const groupKey = templateField.key;
|
|
6643
|
+
const groupOwn = resolveFieldOwnContext(templateField, context);
|
|
6644
|
+
const groupDescendant = resolveDescendantContext(templateField, groupOwn);
|
|
6467
6645
|
if (groupKey) {
|
|
6468
6646
|
const groupPath = pathRecord[groupKey];
|
|
6469
6647
|
if (groupPath) {
|
|
6470
|
-
mapContainerChildren(templateField.fields, groupPath);
|
|
6648
|
+
mapContainerChildren(templateField.fields, groupPath, groupDescendant);
|
|
6471
6649
|
}
|
|
6472
6650
|
}
|
|
6473
6651
|
else {
|
|
6474
6652
|
// No group key - apply children directly (edge case)
|
|
6475
|
-
mapContainerChildren(templateField.fields, itemPath);
|
|
6653
|
+
mapContainerChildren(templateField.fields, itemPath, groupDescendant);
|
|
6476
6654
|
}
|
|
6477
6655
|
}
|
|
6478
6656
|
else {
|
|
@@ -6481,7 +6659,7 @@ function mapArrayFieldToForm(arrayField, fieldPath) {
|
|
|
6481
6659
|
if (fieldKey) {
|
|
6482
6660
|
const fieldPathForKey = pathRecord[fieldKey];
|
|
6483
6661
|
if (fieldPathForKey) {
|
|
6484
|
-
mapFieldToForm(templateField, fieldPathForKey);
|
|
6662
|
+
mapFieldToForm(templateField, fieldPathForKey, context);
|
|
6485
6663
|
}
|
|
6486
6664
|
}
|
|
6487
6665
|
}
|
|
@@ -6899,6 +7077,103 @@ function mapValues(obj, fn) {
|
|
|
6899
7077
|
return acc;
|
|
6900
7078
|
}, {});
|
|
6901
7079
|
}
|
|
7080
|
+
/**
|
|
7081
|
+
* Deep-merges `value` into `defaults`, producing a new object that contains every
|
|
7082
|
+
* key from `defaults` plus any overrides from `value`. Plain-object values at
|
|
7083
|
+
* matching keys are merged recursively; primitives, dates, and class instances
|
|
7084
|
+
* in `value` replace the corresponding default wholesale.
|
|
7085
|
+
*
|
|
7086
|
+
* Arrays whose lengths match between `defaults` and `value` are merged
|
|
7087
|
+
* positionally: when both `defaults[i]` and `value[i]` are plain objects, they
|
|
7088
|
+
* are deep-merged so partial array-item values don't drop sibling sub-field
|
|
7089
|
+
* keys. When the lengths differ — the runtime add/insert/remove case — the
|
|
7090
|
+
* value array is returned by reference so Signal Forms doesn't see a fresh
|
|
7091
|
+
* array of fresh inner objects on every form-value write (which would rebuild
|
|
7092
|
+
* item bindings and desync the rendered rows from their FieldTrees).
|
|
7093
|
+
*
|
|
7094
|
+
* Used by `FormStateManager.entity` so a partial input value (e.g. a nested
|
|
7095
|
+
* group object missing one of its declared sub-fields, or an array item missing
|
|
7096
|
+
* one of its declared sub-fields) does not orphan the absent sub-field in
|
|
7097
|
+
* Angular Signal Forms' validation graph.
|
|
7098
|
+
*
|
|
7099
|
+
* @example
|
|
7100
|
+
* ```typescript
|
|
7101
|
+
* deepMergeDefaults(
|
|
7102
|
+
* { a: { line1: '', line2: '', city: '' }, b: 0 },
|
|
7103
|
+
* { a: { line1: 'X', city: 'Y' }, b: 1 },
|
|
7104
|
+
* );
|
|
7105
|
+
* // { a: { line1: 'X', line2: '', city: 'Y' }, b: 1 }
|
|
7106
|
+
*
|
|
7107
|
+
* deepMergeDefaults(
|
|
7108
|
+
* { items: [{ checkboxA: false, checkboxB: false }] },
|
|
7109
|
+
* { items: [{ checkboxA: true }] },
|
|
7110
|
+
* );
|
|
7111
|
+
* // { items: [{ checkboxA: true, checkboxB: false }] }
|
|
7112
|
+
* ```
|
|
7113
|
+
*/
|
|
7114
|
+
function deepMergeDefaults(defaults, value) {
|
|
7115
|
+
if (value == null)
|
|
7116
|
+
return { ...defaults };
|
|
7117
|
+
const result = { ...defaults };
|
|
7118
|
+
for (const key of Object.keys(value)) {
|
|
7119
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype')
|
|
7120
|
+
continue;
|
|
7121
|
+
const incoming = value[key];
|
|
7122
|
+
const existing = result[key];
|
|
7123
|
+
if (isPlainObject(existing) && isPlainObject(incoming)) {
|
|
7124
|
+
result[key] = deepMergeDefaults(existing, incoming);
|
|
7125
|
+
}
|
|
7126
|
+
else if (Array.isArray(existing) && Array.isArray(incoming)) {
|
|
7127
|
+
result[key] = mergeArrayDefaults(existing, incoming);
|
|
7128
|
+
}
|
|
7129
|
+
else {
|
|
7130
|
+
result[key] = incoming;
|
|
7131
|
+
}
|
|
7132
|
+
}
|
|
7133
|
+
return result;
|
|
7134
|
+
}
|
|
7135
|
+
/**
|
|
7136
|
+
* @internal
|
|
7137
|
+
* Merges a value array into a defaults array element-wise when their lengths
|
|
7138
|
+
* match. When lengths differ — the runtime case: prepend, insert, append,
|
|
7139
|
+
* remove — we return the value array reference as-is so we don't produce
|
|
7140
|
+
* fresh inner object references for items that already have every declared
|
|
7141
|
+
* sub-field key. Replacing references during the linkedSignal source
|
|
7142
|
+
* recompute that runs after Signal Forms wrote the array would otherwise
|
|
7143
|
+
* desync item FieldTrees from the rendered DOM rows.
|
|
7144
|
+
*
|
|
7145
|
+
* When lengths match (the declared/initial-value path the array fix targets)
|
|
7146
|
+
* each `defaults[i]`/`value[i]` pair is deep-merged when both are plain
|
|
7147
|
+
* objects so partial array-item values don't drop sibling sub-field keys.
|
|
7148
|
+
*/
|
|
7149
|
+
function mergeArrayDefaults(defaults, value) {
|
|
7150
|
+
if (defaults.length !== value.length)
|
|
7151
|
+
return value;
|
|
7152
|
+
// Short-circuit when no element pair would be deep-merged. Returning a fresh
|
|
7153
|
+
// .map() result for primitive arrays (e.g. tags: ['a', 'b']) hands Signal
|
|
7154
|
+
// Forms a new reference on every call and rebuilds item FieldTrees needlessly.
|
|
7155
|
+
if (!value.some((item, i) => isPlainObject(defaults[i]) && isPlainObject(item)))
|
|
7156
|
+
return value;
|
|
7157
|
+
return value.map((item, i) => {
|
|
7158
|
+
const defaultItem = defaults[i];
|
|
7159
|
+
if (isPlainObject(defaultItem) && isPlainObject(item)) {
|
|
7160
|
+
return deepMergeDefaults(defaultItem, item);
|
|
7161
|
+
}
|
|
7162
|
+
return item;
|
|
7163
|
+
});
|
|
7164
|
+
}
|
|
7165
|
+
/**
|
|
7166
|
+
* @internal
|
|
7167
|
+
* Returns true for objects that should be deep-merged (plain objects produced
|
|
7168
|
+
* by literals or `Object.create(null)`); false for arrays, Dates, RegExps,
|
|
7169
|
+
* Maps, Sets, class instances, and primitives.
|
|
7170
|
+
*/
|
|
7171
|
+
function isPlainObject(value) {
|
|
7172
|
+
if (typeof value !== 'object' || value === null)
|
|
7173
|
+
return false;
|
|
7174
|
+
const proto = Object.getPrototypeOf(value);
|
|
7175
|
+
return proto === null || proto === Object.prototype;
|
|
7176
|
+
}
|
|
6902
7177
|
/**
|
|
6903
7178
|
* Options for memoize function
|
|
6904
7179
|
*/
|
|
@@ -7088,6 +7363,27 @@ function createFormLevelSchema(formLevelSchema) {
|
|
|
7088
7363
|
});
|
|
7089
7364
|
}
|
|
7090
7365
|
|
|
7366
|
+
/**
|
|
7367
|
+
* Injection token for global validation execution defaults.
|
|
7368
|
+
*
|
|
7369
|
+
* Controls whether validators run on hidden fields. By default, validation is
|
|
7370
|
+
* skipped when a field is hidden (`validateWhenHidden: false`).
|
|
7371
|
+
*
|
|
7372
|
+
* Configured via `withValidationExecutionDefaults()`. Per-form overrides via
|
|
7373
|
+
* `FormOptions.validateWhenHidden`. Per-field overrides via `FieldDef.validateWhenHidden`,
|
|
7374
|
+
* which inherits down the field tree.
|
|
7375
|
+
*
|
|
7376
|
+
* **Resolution order:** Field (with parent inheritance) > Form > Global.
|
|
7377
|
+
*
|
|
7378
|
+
* @internal
|
|
7379
|
+
*/
|
|
7380
|
+
const VALIDATION_EXECUTION_DEFAULTS = new InjectionToken('VALIDATION_EXECUTION_DEFAULTS', {
|
|
7381
|
+
providedIn: 'root',
|
|
7382
|
+
factory: () => ({
|
|
7383
|
+
validateWhenHidden: false,
|
|
7384
|
+
}),
|
|
7385
|
+
});
|
|
7386
|
+
|
|
7091
7387
|
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
7092
7388
|
/**
|
|
7093
7389
|
* Gets a FieldTree by key, supporting nested paths with dot notation.
|
|
@@ -7132,11 +7428,17 @@ function createSchemaFromFields(fields, registry, optionsOrValidators) {
|
|
|
7132
7428
|
// These will be available because createSchemaFromFields is called within runInInjectionContext
|
|
7133
7429
|
const functionRegistry = inject(FunctionRegistryService);
|
|
7134
7430
|
const logger = inject(DynamicFormLogger);
|
|
7431
|
+
const validationExecutionDefaults = inject(VALIDATION_EXECUTION_DEFAULTS);
|
|
7135
7432
|
// Normalize options - support both old array signature and new options object
|
|
7136
7433
|
const options = Array.isArray(optionsOrValidators)
|
|
7137
7434
|
? { crossFieldValidators: optionsOrValidators }
|
|
7138
7435
|
: (optionsOrValidators ?? {});
|
|
7139
7436
|
const { crossFieldValidators, formLevelSchema } = options;
|
|
7437
|
+
const rootContext = {
|
|
7438
|
+
validateWhenHidden: options.validateWhenHidden ?? validationExecutionDefaults.validateWhenHidden,
|
|
7439
|
+
ancestorAlwaysHidden: false,
|
|
7440
|
+
ancestorHiddenLogics: [],
|
|
7441
|
+
};
|
|
7140
7442
|
return schema((path) => {
|
|
7141
7443
|
for (const fieldDef of fields) {
|
|
7142
7444
|
const valueHandling = getFieldValueHandling(fieldDef.type, registry);
|
|
@@ -7146,12 +7448,17 @@ function createSchemaFromFields(fields, registry, optionsOrValidators) {
|
|
|
7146
7448
|
continue;
|
|
7147
7449
|
}
|
|
7148
7450
|
if (valueHandling === 'flatten' && hasChildFields(fieldDef)) {
|
|
7451
|
+
// A flatten container can still set its own cascading overrides that flow
|
|
7452
|
+
// down to its children — including its own hidden state which has no
|
|
7453
|
+
// schema path of its own.
|
|
7454
|
+
const flattenedOwn = resolveFieldOwnContext(fieldDef, rootContext);
|
|
7455
|
+
const flattenedContext = resolveDescendantContext(fieldDef, flattenedOwn);
|
|
7149
7456
|
for (const childField of normalizeFieldsArray(fieldDef.fields)) {
|
|
7150
7457
|
if (!childField.key)
|
|
7151
7458
|
continue;
|
|
7152
7459
|
const childPath = path[childField.key];
|
|
7153
7460
|
if (childPath) {
|
|
7154
|
-
mapFieldToForm(childField, childPath);
|
|
7461
|
+
mapFieldToForm(childField, childPath, flattenedContext);
|
|
7155
7462
|
}
|
|
7156
7463
|
}
|
|
7157
7464
|
continue;
|
|
@@ -7164,7 +7471,7 @@ function createSchemaFromFields(fields, registry, optionsOrValidators) {
|
|
|
7164
7471
|
// Use the new modular form mapping function
|
|
7165
7472
|
// This will progressively apply validators, logic, and schemas
|
|
7166
7473
|
// Cross-field logic is handled automatically via RootFormRegistryService
|
|
7167
|
-
mapFieldToForm(fieldDef, fieldPath);
|
|
7474
|
+
mapFieldToForm(fieldDef, fieldPath, rootContext);
|
|
7168
7475
|
}
|
|
7169
7476
|
// Apply cross-field validators using validateTree
|
|
7170
7477
|
if (crossFieldValidators && crossFieldValidators.length > 0) {
|
|
@@ -7772,49 +8079,54 @@ function flattenFields(fields, registry, options = {}) {
|
|
|
7772
8079
|
* These functions are used by both `FormStateManager` and `GroupFieldComponent`
|
|
7773
8080
|
* to compute flattened fields, field lookups, and default values from field definitions.
|
|
7774
8081
|
*
|
|
8082
|
+
* Caches are keyed on the input array's reference identity (via WeakMap) rather than
|
|
8083
|
+
* a derived content key. Different array instances — even ones with the same field
|
|
8084
|
+
* keys/types — must produce fresh results so per-field content changes (e.g. updated
|
|
8085
|
+
* select options) propagate through the form pipeline.
|
|
8086
|
+
*
|
|
7775
8087
|
* @returns Object containing memoized processing functions
|
|
7776
8088
|
*/
|
|
7777
8089
|
function createContainerFieldProcessors() {
|
|
7778
|
-
const
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
8090
|
+
const flattenCache = new WeakMap();
|
|
8091
|
+
const memoizedFlattenFields = (fields, registry) => {
|
|
8092
|
+
const cached = flattenCache.get(fields);
|
|
8093
|
+
if (cached !== undefined) {
|
|
8094
|
+
return cached;
|
|
7782
8095
|
}
|
|
7783
|
-
|
|
8096
|
+
const result = flattenFields([...fields], registry);
|
|
8097
|
+
flattenCache.set(fields, result);
|
|
8098
|
+
return result;
|
|
8099
|
+
};
|
|
8100
|
+
const flattenForRenderingCache = new WeakMap();
|
|
8101
|
+
const memoizedFlattenFieldsForRendering = (fields, registry) => {
|
|
8102
|
+
const cached = flattenForRenderingCache.get(fields);
|
|
8103
|
+
if (cached !== undefined) {
|
|
8104
|
+
return cached;
|
|
8105
|
+
}
|
|
8106
|
+
const result = flattenFields([...fields], registry, { preserveRows: true });
|
|
8107
|
+
flattenForRenderingCache.set(fields, result);
|
|
8108
|
+
return result;
|
|
8109
|
+
};
|
|
8110
|
+
const keyByCache = new WeakMap();
|
|
8111
|
+
const memoizedKeyBy = (fields) => {
|
|
8112
|
+
const cached = keyByCache.get(fields);
|
|
8113
|
+
if (cached !== undefined) {
|
|
8114
|
+
return cached;
|
|
8115
|
+
}
|
|
8116
|
+
const result = keyBy(fields, 'key');
|
|
8117
|
+
keyByCache.set(fields, result);
|
|
8118
|
+
return result;
|
|
8119
|
+
};
|
|
8120
|
+
const defaultValuesCache = new WeakMap();
|
|
8121
|
+
const memoizedDefaultValues = (fieldsById, registry) => {
|
|
8122
|
+
const cached = defaultValuesCache.get(fieldsById);
|
|
8123
|
+
if (cached !== undefined) {
|
|
8124
|
+
return cached;
|
|
8125
|
+
}
|
|
8126
|
+
const result = mapValues(fieldsById, (field) => getFieldDefaultValue(field, registry));
|
|
8127
|
+
defaultValuesCache.set(fieldsById, result);
|
|
8128
|
+
return result;
|
|
7784
8129
|
};
|
|
7785
|
-
const memoizedFlattenFields = memoize((fields, registry) => flattenFields([...fields], registry), {
|
|
7786
|
-
// registry.size is a valid cache key proxy because the field registry is populated
|
|
7787
|
-
// once at bootstrap and never mutated at runtime. If the registry were mutable,
|
|
7788
|
-
// we would need a content-based hash instead.
|
|
7789
|
-
resolver: createFieldsResolver(),
|
|
7790
|
-
maxSize: 10,
|
|
7791
|
-
});
|
|
7792
|
-
const memoizedFlattenFieldsForRendering = memoize((fields, registry) => flattenFields([...fields], registry, { preserveRows: true }), {
|
|
7793
|
-
// registry.size is a valid cache key proxy because the field registry is populated
|
|
7794
|
-
// once at bootstrap and never mutated at runtime. If the registry were mutable,
|
|
7795
|
-
// we would need a content-based hash instead.
|
|
7796
|
-
resolver: createFieldsResolver(true),
|
|
7797
|
-
maxSize: 10,
|
|
7798
|
-
});
|
|
7799
|
-
const memoizedKeyBy = memoize((fields) => keyBy(fields, 'key'), {
|
|
7800
|
-
resolver: (fields) => {
|
|
7801
|
-
let key = '';
|
|
7802
|
-
for (const f of fields) {
|
|
7803
|
-
key += f.key + '|';
|
|
7804
|
-
}
|
|
7805
|
-
return key;
|
|
7806
|
-
},
|
|
7807
|
-
maxSize: 10,
|
|
7808
|
-
});
|
|
7809
|
-
const memoizedDefaultValues = memoize((fieldsById, registry) => mapValues(fieldsById, (field) => getFieldDefaultValue(field, registry)), {
|
|
7810
|
-
// registry.size is a valid cache key proxy because the field registry is populated
|
|
7811
|
-
// once at bootstrap and never mutated at runtime.
|
|
7812
|
-
resolver: (fieldsById, registry) => {
|
|
7813
|
-
const keys = Object.keys(fieldsById).sort();
|
|
7814
|
-
return keys.join('|') + '|' + registry.size;
|
|
7815
|
-
},
|
|
7816
|
-
maxSize: 10,
|
|
7817
|
-
});
|
|
7818
8130
|
return { memoizedFlattenFields, memoizedFlattenFieldsForRendering, memoizedKeyBy, memoizedDefaultValues };
|
|
7819
8131
|
}
|
|
7820
8132
|
/**
|
|
@@ -8423,6 +8735,11 @@ function resolveFieldSync(fieldDef, context) {
|
|
|
8423
8735
|
* Reconciles previous and current resolved fields to preserve injector instances
|
|
8424
8736
|
* for fields that haven't changed type, preventing unnecessary component recreation.
|
|
8425
8737
|
*
|
|
8738
|
+
* When a field's key, component, and injector all match the previous render but the
|
|
8739
|
+
* underlying `fieldDef` has changed (e.g. updated options/props), the existing
|
|
8740
|
+
* component/injector are preserved while the new `inputs` and `renderReady` signals
|
|
8741
|
+
* are adopted so prop updates flow through to the rendered component.
|
|
8742
|
+
*
|
|
8426
8743
|
* @param prev - Previous resolved fields array
|
|
8427
8744
|
* @param curr - Current resolved fields array
|
|
8428
8745
|
* @returns Reconciled fields with preserved injectors where applicable
|
|
@@ -8432,8 +8749,20 @@ function reconcileFields(prev, curr) {
|
|
|
8432
8749
|
return curr.map((field) => {
|
|
8433
8750
|
const existing = prevMap.get(field.key);
|
|
8434
8751
|
if (existing && existing.component === field.component && existing.injector === field.injector) {
|
|
8435
|
-
|
|
8436
|
-
|
|
8752
|
+
if (existing.fieldDef === field.fieldDef) {
|
|
8753
|
+
// Truly unchanged - preserve object identity for signal stability
|
|
8754
|
+
return existing;
|
|
8755
|
+
}
|
|
8756
|
+
// Same component instance + injector, but the field definition changed.
|
|
8757
|
+
// Preserve component identity (so ngComponentOutlet keeps the same instance)
|
|
8758
|
+
// while swapping in the freshly-mapped inputs/renderReady signals so prop
|
|
8759
|
+
// updates (e.g. select options) reach the component.
|
|
8760
|
+
return {
|
|
8761
|
+
...existing,
|
|
8762
|
+
fieldDef: field.fieldDef,
|
|
8763
|
+
inputs: field.inputs,
|
|
8764
|
+
renderReady: field.renderReady,
|
|
8765
|
+
};
|
|
8437
8766
|
}
|
|
8438
8767
|
// New field, type changed, or context changed (new injector) - use new field
|
|
8439
8768
|
return field;
|
|
@@ -8916,18 +9245,22 @@ function resolveExclusionConfig(global, form, field) {
|
|
|
8916
9245
|
};
|
|
8917
9246
|
}
|
|
8918
9247
|
/**
|
|
8919
|
-
* Determines whether a field's value should be excluded based on its reactive state
|
|
9248
|
+
* Determines whether a field's value should be excluded based on its reactive state,
|
|
9249
|
+
* the field def's static state (for fields whose `state.hidden()` isn't wired — e.g. groups),
|
|
8920
9250
|
* and the resolved exclusion config.
|
|
8921
9251
|
*/
|
|
8922
|
-
function shouldExcludeField(fieldState, config) {
|
|
9252
|
+
function shouldExcludeField(field, fieldState, config) {
|
|
8923
9253
|
const state = fieldState();
|
|
8924
|
-
|
|
9254
|
+
const isHidden = state.hidden() || field.hidden === true;
|
|
9255
|
+
if (config.excludeValueIfHidden && isHidden) {
|
|
8925
9256
|
return true;
|
|
8926
9257
|
}
|
|
8927
|
-
|
|
9258
|
+
const isDisabled = state.disabled() || field.disabled === true;
|
|
9259
|
+
if (config.excludeValueIfDisabled && isDisabled) {
|
|
8928
9260
|
return true;
|
|
8929
9261
|
}
|
|
8930
|
-
|
|
9262
|
+
const isReadonly = state.readonly() || field.readonly === true;
|
|
9263
|
+
if (config.excludeValueIfReadonly && isReadonly) {
|
|
8931
9264
|
return true;
|
|
8932
9265
|
}
|
|
8933
9266
|
return false;
|
|
@@ -8959,14 +9292,6 @@ function filterFormValue(rawValue, schemaFields, formTree, registry, globalDefau
|
|
|
8959
9292
|
// If the key doesn't exist in the raw value, skip
|
|
8960
9293
|
if (!(key in rawValue))
|
|
8961
9294
|
continue;
|
|
8962
|
-
// Try to access the field state from the form tree
|
|
8963
|
-
const fieldState = formTree[key];
|
|
8964
|
-
// If no field state is available (e.g., componentless hidden fields),
|
|
8965
|
-
// include the value as-is since we can't determine reactive state
|
|
8966
|
-
if (!fieldState || typeof fieldState !== 'function') {
|
|
8967
|
-
result[key] = rawValue[key];
|
|
8968
|
-
continue;
|
|
8969
|
-
}
|
|
8970
9295
|
// Resolve per-field exclusion config
|
|
8971
9296
|
const fieldExclusion = {
|
|
8972
9297
|
excludeValueIfHidden: field.excludeValueIfHidden,
|
|
@@ -8974,8 +9299,19 @@ function filterFormValue(rawValue, schemaFields, formTree, registry, globalDefau
|
|
|
8974
9299
|
excludeValueIfReadonly: field.excludeValueIfReadonly,
|
|
8975
9300
|
};
|
|
8976
9301
|
const resolvedConfig = resolveExclusionConfig(globalDefaults, formOptions, fieldExclusion);
|
|
9302
|
+
// Componentless field (no FieldTree) — decide from the static field def alone, then include if not excluded.
|
|
9303
|
+
const fieldState = formTree[key];
|
|
9304
|
+
if (!fieldState || typeof fieldState !== 'function') {
|
|
9305
|
+
const staticallyExcluded = (resolvedConfig.excludeValueIfHidden && field.hidden === true) ||
|
|
9306
|
+
(resolvedConfig.excludeValueIfDisabled && field.disabled === true) ||
|
|
9307
|
+
(resolvedConfig.excludeValueIfReadonly && field.readonly === true);
|
|
9308
|
+
if (!staticallyExcluded) {
|
|
9309
|
+
result[key] = rawValue[key];
|
|
9310
|
+
}
|
|
9311
|
+
continue;
|
|
9312
|
+
}
|
|
8977
9313
|
// Check if this field should be excluded based on its state
|
|
8978
|
-
if (shouldExcludeField(fieldState, resolvedConfig)) {
|
|
9314
|
+
if (shouldExcludeField(field, fieldState, resolvedConfig)) {
|
|
8979
9315
|
continue;
|
|
8980
9316
|
}
|
|
8981
9317
|
// Handle group fields: recurse into nested structure
|
|
@@ -9470,6 +9806,77 @@ const FORM_STATE_DEPS = new InjectionToken('FORM_STATE_DEPS');
|
|
|
9470
9806
|
function asFieldTreeRecord(tree) {
|
|
9471
9807
|
return tree;
|
|
9472
9808
|
}
|
|
9809
|
+
/** All-false baseline for the outward two-way binding filter — see `boundFormValue`. */
|
|
9810
|
+
const BOUND_VALUE_EXCLUSION_BASELINE = {
|
|
9811
|
+
excludeValueIfHidden: false,
|
|
9812
|
+
excludeValueIfDisabled: false,
|
|
9813
|
+
excludeValueIfReadonly: false,
|
|
9814
|
+
};
|
|
9815
|
+
/** True when any of the three exclusion axes is asserted (i.e. the field could be filtered out of the bound model). */
|
|
9816
|
+
function isFieldExcluded(axes) {
|
|
9817
|
+
return !!axes && (axes.hidden || axes.disabled || axes.readonly);
|
|
9818
|
+
}
|
|
9819
|
+
/**
|
|
9820
|
+
* Recursively populates `out` with `dottedPath → exclusion axes` for every keyed field reachable
|
|
9821
|
+
* from `fields`, walking into groups so nested-leaf transitions can be detected. Arrays are
|
|
9822
|
+
* treated as leaves (whole-array filtering only — see value-filter v1 limitation).
|
|
9823
|
+
*/
|
|
9824
|
+
function collectFieldStateSnapshot(fields, treeRecord, pathParts, out) {
|
|
9825
|
+
for (const field of fields) {
|
|
9826
|
+
const key = field.key;
|
|
9827
|
+
if (!key)
|
|
9828
|
+
continue;
|
|
9829
|
+
const subtree = treeRecord[key];
|
|
9830
|
+
if (!subtree || typeof subtree !== 'function')
|
|
9831
|
+
continue;
|
|
9832
|
+
const state = subtree();
|
|
9833
|
+
const path = [...pathParts, key];
|
|
9834
|
+
out[path.join('.')] = { hidden: state.hidden(), disabled: state.disabled(), readonly: state.readonly() };
|
|
9835
|
+
if (isGroupField(field) && field.fields) {
|
|
9836
|
+
collectFieldStateSnapshot(field.fields, asFieldTreeRecord(subtree), path, out);
|
|
9837
|
+
}
|
|
9838
|
+
}
|
|
9839
|
+
}
|
|
9840
|
+
/**
|
|
9841
|
+
* Reads a value from a nested object by dotted-path segments. Returns undefined
|
|
9842
|
+
* if any intermediate segment is missing or non-object.
|
|
9843
|
+
*/
|
|
9844
|
+
function getValueAtPath(source, segments) {
|
|
9845
|
+
let current = source;
|
|
9846
|
+
for (const segment of segments) {
|
|
9847
|
+
if (current === null || typeof current !== 'object')
|
|
9848
|
+
return undefined;
|
|
9849
|
+
current = current[segment];
|
|
9850
|
+
}
|
|
9851
|
+
return current;
|
|
9852
|
+
}
|
|
9853
|
+
/**
|
|
9854
|
+
* Whether a value is worth capturing into the saved-hidden-value store. `NaN` (the number-input
|
|
9855
|
+
* default) is excluded — restoring it would re-leak the default into the bound model when the
|
|
9856
|
+
* field becomes visible again, defeating the bug fix.
|
|
9857
|
+
*/
|
|
9858
|
+
function isRestoreRelevant(value) {
|
|
9859
|
+
return !(typeof value === 'number' && Number.isNaN(value));
|
|
9860
|
+
}
|
|
9861
|
+
/**
|
|
9862
|
+
* Returns a new object with `value` set at the path described by `segments`.
|
|
9863
|
+
* Intermediate objects are cloned shallowly to preserve immutability of `target`.
|
|
9864
|
+
*/
|
|
9865
|
+
function setValueAtPath(target, segments, value) {
|
|
9866
|
+
if (segments.length === 0)
|
|
9867
|
+
return target;
|
|
9868
|
+
const [head, ...rest] = segments;
|
|
9869
|
+
const next = { ...target };
|
|
9870
|
+
if (rest.length === 0) {
|
|
9871
|
+
next[head] = value;
|
|
9872
|
+
}
|
|
9873
|
+
else {
|
|
9874
|
+
const child = target[head];
|
|
9875
|
+
const childObj = child !== null && typeof child === 'object' ? child : {};
|
|
9876
|
+
next[head] = setValueAtPath(childObj, rest, value);
|
|
9877
|
+
}
|
|
9878
|
+
return next;
|
|
9879
|
+
}
|
|
9473
9880
|
/**
|
|
9474
9881
|
* Central service that manages all form state and coordinates the form lifecycle.
|
|
9475
9882
|
* Single source of truth for lifecycle state, field resolution, form signals, and events.
|
|
@@ -9641,6 +10048,29 @@ class FormStateManager {
|
|
|
9641
10048
|
return undefined;
|
|
9642
10049
|
return new Set(schemaFields.map((f) => f.key).filter((key) => key !== undefined));
|
|
9643
10050
|
}, ...(ngDevMode ? [{ debugName: "validKeys" }] : /* istanbul ignore next */ []));
|
|
10051
|
+
/**
|
|
10052
|
+
* Per-field exclusion-axis state (hidden/disabled/readonly) keyed by dotted path. Drives the
|
|
10053
|
+
* save-on-exclude effect's transition detection. Walks recursively into groups so nested-leaf
|
|
10054
|
+
* transitions are captured. Arrays are treated as leaves (whole-array preservation only).
|
|
10055
|
+
*/
|
|
10056
|
+
fieldStateSnapshot = computed(() => {
|
|
10057
|
+
const setup = this.formSetup();
|
|
10058
|
+
if (!setup.schemaFields || setup.schemaFields.length === 0)
|
|
10059
|
+
return {};
|
|
10060
|
+
const snapshot = {};
|
|
10061
|
+
collectFieldStateSnapshot(setup.schemaFields, asFieldTreeRecord(this.form()), [], snapshot);
|
|
10062
|
+
return snapshot;
|
|
10063
|
+
}, ...(ngDevMode ? [{ debugName: "fieldStateSnapshot" }] : /* istanbul ignore next */ []));
|
|
10064
|
+
/**
|
|
10065
|
+
* Captured values for fields actively excluded from the bound model — either via
|
|
10066
|
+
* `excludeValueIfHidden`, `excludeValueIfDisabled`, or `excludeValueIfReadonly`. Restored when
|
|
10067
|
+
* the field becomes re-included (visible/enabled/editable). The save effect only writes here
|
|
10068
|
+
* when the field is being filtered out by `boundFormValue` — fields without explicit opt-in
|
|
10069
|
+
* never enter the store.
|
|
10070
|
+
*/
|
|
10071
|
+
excludedValueStore = signal({}, ...(ngDevMode ? [{ debugName: "excludedValueStore" }] : /* istanbul ignore next */ []));
|
|
10072
|
+
/** Tracks per-path exclusion state across save-effect runs to detect newly-excluded fields. */
|
|
10073
|
+
prevFieldStateSnapshot = signal({}, ...(ngDevMode ? [{ debugName: "prevFieldStateSnapshot" }] : /* istanbul ignore next */ []));
|
|
9644
10074
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
9645
10075
|
// Computed Signals - Entity & Form
|
|
9646
10076
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -9664,7 +10094,13 @@ class FormStateManager {
|
|
|
9664
10094
|
const inputValue = this.deps.value();
|
|
9665
10095
|
const defaults = this.defaultValues();
|
|
9666
10096
|
const keys = this.validKeys();
|
|
9667
|
-
const
|
|
10097
|
+
const saved = this.excludedValueStore();
|
|
10098
|
+
// Deep-merge so a partial nested object in `inputValue` (e.g. a group
|
|
10099
|
+
// value missing one of its declared sub-field keys) does not orphan
|
|
10100
|
+
// the absent sub-field in the Signal Forms validation graph.
|
|
10101
|
+
// Layer order: defaults < saved (excluded-field restorations) < inputValue.
|
|
10102
|
+
const withSaved = deepMergeDefaults(defaults, saved);
|
|
10103
|
+
const combined = deepMergeDefaults(withSaved, inputValue);
|
|
9668
10104
|
if (keys) {
|
|
9669
10105
|
const filtered = {};
|
|
9670
10106
|
for (const key of Object.keys(combined)) {
|
|
@@ -9694,6 +10130,7 @@ class FormStateManager {
|
|
|
9694
10130
|
return createSchemaFromFields(setup.schemaFields, setup.registry, {
|
|
9695
10131
|
crossFieldValidators: crossFieldCollection.validators,
|
|
9696
10132
|
formLevelSchema: config.schema,
|
|
10133
|
+
validateWhenHidden: this.effectiveFormOptions().validateWhenHidden,
|
|
9697
10134
|
});
|
|
9698
10135
|
}
|
|
9699
10136
|
if (config.schema) {
|
|
@@ -9773,6 +10210,35 @@ class FormStateManager {
|
|
|
9773
10210
|
const fieldTreeRecord = asFieldTreeRecord(formTree);
|
|
9774
10211
|
return filterFormValue(rawValue, setup.schemaFields, fieldTreeRecord, setup.registry, this.valueExclusionDefaults, formOptions);
|
|
9775
10212
|
}, ...(ngDevMode ? [{ debugName: "filteredFormValue" }] : /* istanbul ignore next */ []));
|
|
10213
|
+
/**
|
|
10214
|
+
* Form value used for the outward two-way binding sync.
|
|
10215
|
+
*
|
|
10216
|
+
* Only honors *explicit* form-level and field-level `excludeValueIf*` settings — global
|
|
10217
|
+
* defaults (`VALUE_EXCLUSION_DEFAULTS`) are intentionally ignored here. Rationale:
|
|
10218
|
+
* - Original V1 behavior: the bound model carries all values regardless of visibility.
|
|
10219
|
+
* - Bug fix: when a user explicitly sets `excludeValueIfHidden: true` on a field/form,
|
|
10220
|
+
* they expect that value to disappear from the bound model when hidden (e.g. NaN for
|
|
10221
|
+
* number inputs, see issue #394).
|
|
10222
|
+
* - Without explicit opt-in, the bound model keeps the V1 contract.
|
|
10223
|
+
* `filteredFormValue` (above) still applies the full Field > Form > Global hierarchy
|
|
10224
|
+
* for the submission output.
|
|
10225
|
+
*/
|
|
10226
|
+
boundFormValue = computed(() => {
|
|
10227
|
+
const rawValue = this.formValue();
|
|
10228
|
+
const setup = this.formSetup();
|
|
10229
|
+
const options = this.effectiveFormOptions();
|
|
10230
|
+
if (!setup.schemaFields || setup.schemaFields.length === 0) {
|
|
10231
|
+
return rawValue;
|
|
10232
|
+
}
|
|
10233
|
+
const formTree = this.form();
|
|
10234
|
+
const formOptions = {
|
|
10235
|
+
excludeValueIfHidden: options.excludeValueIfHidden,
|
|
10236
|
+
excludeValueIfDisabled: options.excludeValueIfDisabled,
|
|
10237
|
+
excludeValueIfReadonly: options.excludeValueIfReadonly,
|
|
10238
|
+
};
|
|
10239
|
+
const fieldTreeRecord = asFieldTreeRecord(formTree);
|
|
10240
|
+
return filterFormValue(rawValue, setup.schemaFields, fieldTreeRecord, setup.registry, BOUND_VALUE_EXCLUSION_BASELINE, formOptions);
|
|
10241
|
+
}, ...(ngDevMode ? [{ debugName: "boundFormValue" }] : /* istanbul ignore next */ []));
|
|
9776
10242
|
/** Whether the form is currently valid. */
|
|
9777
10243
|
valid = computed(() => this.formInstance().valid(), ...(ngDevMode ? [{ debugName: "valid" }] : /* istanbul ignore next */ []));
|
|
9778
10244
|
/** Whether the form is currently invalid. */
|
|
@@ -9797,7 +10263,12 @@ class FormStateManager {
|
|
|
9797
10263
|
/**
|
|
9798
10264
|
* Phase-aware field source that coordinates component lifecycle with form updates.
|
|
9799
10265
|
* Teardown/Applying → intersection of old/new fields; Restoring → all new fields.
|
|
9800
|
-
*
|
|
10266
|
+
*
|
|
10267
|
+
* Uses per-field reference equality to suppress spurious emissions when the same
|
|
10268
|
+
* field definitions are returned (e.g. memoized intersections during transitions)
|
|
10269
|
+
* while still triggering re-resolution when the underlying field definition
|
|
10270
|
+
* references change (e.g. a config swap that updates options/props for a
|
|
10271
|
+
* same-keyed field).
|
|
9801
10272
|
*/
|
|
9802
10273
|
fieldsSource = computed(() => {
|
|
9803
10274
|
const state = this.machine.state();
|
|
@@ -9818,7 +10289,7 @@ class FormStateManager {
|
|
|
9818
10289
|
return true;
|
|
9819
10290
|
if (a.length !== b.length)
|
|
9820
10291
|
return false;
|
|
9821
|
-
return a.every((field, i) => field
|
|
10292
|
+
return a.every((field, i) => field === b[i]);
|
|
9822
10293
|
} });
|
|
9823
10294
|
/**
|
|
9824
10295
|
* Injector for field components with FIELD_SIGNAL_CONTEXT.
|
|
@@ -9984,6 +10455,7 @@ class FormStateManager {
|
|
|
9984
10455
|
* Resets the form to default values.
|
|
9985
10456
|
*/
|
|
9986
10457
|
reset() {
|
|
10458
|
+
this.excludedValueStore.set({});
|
|
9987
10459
|
const defaults = this.defaultValues();
|
|
9988
10460
|
this.form()().value.set(defaults);
|
|
9989
10461
|
this.deps.value.set(defaults);
|
|
@@ -9992,6 +10464,7 @@ class FormStateManager {
|
|
|
9992
10464
|
* Clears the form to empty state.
|
|
9993
10465
|
*/
|
|
9994
10466
|
clear() {
|
|
10467
|
+
this.excludedValueStore.set({});
|
|
9995
10468
|
const emptyValue = {};
|
|
9996
10469
|
this.form()().value.set(emptyValue);
|
|
9997
10470
|
this.deps.value.set(emptyValue);
|
|
@@ -10016,12 +10489,49 @@ class FormStateManager {
|
|
|
10016
10489
|
this.machine.dispatch({ type: Action.ConfigChange, config });
|
|
10017
10490
|
}
|
|
10018
10491
|
});
|
|
10019
|
-
//
|
|
10020
|
-
//
|
|
10021
|
-
|
|
10492
|
+
// Save-on-exclude: capture entity values for fields that boundFormValue is actively
|
|
10493
|
+
// filtering out via any of the three axes (hidden / disabled / readonly), so they can be
|
|
10494
|
+
// restored when the field becomes re-included. The transition fires once per "no axis
|
|
10495
|
+
// asserted → at least one axis asserted" edge; subsequent axis changes while still excluded
|
|
10496
|
+
// are no-ops. Comparing entity vs bound at the path limits the store to fields that are
|
|
10497
|
+
// actually being filtered — fields without explicit opt-in never enter the store and never
|
|
10498
|
+
// participate in the entity merge. `NaN` values are skipped: they're the number-input default
|
|
10499
|
+
// and would re-leak via the merge once the field becomes re-included.
|
|
10500
|
+
explicitEffect([this.fieldStateSnapshot], ([snapshot]) => {
|
|
10501
|
+
const currentSaved = this.excludedValueStore();
|
|
10502
|
+
const currentEntity = this.entity();
|
|
10503
|
+
const boundValue = this.boundFormValue();
|
|
10504
|
+
const prev = this.prevFieldStateSnapshot();
|
|
10505
|
+
let newSaved = currentSaved;
|
|
10506
|
+
for (const [path, axes] of Object.entries(snapshot)) {
|
|
10507
|
+
if (isFieldExcluded(axes) && !isFieldExcluded(prev[path])) {
|
|
10508
|
+
const segments = path.split('.');
|
|
10509
|
+
const inEntity = getValueAtPath(currentEntity, segments);
|
|
10510
|
+
const inBound = getValueAtPath(boundValue, segments);
|
|
10511
|
+
if (inEntity !== undefined && inBound === undefined && isRestoreRelevant(inEntity)) {
|
|
10512
|
+
newSaved = setValueAtPath(newSaved, segments, inEntity);
|
|
10513
|
+
}
|
|
10514
|
+
}
|
|
10515
|
+
}
|
|
10516
|
+
this.prevFieldStateSnapshot.set({ ...snapshot });
|
|
10517
|
+
if (newSaved !== currentSaved && !isEqual(newSaved, currentSaved)) {
|
|
10518
|
+
this.excludedValueStore.set(newSaved);
|
|
10519
|
+
}
|
|
10520
|
+
});
|
|
10521
|
+
// Schema change resets the saved store + transition tracker so values from a stale schema
|
|
10522
|
+
// can't leak into the new one.
|
|
10523
|
+
explicitEffect([this.formSetup], () => {
|
|
10524
|
+
this.prevFieldStateSnapshot.set({});
|
|
10525
|
+
if (Object.keys(this.excludedValueStore()).length > 0) {
|
|
10526
|
+
this.excludedValueStore.set({});
|
|
10527
|
+
}
|
|
10528
|
+
});
|
|
10529
|
+
// Outward sync uses boundFormValue (honors only explicit excludeValueIf* opt-ins) so
|
|
10530
|
+
// global defaults that target submission output don't reshape the host's bound model.
|
|
10531
|
+
explicitEffect([this.boundFormValue], ([currentBound]) => {
|
|
10022
10532
|
const currentValue = this.deps.value();
|
|
10023
|
-
if (!isEqual(
|
|
10024
|
-
this.deps.value.set(
|
|
10533
|
+
if (!isEqual(currentBound, currentValue)) {
|
|
10534
|
+
this.deps.value.set(currentBound);
|
|
10025
10535
|
}
|
|
10026
10536
|
});
|
|
10027
10537
|
explicitEffect([this.formConfigValidation], ([validation]) => {
|
|
@@ -14758,31 +15268,31 @@ const BUILT_IN_FIELDS = [
|
|
|
14758
15268
|
// `row` is a virtual field type: it maps to ContainerFieldComponent, and
|
|
14759
15269
|
// the rowFieldMapper injects a `{ type: 'row' }` wrapper so the container
|
|
14760
15270
|
// renders the flex/grid layout via RowWrapperComponent.
|
|
14761
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-
|
|
15271
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs'),
|
|
14762
15272
|
mapper: rowFieldMapper,
|
|
14763
15273
|
valueHandling: 'flatten',
|
|
14764
15274
|
},
|
|
14765
15275
|
{
|
|
14766
15276
|
name: 'group',
|
|
14767
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-
|
|
15277
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-BhYFIDA1.mjs'),
|
|
14768
15278
|
mapper: groupFieldMapper,
|
|
14769
15279
|
valueHandling: 'include',
|
|
14770
15280
|
},
|
|
14771
15281
|
{
|
|
14772
15282
|
name: 'array',
|
|
14773
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-
|
|
15283
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-DbWes42V.mjs'),
|
|
14774
15284
|
mapper: arrayFieldMapper,
|
|
14775
15285
|
valueHandling: 'include',
|
|
14776
15286
|
},
|
|
14777
15287
|
{
|
|
14778
15288
|
name: 'page',
|
|
14779
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-
|
|
15289
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-B9v_1shD.mjs'),
|
|
14780
15290
|
mapper: pageFieldMapper,
|
|
14781
15291
|
valueHandling: 'flatten',
|
|
14782
15292
|
},
|
|
14783
15293
|
{
|
|
14784
15294
|
name: 'text',
|
|
14785
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-
|
|
15295
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-B4LnTnIA.mjs'),
|
|
14786
15296
|
mapper: textFieldMapper,
|
|
14787
15297
|
valueHandling: 'exclude',
|
|
14788
15298
|
...DISPLAY_FIELD_TYPES_BASE,
|
|
@@ -14794,7 +15304,7 @@ const BUILT_IN_FIELDS = [
|
|
|
14794
15304
|
},
|
|
14795
15305
|
{
|
|
14796
15306
|
name: 'container',
|
|
14797
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-
|
|
15307
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-CMhqFGJZ.mjs'),
|
|
14798
15308
|
mapper: containerFieldMapper,
|
|
14799
15309
|
valueHandling: 'flatten',
|
|
14800
15310
|
},
|
|
@@ -14802,7 +15312,7 @@ const BUILT_IN_FIELDS = [
|
|
|
14802
15312
|
const BUILT_IN_WRAPPERS = [
|
|
14803
15313
|
{
|
|
14804
15314
|
wrapperName: 'css',
|
|
14805
|
-
loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-
|
|
15315
|
+
loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-htvP_o95.mjs'),
|
|
14806
15316
|
},
|
|
14807
15317
|
{
|
|
14808
15318
|
wrapperName: 'row',
|
|
@@ -15112,6 +15622,48 @@ function withValueExclusionDefaults(config) {
|
|
|
15112
15622
|
return createFeature('value-exclusion', [{ provide: VALUE_EXCLUSION_DEFAULTS, useValue: resolved }]);
|
|
15113
15623
|
}
|
|
15114
15624
|
|
|
15625
|
+
/**
|
|
15626
|
+
* Configures global validation execution defaults — when validators should run
|
|
15627
|
+
* relative to a field's reactive state.
|
|
15628
|
+
*
|
|
15629
|
+
* By default, validators are **skipped** for hidden fields (`validateWhenHidden: false`),
|
|
15630
|
+
* matching what most users expect: a hidden field is inert. Use this feature to
|
|
15631
|
+
* restore the legacy behavior where hidden fields still validate.
|
|
15632
|
+
*
|
|
15633
|
+
* @example Default behavior — no feature needed
|
|
15634
|
+
* ```typescript
|
|
15635
|
+
* provideDynamicForm(...withMaterialFields())
|
|
15636
|
+
* // hidden fields skip validation
|
|
15637
|
+
* ```
|
|
15638
|
+
*
|
|
15639
|
+
* @example Restore legacy validation-on-hidden behavior
|
|
15640
|
+
* ```typescript
|
|
15641
|
+
* provideDynamicForm(
|
|
15642
|
+
* ...withMaterialFields(),
|
|
15643
|
+
* withValidationExecutionDefaults({ validateWhenHidden: true })
|
|
15644
|
+
* )
|
|
15645
|
+
* ```
|
|
15646
|
+
*
|
|
15647
|
+
* @remarks
|
|
15648
|
+
* **Precedence rules:**
|
|
15649
|
+
* 1. Per-field `FieldDef.validateWhenHidden` — wins for that field; inherits down to descendants
|
|
15650
|
+
* that don't override.
|
|
15651
|
+
* 2. Per-form `FormOptions.validateWhenHidden` — root inherited value for the form.
|
|
15652
|
+
* 3. Global `withValidationExecutionDefaults()` — fallback when no form/field setting.
|
|
15653
|
+
* 4. No feature configured — uses token default (`validateWhenHidden: false`).
|
|
15654
|
+
*
|
|
15655
|
+
* @param config - Partial override. Unspecified properties keep their defaults.
|
|
15656
|
+
* @returns A `DynamicFormFeature` that configures validation execution defaults.
|
|
15657
|
+
*
|
|
15658
|
+
* @public
|
|
15659
|
+
*/
|
|
15660
|
+
function withValidationExecutionDefaults(config) {
|
|
15661
|
+
const resolved = {
|
|
15662
|
+
validateWhenHidden: config?.validateWhenHidden ?? false,
|
|
15663
|
+
};
|
|
15664
|
+
return createFeature('validation-execution', [{ provide: VALIDATION_EXECUTION_DEFAULTS, useValue: resolved }]);
|
|
15665
|
+
}
|
|
15666
|
+
|
|
15115
15667
|
/**
|
|
15116
15668
|
* Zero-cost type carrier used in wrapper registrations.
|
|
15117
15669
|
*
|
|
@@ -15834,5 +16386,5 @@ function extractErrorParams(error) {
|
|
|
15834
16386
|
* Generated bundle index. Do not edit.
|
|
15835
16387
|
*/
|
|
15836
16388
|
|
|
15837
|
-
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,
|
|
15838
|
-
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-
|
|
16389
|
+
export { SubmitEvent as $, ARRAY_CONTEXT as A, BUILT_IN_FIELDS as B, CONTAINER_FIELD_PROCESSORS as C, DynamicFormLogger as D, EventBus as E, FIELD_SIGNAL_CONTEXT as F, DEFAULT_VALIDATION_MESSAGES as G, DynamicForm as H, DynamicFormError as I, EventDispatcher as J, FIELD_REGISTRY as K, FORM_OPTIONS as L, FormClearEvent as M, FormResetEvent as N, INITIALIZATION_TIMEOUT_MS as O, InsertArrayItemEvent as P, MoveArrayItemEvent as Q, RootFormRegistryService as R, NextPageEvent as S, NoopLogger as T, PageChangeEvent as U, PopArrayItemEvent as V, WRAPPER_AUTO_ASSOCIATIONS as W, PrependArrayItemEvent as X, PreviousPageEvent as Y, RemoveAtIndexEvent as Z, ShiftArrayItemEvent as _, DEFAULT_WRAPPERS as a, WRAPPER_REGISTRY as a0, applyMetaToElement as a1, applyValidator as a2, applyValidators as a3, arrayEvent as a4, arrayFieldMapper as a5, baseFieldMapper as a6, buildBaseInputs as a7, containerFieldMapper as a8, createField as a9, resolveNextButtonDisabled as aA, resolveNonFieldDisabled as aB, resolveNonFieldHidden as aC, resolveSubmitButtonDisabled as aD, resolveTokens as aE, rowFieldMapper as aF, textFieldMapper as aG, toReadonlyFieldTree as aH, withEventFormValue as aI, withLoggerConfig as aJ, withPreviousValue as aK, withValidationExecutionDefaults as aL, withValueExclusionDefaults as aM, wrapperProps as aN, createWrappers as aa, evaluateNonFieldDisabled as ab, evaluateNonFieldHidden as ac, field as ad, formConfig as ae, getArrayLength as af, groupFieldMapper as ag, hasFormValue as ah, interpolateParams as ai, isArrayField as aj, isCheckedField as ak, isContainerField as al, isContainerTypedField as am, isDisplayOnlyField as an, isFormStateCondition as ao, isLeafField as ap, isPageField as aq, isRowField as ar, isSimplifiedArrayField as as, isValueBearingField as at, isValueField as au, isWrapperTypeDefinition as av, isWrappersBundle as aw, omit as ax, pageFieldMapper as ay, provideDynamicForm as az, isSameWrapperChain as b, createFieldResolutionPipe as c, derivedFromDeferred as d, createWrapperChainController as e, DfFieldOutlet as f, isEqual as g, createSchemaFromFields as h, injectFieldRegistry as i, createRenderReadySignal as j, ARRAY_TEMPLATE_REGISTRY as k, ARRAY_ITEM_ID_GENERATOR as l, mapFieldToInputs as m, getNormalizedArrayMetadata as n, getFieldValueHandling as o, getFieldDefaultValue as p, isGroupField as q, resolveWrappers as r, createArrayItemIdGenerator as s, ComponentInitializedEvent as t, DynamicTextPipe as u, validatePageNesting as v, dynamicTextToObservable as w, AppendArrayItemEvent as x, ConsoleLogger as y, DEFAULT_PROPS as z };
|
|
16390
|
+
//# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-DWPPLOrV.mjs.map
|