@ng-forge/dynamic-forms 1.3.0-next.1 → 1.3.0-next.3
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-active-page-initialized.event-CiKN-mcc.mjs +12 -0
- package/fesm2022/ng-forge-dynamic-forms-active-page-initialized.event-CiKN-mcc.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-Dv9mPnJ3.mjs +770 -0
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-Dv9mPnJ3.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-array-item-registry.service-D-AxVLdk.mjs +83 -0
- package/fesm2022/ng-forge-dynamic-forms-array-item-registry.service-D-AxVLdk.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-EuYCbquo.mjs +1887 -0
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-EuYCbquo.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-web-mcp-2wQK_wPp.mjs +954 -0
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-web-mcp-2wQK_wPp.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-container-field-processors-C_JRvaVt.mjs +117 -0
- package/fesm2022/ng-forge-dynamic-forms-container-field-processors-C_JRvaVt.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-Bg7Win9n.mjs +228 -0
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-Bg7Win9n.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-R2yc-H0T.mjs +102 -0
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-R2yc-H0T.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-debounce-CG3uP5B-.mjs +4 -0
- package/fesm2022/{ng-forge-dynamic-forms-debounce-C9F4v1Ke.mjs.map → ng-forge-dynamic-forms-debounce-CG3uP5B-.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-default-value-D5M4MNE-.mjs +77 -0
- package/fesm2022/ng-forge-dynamic-forms-default-value-D5M4MNE-.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-dev-mode-4rbYBBo_.mjs +4 -0
- package/fesm2022/ng-forge-dynamic-forms-dev-mode-4rbYBBo_.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-df-field-outlet.directive-CQsDMV-s.mjs +644 -0
- package/fesm2022/ng-forge-dynamic-forms-df-field-outlet.directive-CQsDMV-s.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-field-errors-wrapper.component-DKgkpIpG.mjs +56 -0
- package/fesm2022/ng-forge-dynamic-forms-field-errors-wrapper.component-DKgkpIpG.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-form-schema.service-Dd-gHJ_7.mjs +38 -0
- package/fesm2022/{ng-forge-dynamic-forms-form-schema.service-Dx5Dlb15.mjs.map → ng-forge-dynamic-forms-form-schema.service-Dd-gHJ_7.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-form-state-manager-DcOXRFRL.mjs +1522 -0
- package/fesm2022/ng-forge-dynamic-forms-form-state-manager-DcOXRFRL.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-BjZi0bhq.mjs +292 -0
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-BjZi0bhq.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +1318 -843
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs +3326 -3641
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-BRjQmWC9.mjs +295 -0
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-BRjQmWC9.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-resolve-field-ByGa5cpo.mjs +329 -0
- package/fesm2022/ng-forge-dynamic-forms-resolve-field-ByGa5cpo.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CuFycQb-.mjs +81 -0
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CuFycQb-.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-schema-builder-C62afItJ.mjs +400 -0
- package/fesm2022/ng-forge-dynamic-forms-schema-builder-C62afItJ.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-schema-registry.service-axBV9FBH.mjs +45 -0
- package/fesm2022/ng-forge-dynamic-forms-schema-registry.service-axBV9FBH.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-schema.mjs +8 -14
- package/fesm2022/ng-forge-dynamic-forms-schema.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-testing.mjs +68 -73
- package/fesm2022/ng-forge-dynamic-forms-testing.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-text-field.component--MAh_h4Q.mjs +257 -0
- package/fesm2022/ng-forge-dynamic-forms-text-field.component--MAh_h4Q.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-validation-execution.token-BFyySKtp.mjs +8 -0
- package/fesm2022/ng-forge-dynamic-forms-validation-execution.token-BFyySKtp.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-with-logger-config-BAATqYY0.mjs +94 -0
- package/fesm2022/ng-forge-dynamic-forms-with-logger-config-BAATqYY0.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-wrapper-chain-BnntfNU1.mjs +115 -0
- package/fesm2022/ng-forge-dynamic-forms-wrapper-chain-BnntfNU1.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms.mjs +2136 -3
- package/fesm2022/ng-forge-dynamic-forms.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ng-forge-dynamic-forms-integration.d.ts +543 -415
- package/types/ng-forge-dynamic-forms-internal.d.ts +2319 -2436
- package/types/ng-forge-dynamic-forms-schema.d.ts +13 -16
- package/types/ng-forge-dynamic-forms-testing.d.ts +34 -37
- package/types/ng-forge-dynamic-forms.d.ts +735 -722
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-CSCKq_1K.mjs +0 -750
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-CSCKq_1K.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-CTOTwzzw.mjs +0 -2394
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-CTOTwzzw.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-web-mcp-BtjqeJOj.mjs +0 -1054
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-web-mcp-BtjqeJOj.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-Dc3ROhHL.mjs +0 -86
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-Dc3ROhHL.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-BzgX8lAD.mjs +0 -33
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-BzgX8lAD.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-debounce-C9F4v1Ke.mjs +0 -17
- package/fesm2022/ng-forge-dynamic-forms-field-errors-wrapper.component-CIKbWqYA.mjs +0 -25
- package/fesm2022/ng-forge-dynamic-forms-field-errors-wrapper.component-CIKbWqYA.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-form-schema.service-Dx5Dlb15.mjs +0 -24
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-DAC3P9DC.mjs +0 -177
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-DAC3P9DC.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CXQe_5td.mjs +0 -5133
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CXQe_5td.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-DPKJcwUO.mjs +0 -176
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-DPKJcwUO.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-DMEeKZny.mjs +0 -19
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-DMEeKZny.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-schema-builder-BvDTzfdM.mjs +0 -570
- package/fesm2022/ng-forge-dynamic-forms-schema-builder-BvDTzfdM.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-text-field.component-BB-jutBq.mjs +0 -126
- package/fesm2022/ng-forge-dynamic-forms-text-field.component-BB-jutBq.mjs.map +0 -1
|
@@ -0,0 +1,1522 @@
|
|
|
1
|
+
import { r as setNormalizedArrayMetadata } from "./ng-forge-dynamic-forms-default-value-D5M4MNE-.mjs";
|
|
2
|
+
import { t as ArrayItemRegistryService } from "./ng-forge-dynamic-forms-array-item-registry.service-D-AxVLdk.mjs";
|
|
3
|
+
import { t as SchemaRegistryService } from "./ng-forge-dynamic-forms-schema-registry.service-axBV9FBH.mjs";
|
|
4
|
+
import { t as CONTAINER_FIELD_PROCESSORS } from "./ng-forge-dynamic-forms-container-field-processors-C_JRvaVt.mjs";
|
|
5
|
+
import { a as resolveField, i as reconcileFields, o as resolveFieldSync, u as injectFieldRegistry } from "./ng-forge-dynamic-forms-resolve-field-ByGa5cpo.mjs";
|
|
6
|
+
import { t as DEV_MODE } from "./ng-forge-dynamic-forms-dev-mode-4rbYBBo_.mjs";
|
|
7
|
+
import * as i0 from "@angular/core";
|
|
8
|
+
import { DestroyRef, Injectable, InjectionToken, Injector, afterNextRender, computed, inject, injectAsync, linkedSignal, resource, runInInjectionContext, signal, untracked } from "@angular/core";
|
|
9
|
+
import { explicitEffect } from "ngxtension/explicit-effect";
|
|
10
|
+
import { EMPTY, Observable, Subject, catchError, concatMap, filter, forkJoin, map, of, pipe, scan, switchMap, take } from "rxjs";
|
|
11
|
+
import { ADDON_TYPE_REGISTRY, DynamicFormError, DynamicFormLogger, EventBus, FIELD_SIGNAL_CONTEXT, FunctionRegistryService, assertNever, deepMergeDefaults, derivedFromDeferred, detectFormMode, getFieldValueHandling, hasChildFields, isArrayField, isContainerField, isEqual, isGroupField, isPageField, isSimplifiedArrayField, normalizeFieldsArray, validatePageNesting } from "@ng-forge/dynamic-forms/internal";
|
|
12
|
+
import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop";
|
|
13
|
+
import { form } from "@angular/forms/signals";
|
|
14
|
+
var FormSubmitEvent = class {
|
|
15
|
+
reply;
|
|
16
|
+
type = "submit";
|
|
17
|
+
constructor(reply) {
|
|
18
|
+
this.reply = reply;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function normalizeSimplifiedArrays(fields) {
|
|
22
|
+
return fields.flatMap((field) => {
|
|
23
|
+
if (isContainerField(field) && !isArrayField(field) && hasChildFields(field)) return [{
|
|
24
|
+
...field,
|
|
25
|
+
fields: normalizeSimplifiedArrays(field.fields)
|
|
26
|
+
}];
|
|
27
|
+
if (isArrayField(field) && !isSimplifiedArrayField(field)) {
|
|
28
|
+
const normalizedItems = field.fields.map((item) => Array.isArray(item) ? normalizeSimplifiedArrays(item) : item);
|
|
29
|
+
return [{
|
|
30
|
+
...field,
|
|
31
|
+
fields: normalizedItems
|
|
32
|
+
}];
|
|
33
|
+
}
|
|
34
|
+
if (isSimplifiedArrayField(field)) {
|
|
35
|
+
const { arrayField, addButton } = expandSimplifiedArray(field);
|
|
36
|
+
return addButton ? [arrayField, addButton] : [arrayField];
|
|
37
|
+
}
|
|
38
|
+
return [field];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const INVALID_ARRAY_CHILD_TYPES = /* @__PURE__ */ new Set(["page", "array"]);
|
|
42
|
+
function validateSimplifiedTemplate(field) {
|
|
43
|
+
const { template, key } = field;
|
|
44
|
+
if (Array.isArray(template)) {
|
|
45
|
+
const templateFields = template;
|
|
46
|
+
for (const tmpl of templateFields) if (INVALID_ARRAY_CHILD_TYPES.has(tmpl.type)) throw new DynamicFormError(`Simplified array "${key}" template contains a '${tmpl.type}' field (key: '${tmpl.key}'). Only leaf fields, rows, and groups are allowed as array children.`);
|
|
47
|
+
} else {
|
|
48
|
+
const singleTemplate = template;
|
|
49
|
+
if (INVALID_ARRAY_CHILD_TYPES.has(singleTemplate.type)) throw new DynamicFormError(`Simplified array "${key}" template has type '${singleTemplate.type}'. Only leaf fields, rows, and groups are allowed as array children.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function expandSimplifiedArray(field) {
|
|
53
|
+
const { template, value = [], addButton, removeButton, key, logic, minLength, maxLength, wrappers, skipAutoWrappers, skipDefaultWrappers, validators, validationMessages, required, validateWhenHidden } = field;
|
|
54
|
+
const isObjectTemplate = Array.isArray(template);
|
|
55
|
+
const values = value;
|
|
56
|
+
validateSimplifiedTemplate(field);
|
|
57
|
+
const items = values.map((v) => {
|
|
58
|
+
if (isObjectTemplate) return buildObjectItem(template, v, removeButton);
|
|
59
|
+
return buildPrimitiveItem(template, v);
|
|
60
|
+
});
|
|
61
|
+
const addTemplate = isObjectTemplate ? buildObjectItemTemplate(template, removeButton) : template;
|
|
62
|
+
const hasAutoRemove = !isObjectTemplate && removeButton !== false;
|
|
63
|
+
const arrayFieldObj = {
|
|
64
|
+
key,
|
|
65
|
+
type: "array",
|
|
66
|
+
fields: items
|
|
67
|
+
};
|
|
68
|
+
if (logic) arrayFieldObj["logic"] = logic;
|
|
69
|
+
if (minLength !== void 0) arrayFieldObj["minLength"] = minLength;
|
|
70
|
+
if (maxLength !== void 0) arrayFieldObj["maxLength"] = maxLength;
|
|
71
|
+
if (wrappers !== void 0) arrayFieldObj["wrappers"] = wrappers;
|
|
72
|
+
if (skipAutoWrappers !== void 0) arrayFieldObj["skipAutoWrappers"] = skipAutoWrappers;
|
|
73
|
+
if (skipDefaultWrappers !== void 0) arrayFieldObj["skipDefaultWrappers"] = skipDefaultWrappers;
|
|
74
|
+
if (validators !== void 0) arrayFieldObj["validators"] = validators;
|
|
75
|
+
if (validationMessages !== void 0) arrayFieldObj["validationMessages"] = validationMessages;
|
|
76
|
+
if (required !== void 0) arrayFieldObj["required"] = required;
|
|
77
|
+
if (validateWhenHidden !== void 0) arrayFieldObj["validateWhenHidden"] = validateWhenHidden;
|
|
78
|
+
const arrayField = arrayFieldObj;
|
|
79
|
+
const primitiveFieldKey = !isObjectTemplate ? template.key : void 0;
|
|
80
|
+
const metadataTemplate = isObjectTemplate ? buildObjectItemTemplate(template, removeButton) : template;
|
|
81
|
+
setNormalizedArrayMetadata(arrayFieldObj, {
|
|
82
|
+
...hasAutoRemove && { autoRemoveButton: buildRemoveButton(removeButton) },
|
|
83
|
+
...primitiveFieldKey && { primitiveFieldKey },
|
|
84
|
+
template: metadataTemplate
|
|
85
|
+
});
|
|
86
|
+
let addButtonField;
|
|
87
|
+
if (addButton !== false) {
|
|
88
|
+
const buttonConfig = typeof addButton === "object" ? addButton : {};
|
|
89
|
+
addButtonField = {
|
|
90
|
+
key: `${key}__add`,
|
|
91
|
+
type: "add-array-item",
|
|
92
|
+
label: buttonConfig.label ?? "Add",
|
|
93
|
+
arrayKey: key,
|
|
94
|
+
template: addTemplate,
|
|
95
|
+
...buttonConfig.props && { props: buttonConfig.props },
|
|
96
|
+
...logic && { logic }
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
arrayField,
|
|
101
|
+
addButton: addButtonField
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function buildPrimitiveItem(template, value) {
|
|
105
|
+
return {
|
|
106
|
+
...template,
|
|
107
|
+
value
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function buildObjectItem(template, valueObj, removeButton) {
|
|
111
|
+
const fieldsWithValues = template.map((templateField) => ({
|
|
112
|
+
...templateField,
|
|
113
|
+
...Object.hasOwn(valueObj, templateField.key) && { value: valueObj[templateField.key] }
|
|
114
|
+
}));
|
|
115
|
+
if (removeButton === false) return fieldsWithValues;
|
|
116
|
+
return [...fieldsWithValues, buildRemoveButton(removeButton)];
|
|
117
|
+
}
|
|
118
|
+
function buildObjectItemTemplate(template, removeButton) {
|
|
119
|
+
if (removeButton === false) return template;
|
|
120
|
+
return [...template, buildRemoveButton(removeButton)];
|
|
121
|
+
}
|
|
122
|
+
function buildRemoveButton(config) {
|
|
123
|
+
const buttonConfig = typeof config === "object" ? config : {};
|
|
124
|
+
return {
|
|
125
|
+
key: "__remove",
|
|
126
|
+
type: "remove-array-item",
|
|
127
|
+
label: buttonConfig.label ?? "Remove",
|
|
128
|
+
...buttonConfig.props && { props: buttonConfig.props }
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
var FormModeValidator = class {
|
|
132
|
+
static validateFormConfiguration(fields) {
|
|
133
|
+
const modeDetection = detectFormMode(fields);
|
|
134
|
+
const additionalErrors = [];
|
|
135
|
+
if (modeDetection.mode === "paged" && modeDetection.isValid) for (let i = 0; i < fields.length; i++) {
|
|
136
|
+
const field = fields[i];
|
|
137
|
+
if (isPageField(field)) {
|
|
138
|
+
if (!validatePageNesting(field)) additionalErrors.push(`Page field at index ${i} (key: "${field.key || "unknown"}") contains nested page fields, which is not allowed.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const allErrors = [...modeDetection.errors, ...additionalErrors];
|
|
142
|
+
const isFullyValid = modeDetection.isValid && additionalErrors.length === 0;
|
|
143
|
+
return {
|
|
144
|
+
mode: modeDetection.mode,
|
|
145
|
+
isValid: isFullyValid,
|
|
146
|
+
errors: allErrors,
|
|
147
|
+
warnings: this.generateWarnings(fields, modeDetection)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
static validateFormConfigurationOrThrow(fields) {
|
|
151
|
+
const result = this.validateFormConfiguration(fields);
|
|
152
|
+
if (!result.isValid) {
|
|
153
|
+
const errorMessage = [`Invalid form configuration (${result.mode} mode):`, ...result.errors.map((error) => ` - ${error}`)].join("\n");
|
|
154
|
+
throw new DynamicFormError(errorMessage);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
static generateWarnings(fields, modeDetection) {
|
|
158
|
+
const warnings = [];
|
|
159
|
+
if (modeDetection.mode === "paged" && fields.length === 1) warnings.push("Single page form detected. Consider using non-paged mode for better performance if page navigation is not needed.");
|
|
160
|
+
if (modeDetection.mode === "paged") for (let i = 0; i < fields.length; i++) {
|
|
161
|
+
const field = fields[i];
|
|
162
|
+
if (isPageField(field)) {
|
|
163
|
+
if (!field.fields || field.fields.length === 0) warnings.push(`Page field at index ${i} (key: "${field.key || "unknown"}") contains no fields and will render as empty.`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return warnings;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
function collectFieldData(fields, data, registry, pathPrefix, collectKeys = true) {
|
|
170
|
+
for (const field of fields) {
|
|
171
|
+
const valueHandling = field.type ? getFieldValueHandling(field.type, registry) : "include";
|
|
172
|
+
const participatesInValue = valueHandling !== "exclude" && valueHandling !== "flatten";
|
|
173
|
+
if (field.key && field.key.includes(".") && participatesInValue) data.dottedKeys.push(field.key);
|
|
174
|
+
if (collectKeys && field.key && participatesInValue) {
|
|
175
|
+
const scopedKey = pathPrefix ? `${pathPrefix}.${field.key}` : field.key;
|
|
176
|
+
data.keys.push(scopedKey);
|
|
177
|
+
data.domIds.push(scopedKey.replace(/\./g, "_"));
|
|
178
|
+
}
|
|
179
|
+
if (field.type) data.types.add(field.type);
|
|
180
|
+
const fieldWithValidation = field;
|
|
181
|
+
if ("pattern" in field && typeof fieldWithValidation.pattern === "string") validateRegexPattern(fieldWithValidation.pattern, field.key || "<unknown>", data.regexErrors);
|
|
182
|
+
if ("validators" in field && fieldWithValidation.validators) {
|
|
183
|
+
for (const validator of fieldWithValidation.validators) if (validator.type === "pattern" && "value" in validator && typeof validator.value === "string") validateRegexPattern(validator.value, field.key || "<unknown>", data.regexErrors);
|
|
184
|
+
}
|
|
185
|
+
if (hasChildFields(field)) {
|
|
186
|
+
const childCollectKeys = collectKeys && field.type !== "array";
|
|
187
|
+
const childPrefix = isGroupField(field) && field.key ? pathPrefix ? `${pathPrefix}.${field.key}` : field.key : pathPrefix;
|
|
188
|
+
const children = normalizeFieldsArray(field.fields);
|
|
189
|
+
for (const child of children) if (Array.isArray(child)) collectFieldData(child, data, registry, childPrefix, childCollectKeys);
|
|
190
|
+
else collectFieldData([child], data, registry, childPrefix, childCollectKeys);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function validateRegexPattern(pattern, fieldKey, errors) {
|
|
195
|
+
try {
|
|
196
|
+
new RegExp(pattern);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
errors.push(DEV_MODE ? `Invalid regex pattern in validator for field '${fieldKey}': '${pattern}' — ${e instanceof Error ? e.message : String(e)}` : `Invalid regex pattern: '${pattern}'`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function validateNoDuplicateKeys(allKeys) {
|
|
202
|
+
const seen = /* @__PURE__ */ new Set();
|
|
203
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
204
|
+
for (const key of allKeys) {
|
|
205
|
+
if (seen.has(key)) duplicates.add(key);
|
|
206
|
+
seen.add(key);
|
|
207
|
+
}
|
|
208
|
+
if (duplicates.size > 0) {
|
|
209
|
+
const duplicateList = Array.from(duplicates).map((k) => `'${k}'`).join(", ");
|
|
210
|
+
throw new DynamicFormError(DEV_MODE ? `Duplicate field keys detected: ${duplicateList}. Field keys must be unique within their group scope (the same key may appear inside different groups).` : `Duplicate field keys detected: ${duplicateList}.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function validateNoDomIdCollisions(domIds) {
|
|
214
|
+
const seen = /* @__PURE__ */ new Set();
|
|
215
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
216
|
+
for (const id of domIds) {
|
|
217
|
+
if (seen.has(id)) duplicates.add(id);
|
|
218
|
+
seen.add(id);
|
|
219
|
+
}
|
|
220
|
+
if (duplicates.size > 0) {
|
|
221
|
+
const duplicateList = Array.from(duplicates).map((k) => `'${k}'`).join(", ");
|
|
222
|
+
throw new DynamicFormError(DEV_MODE ? `DOM id collision detected for: ${duplicateList}. Group-nested keys are joined with '_' to form DOM ids (e.g. group 'foo' + leaf 'bar' renders as id='foo_bar'). A separate top-level key like 'foo_bar' or a different group/leaf pair that underscores to the same string will produce duplicate ids. Rename one of the colliding keys.` : `DOM id collision detected for: ${duplicateList}.`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function validateFieldTypesRegistered(allTypes, registry, logger) {
|
|
226
|
+
if (registry.size === 0) return;
|
|
227
|
+
const unregistered = [];
|
|
228
|
+
for (const type of allTypes) if (!registry.has(type)) unregistered.push(type);
|
|
229
|
+
if (unregistered.length > 0) {
|
|
230
|
+
const typeList = unregistered.map((t) => `'${t}'`).join(", ");
|
|
231
|
+
logger.warn(`Unknown field type(s): ${typeList}. Register them via provideDynamicForm(...withXxxFields()) or a custom registry entry. These fields will be skipped during rendering.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function validateFormConfig(fields, registry, logger) {
|
|
235
|
+
const data = {
|
|
236
|
+
keys: [],
|
|
237
|
+
domIds: [],
|
|
238
|
+
types: /* @__PURE__ */ new Set(),
|
|
239
|
+
regexErrors: [],
|
|
240
|
+
dottedKeys: []
|
|
241
|
+
};
|
|
242
|
+
collectFieldData(fields, data, registry, "");
|
|
243
|
+
if (data.dottedKeys.length > 0) {
|
|
244
|
+
const list = Array.from(new Set(data.dottedKeys)).map((k) => `'${k}'`).join(", ");
|
|
245
|
+
throw new DynamicFormError(DEV_MODE ? `Field key(s) contain a dot: ${list}. Dotted keys are not supported — express nesting with 'group' fields (e.g. { type: 'group', key: 'address', fields: [{ type: 'input', key: 'city' }] }).` : `Field key(s) contain a dot: ${list}.`);
|
|
246
|
+
}
|
|
247
|
+
validateNoDuplicateKeys(data.keys);
|
|
248
|
+
validateNoDomIdCollisions(data.domIds);
|
|
249
|
+
if (DEV_MODE) validateFieldTypesRegistered(data.types, registry, logger);
|
|
250
|
+
if (data.regexErrors.length > 0) throw new DynamicFormError(data.regexErrors.length === 1 ? data.regexErrors[0] : `Invalid regex pattern(s) found:\n${data.regexErrors.map((e) => ` - ${e}`).join("\n")}`);
|
|
251
|
+
}
|
|
252
|
+
function validateFieldAddons(field, fieldRegistry, typeRegistry, source) {
|
|
253
|
+
const warnings = [];
|
|
254
|
+
const incoming = field.addons;
|
|
255
|
+
if (!incoming?.length) return {
|
|
256
|
+
addons: [],
|
|
257
|
+
warnings
|
|
258
|
+
};
|
|
259
|
+
const fieldType = field.type;
|
|
260
|
+
const fieldDef = fieldRegistry.get(fieldType);
|
|
261
|
+
if (!fieldDef) {
|
|
262
|
+
warnings.push({
|
|
263
|
+
type: "unknown-field-type",
|
|
264
|
+
fieldKey: field.key,
|
|
265
|
+
fieldType
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
addons: [],
|
|
269
|
+
warnings
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const addonSupport = fieldDef.addons;
|
|
273
|
+
if (!addonSupport) {
|
|
274
|
+
warnings.push({
|
|
275
|
+
type: "field-type-no-addon-support",
|
|
276
|
+
fieldKey: field.key,
|
|
277
|
+
fieldType
|
|
278
|
+
});
|
|
279
|
+
return {
|
|
280
|
+
addons: [],
|
|
281
|
+
warnings
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const allowedSlots = addonSupport.slots;
|
|
285
|
+
const allowedTypes = addonSupport.allowedTypes;
|
|
286
|
+
const registeredTypes = Array.from(typeRegistry.keys());
|
|
287
|
+
const survivors = [];
|
|
288
|
+
for (const addon of incoming) {
|
|
289
|
+
if (!allowedSlots.includes(addon.slot)) {
|
|
290
|
+
warnings.push({
|
|
291
|
+
type: "unknown-slot",
|
|
292
|
+
fieldKey: field.key,
|
|
293
|
+
fieldType,
|
|
294
|
+
slot: String(addon.slot),
|
|
295
|
+
allowedSlots
|
|
296
|
+
});
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const typeDef = typeRegistry.get(addon.type);
|
|
300
|
+
if (!typeDef) {
|
|
301
|
+
warnings.push({
|
|
302
|
+
type: "unknown-type",
|
|
303
|
+
fieldKey: field.key,
|
|
304
|
+
addonType: addon.type,
|
|
305
|
+
registeredTypes
|
|
306
|
+
});
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (allowedTypes && !allowedTypes.includes(typeDef.type)) {
|
|
310
|
+
warnings.push({
|
|
311
|
+
type: "type-not-allowed",
|
|
312
|
+
fieldKey: field.key,
|
|
313
|
+
fieldType,
|
|
314
|
+
addonType: typeDef.type,
|
|
315
|
+
allowedTypes
|
|
316
|
+
});
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (source === "json" && typeDef.jsonSafe === false) {
|
|
320
|
+
warnings.push({
|
|
321
|
+
type: "code-only-type-in-json",
|
|
322
|
+
fieldKey: field.key,
|
|
323
|
+
addonType: typeDef.type
|
|
324
|
+
});
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
let sanitized = addon;
|
|
328
|
+
let stripped = false;
|
|
329
|
+
const ensureCopy = () => {
|
|
330
|
+
if (!stripped) sanitized = { ...sanitized };
|
|
331
|
+
stripped = true;
|
|
332
|
+
};
|
|
333
|
+
const declaredVariants = [
|
|
334
|
+
"preset",
|
|
335
|
+
"actionRef",
|
|
336
|
+
"action"
|
|
337
|
+
].filter((k) => sanitized[k] !== void 0);
|
|
338
|
+
if (source === "json") {
|
|
339
|
+
const dropReactiveFn = (key) => {
|
|
340
|
+
if (typeof sanitized[key] === "function") {
|
|
341
|
+
ensureCopy();
|
|
342
|
+
delete sanitized[key];
|
|
343
|
+
warnings.push({
|
|
344
|
+
type: "code-only-action-in-json",
|
|
345
|
+
fieldKey: field.key,
|
|
346
|
+
reason: `function '${key}' on type '${typeDef.type}' (use a signal/observable in code, or omit in JSON)`
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
if (typeof sanitized.action === "function") {
|
|
351
|
+
ensureCopy();
|
|
352
|
+
delete sanitized.action;
|
|
353
|
+
warnings.push({
|
|
354
|
+
type: "code-only-action-in-json",
|
|
355
|
+
fieldKey: field.key,
|
|
356
|
+
reason: `inline 'action' function on type '${typeDef.type}'`
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
dropReactiveFn("hidden");
|
|
360
|
+
dropReactiveFn("disabled");
|
|
361
|
+
dropReactiveFn("loading");
|
|
362
|
+
}
|
|
363
|
+
if (declaredVariants.length > 1) {
|
|
364
|
+
ensureCopy();
|
|
365
|
+
for (const k of declaredVariants.slice(1)) delete sanitized[k];
|
|
366
|
+
warnings.push({
|
|
367
|
+
type: "code-only-action-in-json",
|
|
368
|
+
fieldKey: field.key,
|
|
369
|
+
reason: `multiple click variants on type '${typeDef.type}' (${declaredVariants.join(", ")}); kept '${declaredVariants[0]}'`
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (stripped) {
|
|
373
|
+
survivors.push(sanitized);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (typeDef.validate) try {
|
|
377
|
+
typeDef.validate(addon, field.key);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
warnings.push({
|
|
380
|
+
type: "shape-violation",
|
|
381
|
+
fieldKey: field.key,
|
|
382
|
+
addonType: typeDef.type,
|
|
383
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
384
|
+
});
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
survivors.push(addon);
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
addons: survivors,
|
|
391
|
+
warnings
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function walkAndValidateAddons(fields, fieldRegistry, typeRegistry, source) {
|
|
395
|
+
const allWarnings = [];
|
|
396
|
+
return {
|
|
397
|
+
fields: fields.map((field) => {
|
|
398
|
+
let next = field;
|
|
399
|
+
if (field.addons?.length) {
|
|
400
|
+
const { addons, warnings } = validateFieldAddons(field, fieldRegistry, typeRegistry, source);
|
|
401
|
+
allWarnings.push(...warnings);
|
|
402
|
+
next = {
|
|
403
|
+
...next,
|
|
404
|
+
addons
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const children = next.fields;
|
|
408
|
+
if (Array.isArray(children)) {
|
|
409
|
+
const { fields: visited, warnings } = walkAndValidateAddons(children, fieldRegistry, typeRegistry, source);
|
|
410
|
+
allWarnings.push(...warnings);
|
|
411
|
+
next = {
|
|
412
|
+
...next,
|
|
413
|
+
fields: visited
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
return next;
|
|
417
|
+
}),
|
|
418
|
+
warnings: allWarnings
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function addonWarningKey(w) {
|
|
422
|
+
switch (w.type) {
|
|
423
|
+
case "unknown-field-type": return `${w.type}|${w.fieldKey}|${w.fieldType}`;
|
|
424
|
+
case "field-type-no-addon-support": return `${w.type}|${w.fieldKey}|${w.fieldType}`;
|
|
425
|
+
case "unknown-slot": return `${w.type}|${w.fieldKey}|${w.fieldType}|${w.slot}`;
|
|
426
|
+
case "unknown-type": return `${w.type}|${w.fieldKey}|${w.addonType}`;
|
|
427
|
+
case "type-not-allowed": return `${w.type}|${w.fieldKey}|${w.fieldType}|${w.addonType}`;
|
|
428
|
+
case "shape-violation": return `${w.type}|${w.fieldKey}|${w.addonType}|${w.reason}`;
|
|
429
|
+
case "code-only-type-in-json": return `${w.type}|${w.fieldKey}|${w.addonType}`;
|
|
430
|
+
case "code-only-action-in-json": return `${w.type}|${w.fieldKey}|${w.reason}`;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function formatAddonWarning(w) {
|
|
434
|
+
switch (w.type) {
|
|
435
|
+
case "unknown-field-type": return `Field '${w.fieldKey}': unknown field type '${w.fieldType}'. Addons dropped.`;
|
|
436
|
+
case "field-type-no-addon-support": return `Field '${w.fieldKey}' (type: '${w.fieldType}'): this field type does not declare addon support. Addons dropped. If this is incorrect, add 'addons: { slots: [...] }' to the field type registration.`;
|
|
437
|
+
case "unknown-slot": return `Field '${w.fieldKey}' (type: '${w.fieldType}'): slot '${w.slot}' is not supported. Allowed: ${w.allowedSlots.join(", ")}. Addon dropped.`;
|
|
438
|
+
case "unknown-type": return `Field '${w.fieldKey}': addon type '${w.addonType}' is not registered. Did you forget withCustomAddon({ type: '${w.addonType}', ... })? Currently registered: ${w.registeredTypes.join(", ") || "(none)"}. Addon dropped.`;
|
|
439
|
+
case "type-not-allowed": return `Field '${w.fieldKey}' (type: '${w.fieldType}'): addon type '${w.addonType}' is not in the allowed list. Allowed: ${w.allowedTypes.join(", ")}. Addon dropped.`;
|
|
440
|
+
case "shape-violation": return `Field '${w.fieldKey}': addon type '${w.addonType}' shape invalid: ${w.reason}. Addon dropped.`;
|
|
441
|
+
case "code-only-type-in-json": return `Field '${w.fieldKey}': addon type '${w.addonType}' is code-only and cannot be used with JSON-derived configs. Addon dropped.`;
|
|
442
|
+
case "code-only-action-in-json": return `Field '${w.fieldKey}': ${w.reason}. Use 'preset' or 'actionRef' instead. Addon dropped.`;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const VALUE_EXCLUSION_DEFAULTS = new InjectionToken("VALUE_EXCLUSION_DEFAULTS", {
|
|
446
|
+
providedIn: "root",
|
|
447
|
+
factory: () => ({
|
|
448
|
+
excludeValueIfHidden: true,
|
|
449
|
+
excludeValueIfDisabled: true,
|
|
450
|
+
excludeValueIfReadonly: true
|
|
451
|
+
})
|
|
452
|
+
});
|
|
453
|
+
function resolveExclusionConfig(global, form, field) {
|
|
454
|
+
return {
|
|
455
|
+
excludeValueIfHidden: field?.excludeValueIfHidden ?? form?.excludeValueIfHidden ?? global.excludeValueIfHidden,
|
|
456
|
+
excludeValueIfDisabled: field?.excludeValueIfDisabled ?? form?.excludeValueIfDisabled ?? global.excludeValueIfDisabled,
|
|
457
|
+
excludeValueIfReadonly: field?.excludeValueIfReadonly ?? form?.excludeValueIfReadonly ?? global.excludeValueIfReadonly
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function hasEnabledValueExclusion(fields, global, form) {
|
|
461
|
+
return fields.some((field) => {
|
|
462
|
+
const resolved = resolveExclusionConfig(global, form, field);
|
|
463
|
+
if (resolved.excludeValueIfHidden || resolved.excludeValueIfDisabled || resolved.excludeValueIfReadonly) return true;
|
|
464
|
+
return isGroupField(field) && !!field.fields && hasEnabledValueExclusion(Object.values(field.fields), global, form);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function filterDeclaredValueShape(rawValue, fields, registry) {
|
|
468
|
+
const kept = {};
|
|
469
|
+
for (const field of fields) {
|
|
470
|
+
const key = field.key;
|
|
471
|
+
if (!key || !(key in rawValue)) continue;
|
|
472
|
+
const valueHandling = getFieldValueHandling(field.type, registry);
|
|
473
|
+
if (valueHandling === "exclude" || valueHandling === "flatten") continue;
|
|
474
|
+
const value = rawValue[key];
|
|
475
|
+
if (isGroupField(field) && field.fields && value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
476
|
+
kept[key] = filterDeclaredValueShape(value, Object.values(field.fields), registry);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
kept[key] = value;
|
|
480
|
+
}
|
|
481
|
+
return kept;
|
|
482
|
+
}
|
|
483
|
+
function shouldExcludeField(field, fieldState, config) {
|
|
484
|
+
const state = fieldState();
|
|
485
|
+
const inherited = field.inheritedExclusionState;
|
|
486
|
+
const isHidden = state.hidden() || field.hidden === true || inherited?.hidden === true;
|
|
487
|
+
if (config.excludeValueIfHidden && isHidden) return true;
|
|
488
|
+
const isDisabled = state.disabled() || field.disabled === true || inherited?.disabled === true;
|
|
489
|
+
if (config.excludeValueIfDisabled && isDisabled) return true;
|
|
490
|
+
const isReadonly = state.readonly() || field.readonly === true || inherited?.readonly === true;
|
|
491
|
+
if (config.excludeValueIfReadonly && isReadonly) return true;
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
function filterFormValue(rawValue, schemaFields, formTree, registry, globalDefaults, formOptions, exclusionEnabled = hasEnabledValueExclusion(schemaFields, globalDefaults, formOptions)) {
|
|
495
|
+
if (!exclusionEnabled) return filterDeclaredValueShape(rawValue, schemaFields, registry);
|
|
496
|
+
const result = {};
|
|
497
|
+
for (const field of schemaFields) {
|
|
498
|
+
const key = field.key;
|
|
499
|
+
if (!key) continue;
|
|
500
|
+
const valueHandling = getFieldValueHandling(field.type, registry);
|
|
501
|
+
if (valueHandling === "exclude" || valueHandling === "flatten") continue;
|
|
502
|
+
if (!(key in rawValue)) continue;
|
|
503
|
+
const resolvedConfig = resolveExclusionConfig(globalDefaults, formOptions, {
|
|
504
|
+
excludeValueIfHidden: field.excludeValueIfHidden,
|
|
505
|
+
excludeValueIfDisabled: field.excludeValueIfDisabled,
|
|
506
|
+
excludeValueIfReadonly: field.excludeValueIfReadonly
|
|
507
|
+
});
|
|
508
|
+
const fieldState = formTree[key];
|
|
509
|
+
if (!fieldState || typeof fieldState !== "function") {
|
|
510
|
+
const inherited = field.inheritedExclusionState;
|
|
511
|
+
if (!(resolvedConfig.excludeValueIfHidden && (field.hidden === true || inherited?.hidden === true) || resolvedConfig.excludeValueIfDisabled && (field.disabled === true || inherited?.disabled === true) || resolvedConfig.excludeValueIfReadonly && (field.readonly === true || inherited?.readonly === true))) result[key] = rawValue[key];
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (shouldExcludeField(field, fieldState, resolvedConfig)) continue;
|
|
515
|
+
if (isGroupField(field) && field.fields) {
|
|
516
|
+
const groupValue = rawValue[key];
|
|
517
|
+
if (groupValue && typeof groupValue === "object") result[key] = filterFormValue(groupValue, Object.values(field.fields), fieldState, registry, globalDefaults, formOptions, true);
|
|
518
|
+
else result[key] = groupValue;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (isArrayField(field)) {
|
|
522
|
+
result[key] = rawValue[key];
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
result[key] = rawValue[key];
|
|
526
|
+
}
|
|
527
|
+
return result;
|
|
528
|
+
}
|
|
529
|
+
const LifecycleState = {
|
|
530
|
+
Uninitialized: "uninitialized",
|
|
531
|
+
Initializing: "initializing",
|
|
532
|
+
Ready: "ready",
|
|
533
|
+
Transitioning: "transitioning",
|
|
534
|
+
Destroyed: "destroyed"
|
|
535
|
+
};
|
|
536
|
+
const Phase = {
|
|
537
|
+
Teardown: "teardown",
|
|
538
|
+
Applying: "applying",
|
|
539
|
+
Restoring: "restoring"
|
|
540
|
+
};
|
|
541
|
+
const Action = {
|
|
542
|
+
Initialize: "initialize",
|
|
543
|
+
ConfigChange: "config-change",
|
|
544
|
+
SetupComplete: "setup-complete",
|
|
545
|
+
ValueCaptured: "value-captured",
|
|
546
|
+
TeardownComplete: "teardown-complete",
|
|
547
|
+
ApplyComplete: "apply-complete",
|
|
548
|
+
RestoreComplete: "restore-complete",
|
|
549
|
+
Destroy: "destroy"
|
|
550
|
+
};
|
|
551
|
+
const Effect = {
|
|
552
|
+
CaptureValue: "capture-value",
|
|
553
|
+
WaitFrameBoundary: "wait-frame-boundary",
|
|
554
|
+
CreateForm: "create-form",
|
|
555
|
+
RestoreValues: "restore-values"
|
|
556
|
+
};
|
|
557
|
+
function isReadyState(state) {
|
|
558
|
+
return state.type === LifecycleState.Ready;
|
|
559
|
+
}
|
|
560
|
+
function isTransitioningState(state) {
|
|
561
|
+
return state.type === LifecycleState.Transitioning;
|
|
562
|
+
}
|
|
563
|
+
function createUninitializedState() {
|
|
564
|
+
return { type: LifecycleState.Uninitialized };
|
|
565
|
+
}
|
|
566
|
+
function createInitializingState(config) {
|
|
567
|
+
return {
|
|
568
|
+
type: LifecycleState.Initializing,
|
|
569
|
+
config
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function createReadyState(config, formSetup) {
|
|
573
|
+
return {
|
|
574
|
+
type: LifecycleState.Ready,
|
|
575
|
+
config,
|
|
576
|
+
formSetup
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function createTransitioningState(phase, currentConfig, pendingConfig, currentFormSetup, preservedValue, pendingFormSetup) {
|
|
580
|
+
return {
|
|
581
|
+
type: LifecycleState.Transitioning,
|
|
582
|
+
phase,
|
|
583
|
+
currentConfig,
|
|
584
|
+
pendingConfig,
|
|
585
|
+
currentFormSetup,
|
|
586
|
+
preservedValue,
|
|
587
|
+
pendingFormSetup
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function createDestroyedState() {
|
|
591
|
+
return { type: LifecycleState.Destroyed };
|
|
592
|
+
}
|
|
593
|
+
var SideEffectScheduler = class {
|
|
594
|
+
injector;
|
|
595
|
+
abortController = new AbortController();
|
|
596
|
+
get destroyed() {
|
|
597
|
+
return this.abortController.signal.aborted;
|
|
598
|
+
}
|
|
599
|
+
constructor(options) {
|
|
600
|
+
this.injector = options.injector;
|
|
601
|
+
options.destroyRef.onDestroy(() => {
|
|
602
|
+
this.abortController.abort();
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
executeBlocking(effect) {
|
|
606
|
+
return new Observable((subscriber) => {
|
|
607
|
+
if (this.destroyed) {
|
|
608
|
+
subscriber.complete();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
const result = effect();
|
|
613
|
+
subscriber.next(result);
|
|
614
|
+
subscriber.complete();
|
|
615
|
+
} catch (error) {
|
|
616
|
+
subscriber.error(error);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
executeAtFrameBoundary(effect, options) {
|
|
621
|
+
if (options?.skipIf?.()) return this.executeBlocking(effect);
|
|
622
|
+
return new Observable((subscriber) => {
|
|
623
|
+
if (this.destroyed) {
|
|
624
|
+
subscriber.complete();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const frameId = requestAnimationFrame(() => {
|
|
628
|
+
if (this.destroyed) {
|
|
629
|
+
subscriber.complete();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
const result = effect();
|
|
634
|
+
subscriber.next(result);
|
|
635
|
+
subscriber.complete();
|
|
636
|
+
} catch (error) {
|
|
637
|
+
subscriber.error(error);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
return () => {
|
|
641
|
+
cancelAnimationFrame(frameId);
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
executeAfterRender(effect) {
|
|
646
|
+
return new Observable((subscriber) => {
|
|
647
|
+
if (this.destroyed) {
|
|
648
|
+
subscriber.complete();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const local = new AbortController();
|
|
652
|
+
afterNextRender(() => {
|
|
653
|
+
if (local.signal.aborted || this.destroyed) {
|
|
654
|
+
subscriber.complete();
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
const result = effect();
|
|
659
|
+
subscriber.next(result);
|
|
660
|
+
subscriber.complete();
|
|
661
|
+
} catch (error) {
|
|
662
|
+
subscriber.error(error);
|
|
663
|
+
}
|
|
664
|
+
}, { injector: this.injector });
|
|
665
|
+
return () => {
|
|
666
|
+
local.abort();
|
|
667
|
+
};
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
function createSideEffectScheduler(injector) {
|
|
672
|
+
return new SideEffectScheduler({
|
|
673
|
+
injector,
|
|
674
|
+
destroyRef: injector.get(DestroyRef)
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
var FormStateMachine = class {
|
|
678
|
+
config;
|
|
679
|
+
actions$ = new Subject();
|
|
680
|
+
_state;
|
|
681
|
+
state;
|
|
682
|
+
state$;
|
|
683
|
+
get currentState() {
|
|
684
|
+
return this._state();
|
|
685
|
+
}
|
|
686
|
+
constructor(config) {
|
|
687
|
+
this.config = config;
|
|
688
|
+
this._state = signal(createUninitializedState(), ...ngDevMode ? [{ debugName: "_state" }] : /* istanbul ignore next */ []);
|
|
689
|
+
this.state = this._state.asReadonly();
|
|
690
|
+
this.state$ = toObservable(this._state, { injector: config.injector });
|
|
691
|
+
this.setupActionProcessing();
|
|
692
|
+
}
|
|
693
|
+
dispatch(action) {
|
|
694
|
+
this.actions$.next(action);
|
|
695
|
+
}
|
|
696
|
+
setupActionProcessing() {
|
|
697
|
+
this.actions$.pipe(concatMap((action) => this.processAction(action).pipe(catchError((error) => {
|
|
698
|
+
this.config.logger.error(`Action '${action.type}' failed:`, error);
|
|
699
|
+
this.recoverFromError(error, action);
|
|
700
|
+
return EMPTY;
|
|
701
|
+
}))), takeUntilDestroyed(this.config.destroyRef)).subscribe();
|
|
702
|
+
}
|
|
703
|
+
processAction(action) {
|
|
704
|
+
const currentState = this._state();
|
|
705
|
+
const result = this.computeTransition(currentState, action);
|
|
706
|
+
this.config.onTransition?.({
|
|
707
|
+
from: currentState,
|
|
708
|
+
to: result.state,
|
|
709
|
+
action,
|
|
710
|
+
timestamp: Date.now()
|
|
711
|
+
});
|
|
712
|
+
this._state.set(result.state);
|
|
713
|
+
return this.executeSideEffects(result.sideEffects);
|
|
714
|
+
}
|
|
715
|
+
recoverFromError(error, action) {
|
|
716
|
+
const state = this._state();
|
|
717
|
+
if (state.type === LifecycleState.Initializing) this._state.set(createUninitializedState());
|
|
718
|
+
else if (isTransitioningState(state)) this._state.set(createReadyState(state.currentConfig, state.currentFormSetup));
|
|
719
|
+
this.config.onError?.(error, action);
|
|
720
|
+
}
|
|
721
|
+
computeTransition(state, action) {
|
|
722
|
+
switch (action.type) {
|
|
723
|
+
case Action.Initialize: return this.handleInitialize(state, action.config);
|
|
724
|
+
case Action.ConfigChange: return this.handleConfigChange(state, action.config);
|
|
725
|
+
case Action.SetupComplete: return this.handleSetupComplete(state, action.formSetup);
|
|
726
|
+
case Action.ValueCaptured: return this.handleValueCaptured(state, action.value);
|
|
727
|
+
case Action.TeardownComplete: return this.handleTeardownComplete(state);
|
|
728
|
+
case Action.ApplyComplete: return this.handleApplyComplete(state, action.formSetup);
|
|
729
|
+
case Action.RestoreComplete: return this.handleRestoreComplete(state);
|
|
730
|
+
case Action.Destroy: return this.handleDestroy();
|
|
731
|
+
default: return assertNever(action);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
handleInitialize(state, config) {
|
|
735
|
+
if (state.type !== LifecycleState.Uninitialized) return {
|
|
736
|
+
state,
|
|
737
|
+
sideEffects: []
|
|
738
|
+
};
|
|
739
|
+
return {
|
|
740
|
+
state: createInitializingState(config),
|
|
741
|
+
sideEffects: [{ type: Effect.CreateForm }]
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
handleConfigChange(state, config) {
|
|
745
|
+
if (state.type === LifecycleState.Uninitialized) return {
|
|
746
|
+
state: createInitializingState(config),
|
|
747
|
+
sideEffects: [{ type: Effect.CreateForm }]
|
|
748
|
+
};
|
|
749
|
+
if (state.type === LifecycleState.Initializing) return {
|
|
750
|
+
state: createInitializingState(config),
|
|
751
|
+
sideEffects: [{ type: Effect.CreateForm }]
|
|
752
|
+
};
|
|
753
|
+
if (this.config.isSubmitting?.()) {
|
|
754
|
+
this.config.logger.warn("Config changed during active submission; ignoring. Apply config changes after submission completes.");
|
|
755
|
+
return {
|
|
756
|
+
state,
|
|
757
|
+
sideEffects: []
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
if (isReadyState(state)) return {
|
|
761
|
+
state: createTransitioningState(Phase.Teardown, state.config, config, state.formSetup),
|
|
762
|
+
sideEffects: [{ type: Effect.CaptureValue }, { type: Effect.WaitFrameBoundary }]
|
|
763
|
+
};
|
|
764
|
+
if (isTransitioningState(state)) return {
|
|
765
|
+
state: createTransitioningState(state.phase, state.currentConfig, config, state.currentFormSetup, state.preservedValue, state.pendingFormSetup),
|
|
766
|
+
sideEffects: []
|
|
767
|
+
};
|
|
768
|
+
return {
|
|
769
|
+
state,
|
|
770
|
+
sideEffects: []
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
handleSetupComplete(state, formSetup) {
|
|
774
|
+
if (state.type === LifecycleState.Initializing) return {
|
|
775
|
+
state: createReadyState(state.config, formSetup),
|
|
776
|
+
sideEffects: []
|
|
777
|
+
};
|
|
778
|
+
return {
|
|
779
|
+
state,
|
|
780
|
+
sideEffects: []
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
handleValueCaptured(state, value) {
|
|
784
|
+
if (!isTransitioningState(state)) return {
|
|
785
|
+
state,
|
|
786
|
+
sideEffects: []
|
|
787
|
+
};
|
|
788
|
+
return {
|
|
789
|
+
state: createTransitioningState(state.phase, state.currentConfig, state.pendingConfig, state.currentFormSetup, value, state.pendingFormSetup),
|
|
790
|
+
sideEffects: []
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
handleTeardownComplete(state) {
|
|
794
|
+
if (!isTransitioningState(state) || state.phase !== Phase.Teardown) return {
|
|
795
|
+
state,
|
|
796
|
+
sideEffects: []
|
|
797
|
+
};
|
|
798
|
+
return {
|
|
799
|
+
state: createTransitioningState(Phase.Applying, state.currentConfig, state.pendingConfig, state.currentFormSetup, state.preservedValue),
|
|
800
|
+
sideEffects: [{ type: Effect.CreateForm }]
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
handleApplyComplete(state, formSetup) {
|
|
804
|
+
if (!isTransitioningState(state) || state.phase !== Phase.Applying) return {
|
|
805
|
+
state,
|
|
806
|
+
sideEffects: []
|
|
807
|
+
};
|
|
808
|
+
if (state.preservedValue && Object.keys(state.preservedValue).length > 0) return {
|
|
809
|
+
state: createTransitioningState(Phase.Restoring, state.currentConfig, state.pendingConfig, state.currentFormSetup, state.preservedValue, formSetup),
|
|
810
|
+
sideEffects: [{
|
|
811
|
+
type: Effect.RestoreValues,
|
|
812
|
+
values: state.preservedValue
|
|
813
|
+
}]
|
|
814
|
+
};
|
|
815
|
+
return {
|
|
816
|
+
state: createReadyState(state.pendingConfig, formSetup),
|
|
817
|
+
sideEffects: []
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
handleRestoreComplete(state) {
|
|
821
|
+
if (!isTransitioningState(state) || state.phase !== Phase.Restoring) return {
|
|
822
|
+
state,
|
|
823
|
+
sideEffects: []
|
|
824
|
+
};
|
|
825
|
+
if (DEV_MODE && !state.pendingFormSetup) this.config.logger.warn("handleRestoreComplete: pendingFormSetup was not set — falling back to recomputing. This indicates a bug in the transition flow.");
|
|
826
|
+
const formSetup = state.pendingFormSetup ?? this.config.createFormSetup(state.pendingConfig);
|
|
827
|
+
return {
|
|
828
|
+
state: createReadyState(state.pendingConfig, formSetup),
|
|
829
|
+
sideEffects: []
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
handleDestroy() {
|
|
833
|
+
return {
|
|
834
|
+
state: createDestroyedState(),
|
|
835
|
+
sideEffects: []
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
executeSideEffects(effects) {
|
|
839
|
+
if (effects.length === 0) return of(void 0);
|
|
840
|
+
return effects.reduce((chain$, effect) => chain$.pipe(concatMap(() => this.executeSideEffect(effect))), of(void 0));
|
|
841
|
+
}
|
|
842
|
+
executeSideEffect(effect) {
|
|
843
|
+
const { scheduler } = this.config;
|
|
844
|
+
switch (effect.type) {
|
|
845
|
+
case Effect.CaptureValue: return scheduler.executeBlocking(() => {
|
|
846
|
+
const value = this.config.captureValue();
|
|
847
|
+
this.dispatch({
|
|
848
|
+
type: Action.ValueCaptured,
|
|
849
|
+
value
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
case Effect.WaitFrameBoundary: return scheduler.executeAtFrameBoundary(() => {
|
|
853
|
+
this.dispatch({ type: Action.TeardownComplete });
|
|
854
|
+
}, { skipIf: this.config.isFieldPipelineSettled });
|
|
855
|
+
case Effect.CreateForm: return scheduler.executeBlocking(() => {
|
|
856
|
+
const state = untracked(() => this._state());
|
|
857
|
+
let config;
|
|
858
|
+
if (state.type === LifecycleState.Initializing) config = state.config;
|
|
859
|
+
else if (isTransitioningState(state) && state.phase === Phase.Applying) config = state.pendingConfig;
|
|
860
|
+
if (!config) return;
|
|
861
|
+
const formSetup = this.config.createFormSetup(config);
|
|
862
|
+
this.config.onFormCreated?.(formSetup);
|
|
863
|
+
if (state.type === LifecycleState.Initializing) this.dispatch({
|
|
864
|
+
type: Action.SetupComplete,
|
|
865
|
+
formSetup
|
|
866
|
+
});
|
|
867
|
+
else if (isTransitioningState(state) && state.phase === Phase.Applying) this.dispatch({
|
|
868
|
+
type: Action.ApplyComplete,
|
|
869
|
+
formSetup
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
case Effect.RestoreValues: return scheduler.executeAfterRender(() => {
|
|
873
|
+
const state = untracked(() => this._state());
|
|
874
|
+
if (!isTransitioningState(state) || state.phase !== Phase.Restoring) return;
|
|
875
|
+
if (DEV_MODE && !state.pendingFormSetup) this.config.logger.warn("RestoreValues effect: pendingFormSetup was not set — falling back to recomputing.");
|
|
876
|
+
const formSetup = state.pendingFormSetup ?? this.config.createFormSetup(state.pendingConfig);
|
|
877
|
+
const oldTypeByKey = /* @__PURE__ */ new Map();
|
|
878
|
+
for (const f of state.currentFormSetup.schemaFields) if (f.key !== void 0) oldTypeByKey.set(f.key, f.type);
|
|
879
|
+
const validKeys = /* @__PURE__ */ new Set();
|
|
880
|
+
for (const f of formSetup.schemaFields) if (f.key !== void 0 && oldTypeByKey.get(f.key) === f.type) validKeys.add(f.key);
|
|
881
|
+
this.config.restoreValue(effect.values, validKeys);
|
|
882
|
+
this.dispatch({ type: Action.RestoreComplete });
|
|
883
|
+
});
|
|
884
|
+
default: return assertNever(effect);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
function createFormStateMachine(config) {
|
|
889
|
+
return new FormStateMachine(config);
|
|
890
|
+
}
|
|
891
|
+
const FORM_STATE_DEPS = new InjectionToken("FORM_STATE_DEPS");
|
|
892
|
+
function asFieldTreeRecord(tree) {
|
|
893
|
+
return tree;
|
|
894
|
+
}
|
|
895
|
+
const BOUND_VALUE_EXCLUSION_BASELINE = {
|
|
896
|
+
excludeValueIfHidden: false,
|
|
897
|
+
excludeValueIfDisabled: false,
|
|
898
|
+
excludeValueIfReadonly: false
|
|
899
|
+
};
|
|
900
|
+
function isFieldExcluded(axes) {
|
|
901
|
+
return !!axes && (axes.hidden || axes.disabled || axes.readonly);
|
|
902
|
+
}
|
|
903
|
+
function collectFieldStateSnapshot(fields, treeRecord, pathParts, out) {
|
|
904
|
+
for (const field of fields) {
|
|
905
|
+
const key = field.key;
|
|
906
|
+
if (!key) continue;
|
|
907
|
+
const subtree = treeRecord[key];
|
|
908
|
+
if (!subtree || typeof subtree !== "function") continue;
|
|
909
|
+
const state = subtree();
|
|
910
|
+
const path = [...pathParts, key];
|
|
911
|
+
out[path.join(".")] = {
|
|
912
|
+
hidden: state.hidden(),
|
|
913
|
+
disabled: state.disabled(),
|
|
914
|
+
readonly: state.readonly()
|
|
915
|
+
};
|
|
916
|
+
if (isGroupField(field) && field.fields) collectFieldStateSnapshot(field.fields, asFieldTreeRecord(subtree), path, out);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function getValueAtPath(source, segments) {
|
|
920
|
+
let current = source;
|
|
921
|
+
for (const segment of segments) {
|
|
922
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
923
|
+
current = current[segment];
|
|
924
|
+
}
|
|
925
|
+
return current;
|
|
926
|
+
}
|
|
927
|
+
function isRestoreRelevant(value) {
|
|
928
|
+
return !(typeof value === "number" && Number.isNaN(value));
|
|
929
|
+
}
|
|
930
|
+
function setValueAtPath(target, segments, value) {
|
|
931
|
+
if (segments.length === 0) return target;
|
|
932
|
+
const [head, ...rest] = segments;
|
|
933
|
+
const next = { ...target };
|
|
934
|
+
if (rest.length === 0) next[head] = value;
|
|
935
|
+
else {
|
|
936
|
+
const child = target[head];
|
|
937
|
+
next[head] = setValueAtPath(child !== null && typeof child === "object" ? child : {}, rest, value);
|
|
938
|
+
}
|
|
939
|
+
return next;
|
|
940
|
+
}
|
|
941
|
+
var FormStateManager = class FormStateManager {
|
|
942
|
+
injector = inject(Injector);
|
|
943
|
+
destroyRef = inject(DestroyRef);
|
|
944
|
+
logger = inject(DynamicFormLogger);
|
|
945
|
+
eventBus = inject(EventBus);
|
|
946
|
+
functionRegistry = inject(FunctionRegistryService);
|
|
947
|
+
schemaRegistry = inject(SchemaRegistryService);
|
|
948
|
+
arrayItemRegistry = inject(ArrayItemRegistryService, { optional: true });
|
|
949
|
+
deps = (() => {
|
|
950
|
+
const raw = inject(FORM_STATE_DEPS);
|
|
951
|
+
if (!raw.config || !raw.formOptions || !raw.value) throw new DynamicFormError("FormStateDeps must be connected before FormStateManager is created. Ensure DynamicForm assigns its input signals to FORM_STATE_DEPS.");
|
|
952
|
+
return raw;
|
|
953
|
+
})();
|
|
954
|
+
valueExclusionDefaults = inject(VALUE_EXCLUSION_DEFAULTS);
|
|
955
|
+
fieldRegistry = injectFieldRegistry();
|
|
956
|
+
addonKindRegistry = inject(ADDON_TYPE_REGISTRY);
|
|
957
|
+
lastAddonWarningKeys = /* @__PURE__ */ new Set();
|
|
958
|
+
scheduler = createSideEffectScheduler(this.injector);
|
|
959
|
+
machine;
|
|
960
|
+
fieldProcessors = inject(CONTAINER_FIELD_PROCESSORS);
|
|
961
|
+
fieldLoadingErrors = signal([], ...ngDevMode ? [{ debugName: "fieldLoadingErrors" }] : /* istanbul ignore next */ []);
|
|
962
|
+
_formCache = { current: void 0 };
|
|
963
|
+
activeConfig = computed(() => {
|
|
964
|
+
const state = this.machine.state();
|
|
965
|
+
if (state.type === LifecycleState.Uninitialized) return this.deps.config();
|
|
966
|
+
if (isReadyState(state)) return state.config;
|
|
967
|
+
if (isTransitioningState(state)) {
|
|
968
|
+
if (state.phase === Phase.Teardown || state.phase === Phase.Applying) return state.currentConfig;
|
|
969
|
+
return state.pendingConfig;
|
|
970
|
+
}
|
|
971
|
+
if (state.type === LifecycleState.Initializing) return state.config;
|
|
972
|
+
}, ...ngDevMode ? [{ debugName: "activeConfig" }] : /* istanbul ignore next */ []);
|
|
973
|
+
renderPhase = computed(() => {
|
|
974
|
+
const state = this.machine.state();
|
|
975
|
+
if (isTransitioningState(state) && state.phase === Phase.Teardown) return Phase.Teardown;
|
|
976
|
+
return "render";
|
|
977
|
+
}, ...ngDevMode ? [{ debugName: "renderPhase" }] : /* istanbul ignore next */ []);
|
|
978
|
+
shouldRender = computed(() => this.activeConfig() !== void 0, ...ngDevMode ? [{ debugName: "shouldRender" }] : /* istanbul ignore next */ []);
|
|
979
|
+
formModeDetection = computed(() => {
|
|
980
|
+
const config = this.activeConfig();
|
|
981
|
+
if (!config) return {
|
|
982
|
+
mode: "non-paged",
|
|
983
|
+
isValid: true,
|
|
984
|
+
errors: []
|
|
985
|
+
};
|
|
986
|
+
return detectFormMode(config.fields || []);
|
|
987
|
+
}, ...ngDevMode ? [{ debugName: "formModeDetection" }] : /* istanbul ignore next */ []);
|
|
988
|
+
effectiveFormOptions = computed(() => {
|
|
989
|
+
const configOptions = this.activeConfig()?.options || {};
|
|
990
|
+
const inputOptions = this.deps.formOptions() ?? void 0;
|
|
991
|
+
return {
|
|
992
|
+
...configOptions,
|
|
993
|
+
...inputOptions
|
|
994
|
+
};
|
|
995
|
+
}, ...ngDevMode ? [{ debugName: "effectiveFormOptions" }] : /* istanbul ignore next */ []);
|
|
996
|
+
pendingPageRequest = signal(null, ...ngDevMode ? [{ debugName: "pendingPageRequest" }] : /* istanbul ignore next */ []);
|
|
997
|
+
pageFieldDefinitions = computed(() => {
|
|
998
|
+
const config = this.activeConfig();
|
|
999
|
+
if (this.formModeDetection().mode === "paged" && config?.fields) return config.fields.filter(isPageField);
|
|
1000
|
+
return [];
|
|
1001
|
+
}, ...ngDevMode ? [{ debugName: "pageFieldDefinitions" }] : /* istanbul ignore next */ []);
|
|
1002
|
+
rawFieldRegistry = computed(() => this.fieldRegistry.raw, ...ngDevMode ? [{ debugName: "rawFieldRegistry" }] : /* istanbul ignore next */ []);
|
|
1003
|
+
formSetup = computed(() => {
|
|
1004
|
+
const registry = this.rawFieldRegistry();
|
|
1005
|
+
const state = this.machine.state();
|
|
1006
|
+
if (isReadyState(state)) return state.formSetup;
|
|
1007
|
+
if (isTransitioningState(state)) {
|
|
1008
|
+
if (state.phase === Phase.Restoring && state.pendingFormSetup) return state.pendingFormSetup;
|
|
1009
|
+
return state.currentFormSetup;
|
|
1010
|
+
}
|
|
1011
|
+
const config = this.activeConfig();
|
|
1012
|
+
if (config) this.registerValidatorsFromConfig(config);
|
|
1013
|
+
if (!config) return this.createEmptyFormSetup(registry);
|
|
1014
|
+
if (config.fields && config.fields.length > 0) {
|
|
1015
|
+
const modeDetection = this.formModeDetection();
|
|
1016
|
+
return this.createFormSetupFromConfig(config.fields, modeDetection.mode, registry);
|
|
1017
|
+
}
|
|
1018
|
+
return this.createEmptyFormSetup(registry);
|
|
1019
|
+
}, ...ngDevMode ? [{ debugName: "formSetup" }] : /* istanbul ignore next */ []);
|
|
1020
|
+
formSetupConfig = computed(() => {
|
|
1021
|
+
const state = this.machine.state();
|
|
1022
|
+
if (isReadyState(state)) return state.config;
|
|
1023
|
+
if (isTransitioningState(state)) {
|
|
1024
|
+
if (state.phase === Phase.Restoring && state.pendingFormSetup) return state.pendingConfig;
|
|
1025
|
+
return state.currentConfig;
|
|
1026
|
+
}
|
|
1027
|
+
if (state.type === LifecycleState.Initializing) return state.config;
|
|
1028
|
+
if (state.type === LifecycleState.Uninitialized) return this.activeConfig();
|
|
1029
|
+
}, ...ngDevMode ? [{ debugName: "formSetupConfig" }] : /* istanbul ignore next */ []);
|
|
1030
|
+
activePageState = linkedSignal({
|
|
1031
|
+
...ngDevMode ? { debugName: "activePageState" } : /* istanbul ignore next */ {},
|
|
1032
|
+
source: this.activeConfig,
|
|
1033
|
+
computation: (config) => ({
|
|
1034
|
+
config,
|
|
1035
|
+
initialized: false,
|
|
1036
|
+
index: 0
|
|
1037
|
+
})
|
|
1038
|
+
});
|
|
1039
|
+
defaultValues = linkedSignal(() => this.formSetup().defaultValues, ...ngDevMode ? [{ debugName: "defaultValues" }] : /* istanbul ignore next */ []);
|
|
1040
|
+
validKeys = computed(() => {
|
|
1041
|
+
const schemaFields = this.formSetup().schemaFields;
|
|
1042
|
+
if (!schemaFields || schemaFields.length === 0) return void 0;
|
|
1043
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1044
|
+
for (const field of schemaFields) {
|
|
1045
|
+
if (field.key === void 0) continue;
|
|
1046
|
+
keys.add(field.key);
|
|
1047
|
+
}
|
|
1048
|
+
return keys;
|
|
1049
|
+
}, ...ngDevMode ? [{ debugName: "validKeys" }] : /* istanbul ignore next */ []);
|
|
1050
|
+
boundValueExclusionEnabled = computed(() => {
|
|
1051
|
+
const setup = this.formSetup();
|
|
1052
|
+
if (!setup.schemaFields || setup.schemaFields.length === 0) return false;
|
|
1053
|
+
const options = this.effectiveFormOptions();
|
|
1054
|
+
return hasEnabledValueExclusion(setup.schemaFields, BOUND_VALUE_EXCLUSION_BASELINE, {
|
|
1055
|
+
excludeValueIfHidden: options.excludeValueIfHidden,
|
|
1056
|
+
excludeValueIfDisabled: options.excludeValueIfDisabled,
|
|
1057
|
+
excludeValueIfReadonly: options.excludeValueIfReadonly
|
|
1058
|
+
});
|
|
1059
|
+
}, ...ngDevMode ? [{ debugName: "boundValueExclusionEnabled" }] : /* istanbul ignore next */ []);
|
|
1060
|
+
valueExclusionEnabled = computed(() => {
|
|
1061
|
+
const setup = this.formSetup();
|
|
1062
|
+
if (!setup.schemaFields || setup.schemaFields.length === 0) return false;
|
|
1063
|
+
const options = this.effectiveFormOptions();
|
|
1064
|
+
return hasEnabledValueExclusion(setup.schemaFields, this.valueExclusionDefaults, {
|
|
1065
|
+
excludeValueIfHidden: options.excludeValueIfHidden,
|
|
1066
|
+
excludeValueIfDisabled: options.excludeValueIfDisabled,
|
|
1067
|
+
excludeValueIfReadonly: options.excludeValueIfReadonly
|
|
1068
|
+
});
|
|
1069
|
+
}, ...ngDevMode ? [{ debugName: "valueExclusionEnabled" }] : /* istanbul ignore next */ []);
|
|
1070
|
+
fieldStateSnapshot = computed(() => {
|
|
1071
|
+
const schemaFields = this.formSetup().schemaFields;
|
|
1072
|
+
if (!this.formSchemaReady() || !this.boundValueExclusionEnabled() || !schemaFields || schemaFields.length === 0) return {};
|
|
1073
|
+
const snapshot = {};
|
|
1074
|
+
collectFieldStateSnapshot(schemaFields, asFieldTreeRecord(this.form()), [], snapshot);
|
|
1075
|
+
return snapshot;
|
|
1076
|
+
}, ...ngDevMode ? [{ debugName: "fieldStateSnapshot" }] : /* istanbul ignore next */ []);
|
|
1077
|
+
excludedValueStore = signal({}, ...ngDevMode ? [{ debugName: "excludedValueStore" }] : /* istanbul ignore next */ []);
|
|
1078
|
+
prevFieldStateSnapshot = signal({}, ...ngDevMode ? [{ debugName: "prevFieldStateSnapshot" }] : /* istanbul ignore next */ []);
|
|
1079
|
+
lastOutwardValue = {
|
|
1080
|
+
current: void 0,
|
|
1081
|
+
pending: false
|
|
1082
|
+
};
|
|
1083
|
+
lastExternalValue = { current: void 0 };
|
|
1084
|
+
externalValue = computed(() => {
|
|
1085
|
+
const incoming = this.deps.value();
|
|
1086
|
+
if (this.lastOutwardValue.pending && incoming === this.lastOutwardValue.current) {
|
|
1087
|
+
this.lastOutwardValue.pending = false;
|
|
1088
|
+
return this.lastExternalValue.current;
|
|
1089
|
+
}
|
|
1090
|
+
this.lastOutwardValue.pending = false;
|
|
1091
|
+
this.lastExternalValue.current = incoming;
|
|
1092
|
+
return incoming;
|
|
1093
|
+
}, ...ngDevMode ? [{ debugName: "externalValue" }] : /* istanbul ignore next */ []);
|
|
1094
|
+
entity = linkedSignal(() => {
|
|
1095
|
+
this.externalValue();
|
|
1096
|
+
const inputValue = untracked(() => this.deps.value());
|
|
1097
|
+
const defaults = this.defaultValues();
|
|
1098
|
+
const keys = this.validKeys();
|
|
1099
|
+
const saved = this.excludedValueStore();
|
|
1100
|
+
const withSaved = deepMergeDefaults(defaults, saved);
|
|
1101
|
+
const combined = deepMergeDefaults(withSaved, inputValue);
|
|
1102
|
+
if (keys) {
|
|
1103
|
+
const filtered = {};
|
|
1104
|
+
for (const key of Object.keys(combined)) if (keys.has(key)) filtered[key] = combined[key];
|
|
1105
|
+
return filtered;
|
|
1106
|
+
}
|
|
1107
|
+
return combined;
|
|
1108
|
+
}, {
|
|
1109
|
+
debugName: "FormStateManager.entity",
|
|
1110
|
+
equal: isEqual
|
|
1111
|
+
});
|
|
1112
|
+
loadFormSchemaService = injectAsync(() => import("./ng-forge-dynamic-forms-form-schema.service-Dd-gHJ_7.mjs"));
|
|
1113
|
+
formSchemaRequest = computed(() => {
|
|
1114
|
+
const setup = this.formSetup();
|
|
1115
|
+
const config = this.activeConfig();
|
|
1116
|
+
if (!config || !(setup.schemaFields?.length > 0) && !config.schema) return void 0;
|
|
1117
|
+
return {
|
|
1118
|
+
fields: setup.schemaFields ?? [],
|
|
1119
|
+
registry: setup.registry,
|
|
1120
|
+
formLevelSchema: config.schema,
|
|
1121
|
+
validateWhenHidden: this.effectiveFormOptions().validateWhenHidden
|
|
1122
|
+
};
|
|
1123
|
+
}, ...ngDevMode ? [{ debugName: "formSchemaRequest" }] : /* istanbul ignore next */ []);
|
|
1124
|
+
formSchemaResource = resource({
|
|
1125
|
+
...ngDevMode ? { debugName: "formSchemaResource" } : /* istanbul ignore next */ {},
|
|
1126
|
+
params: () => this.formSchemaRequest(),
|
|
1127
|
+
loader: async ({ params }) => {
|
|
1128
|
+
const schemaService = await this.loadFormSchemaService();
|
|
1129
|
+
return runInInjectionContext(this.injector, () => schemaService.create(params));
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
schemaCache = { current: void 0 };
|
|
1133
|
+
trackSchemaCache = computed(() => {
|
|
1134
|
+
const request = this.formSchemaRequest();
|
|
1135
|
+
if (!request) {
|
|
1136
|
+
this.schemaCache.current = void 0;
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (this.formSchemaResource.status() === "error") {
|
|
1140
|
+
this.schemaCache.current = void 0;
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (this.formSchemaResource.hasValue()) this.schemaCache.current = {
|
|
1144
|
+
schema: this.formSchemaResource.value(),
|
|
1145
|
+
request
|
|
1146
|
+
};
|
|
1147
|
+
return this.schemaCache.current;
|
|
1148
|
+
}, ...ngDevMode ? [{ debugName: "trackSchemaCache" }] : /* istanbul ignore next */ []);
|
|
1149
|
+
isSchemaCurrent = computed(() => {
|
|
1150
|
+
if (this.formSetupConfig() !== this.activeConfig()) return false;
|
|
1151
|
+
const request = this.formSchemaRequest();
|
|
1152
|
+
if (!request) return true;
|
|
1153
|
+
return this.trackSchemaCache()?.request === request;
|
|
1154
|
+
}, ...ngDevMode ? [{ debugName: "isSchemaCurrent" }] : /* istanbul ignore next */ []);
|
|
1155
|
+
formSchemaReady = computed(() => {
|
|
1156
|
+
if (this.formSetupConfig() !== this.activeConfig()) return false;
|
|
1157
|
+
if (!this.formSchemaRequest()) return true;
|
|
1158
|
+
if (this.formSchemaResource.status() === "error") return true;
|
|
1159
|
+
return this.trackSchemaCache() !== void 0;
|
|
1160
|
+
}, ...ngDevMode ? [{ debugName: "formSchemaReady" }] : /* istanbul ignore next */ []);
|
|
1161
|
+
formSchema = computed(() => this.trackSchemaCache()?.schema, ...ngDevMode ? [{ debugName: "formSchema" }] : /* istanbul ignore next */ []);
|
|
1162
|
+
form = computed(() => {
|
|
1163
|
+
const schema = this.formSchema();
|
|
1164
|
+
const injector = this.injector;
|
|
1165
|
+
return untracked(() => schema ? form(this.entity, schema, { injector }) : form(this.entity, { injector }));
|
|
1166
|
+
}, ...ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []);
|
|
1167
|
+
isFieldPipelineSettled;
|
|
1168
|
+
fieldSignalContext = computed(() => {
|
|
1169
|
+
const formSignal = this.form;
|
|
1170
|
+
const isSettled = () => this.isFieldPipelineSettled();
|
|
1171
|
+
const formCache = this._formCache;
|
|
1172
|
+
return {
|
|
1173
|
+
injector: this.injector,
|
|
1174
|
+
value: this.deps.value,
|
|
1175
|
+
defaultValues: this.defaultValues,
|
|
1176
|
+
get form() {
|
|
1177
|
+
const currentForm = formSignal();
|
|
1178
|
+
if (!isSettled() && formCache.current) return formCache.current;
|
|
1179
|
+
formCache.current = currentForm;
|
|
1180
|
+
return currentForm;
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
}, ...ngDevMode ? [{ debugName: "fieldSignalContext" }] : /* istanbul ignore next */ []);
|
|
1184
|
+
formInstance = computed(() => this.form()(), ...ngDevMode ? [{ debugName: "formInstance" }] : /* istanbul ignore next */ []);
|
|
1185
|
+
modelValue = computed(() => this.formInstance().value(), ...ngDevMode ? [{ debugName: "modelValue" }] : /* istanbul ignore next */ []);
|
|
1186
|
+
formValue = computed(() => this.modelValue(), ...ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []);
|
|
1187
|
+
filteredFormValue = computed(() => {
|
|
1188
|
+
const rawValue = this.modelValue();
|
|
1189
|
+
const setup = this.formSetup();
|
|
1190
|
+
const schemaFields = setup.schemaFields;
|
|
1191
|
+
const options = this.effectiveFormOptions();
|
|
1192
|
+
if (!this.formSchemaReady() || !schemaFields || schemaFields.length === 0) return rawValue;
|
|
1193
|
+
const formTree = this.form();
|
|
1194
|
+
const formOptions = {
|
|
1195
|
+
excludeValueIfHidden: options.excludeValueIfHidden,
|
|
1196
|
+
excludeValueIfDisabled: options.excludeValueIfDisabled,
|
|
1197
|
+
excludeValueIfReadonly: options.excludeValueIfReadonly
|
|
1198
|
+
};
|
|
1199
|
+
return filterFormValue(rawValue, schemaFields, asFieldTreeRecord(formTree), setup.registry, this.valueExclusionDefaults, formOptions, this.valueExclusionEnabled());
|
|
1200
|
+
}, ...ngDevMode ? [{ debugName: "filteredFormValue" }] : /* istanbul ignore next */ []);
|
|
1201
|
+
boundFormValue = computed(() => {
|
|
1202
|
+
const rawValue = this.modelValue();
|
|
1203
|
+
const setup = this.formSetup();
|
|
1204
|
+
const schemaFields = setup.schemaFields;
|
|
1205
|
+
const options = this.effectiveFormOptions();
|
|
1206
|
+
if (!this.formSchemaReady() || !schemaFields || schemaFields.length === 0) return rawValue;
|
|
1207
|
+
const formTree = this.form();
|
|
1208
|
+
const formOptions = {
|
|
1209
|
+
excludeValueIfHidden: options.excludeValueIfHidden,
|
|
1210
|
+
excludeValueIfDisabled: options.excludeValueIfDisabled,
|
|
1211
|
+
excludeValueIfReadonly: options.excludeValueIfReadonly
|
|
1212
|
+
};
|
|
1213
|
+
return filterFormValue(rawValue, schemaFields, asFieldTreeRecord(formTree), setup.registry, BOUND_VALUE_EXCLUSION_BASELINE, formOptions, this.boundValueExclusionEnabled());
|
|
1214
|
+
}, ...ngDevMode ? [{ debugName: "boundFormValue" }] : /* istanbul ignore next */ []);
|
|
1215
|
+
valid = computed(() => this.formSchemaReady() && this.formSchemaResource.status() !== "error" && this.formInstance().valid(), ...ngDevMode ? [{ debugName: "valid" }] : /* istanbul ignore next */ []);
|
|
1216
|
+
invalid = computed(() => !this.formSchemaReady() || this.formSchemaResource.status() === "error" || this.formInstance().invalid(), ...ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []);
|
|
1217
|
+
dirty = computed(() => this.formSchemaReady() && this.formInstance().dirty(), ...ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []);
|
|
1218
|
+
touched = computed(() => this.formSchemaReady() && this.formInstance().touched(), ...ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []);
|
|
1219
|
+
errors = computed(() => this.formSchemaReady() ? this.formInstance().errors() : [], ...ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []);
|
|
1220
|
+
disabled = computed(() => {
|
|
1221
|
+
const optionsDisabled = this.effectiveFormOptions().disabled;
|
|
1222
|
+
const formDisabled = this.formSchemaReady() && this.formInstance().disabled();
|
|
1223
|
+
return optionsDisabled ?? formDisabled;
|
|
1224
|
+
}, ...ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []);
|
|
1225
|
+
submitting = computed(() => this.formSchemaReady() && this.formInstance().submitting(), ...ngDevMode ? [{ debugName: "submitting" }] : /* istanbul ignore next */ []);
|
|
1226
|
+
fieldsSource = computed(() => {
|
|
1227
|
+
const state = this.machine.state();
|
|
1228
|
+
if (isTransitioningState(state)) {
|
|
1229
|
+
const oldFields = state.currentConfig.fields ?? [];
|
|
1230
|
+
const newFields = state.pendingConfig.fields ?? [];
|
|
1231
|
+
const newKeys = new Set(newFields.map((f) => f.key));
|
|
1232
|
+
if (state.phase === Phase.Teardown || state.phase === Phase.Applying) return oldFields.filter((f) => newKeys.has(f.key));
|
|
1233
|
+
if (state.phase === Phase.Restoring) return state.pendingFormSetup?.fields ?? this.formSetup().fields;
|
|
1234
|
+
}
|
|
1235
|
+
return this.formSetup().fields;
|
|
1236
|
+
}, {
|
|
1237
|
+
...ngDevMode ? { debugName: "fieldsSource" } : /* istanbul ignore next */ {},
|
|
1238
|
+
equal: (a, b) => {
|
|
1239
|
+
if (a === b) return true;
|
|
1240
|
+
if (a.length !== b.length) return false;
|
|
1241
|
+
return a.every((field, i) => field === b[i]);
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
fieldInjector = computed(() => Injector.create({
|
|
1245
|
+
parent: this.injector,
|
|
1246
|
+
providers: [{
|
|
1247
|
+
provide: FIELD_SIGNAL_CONTEXT,
|
|
1248
|
+
useValue: this.fieldSignalContext()
|
|
1249
|
+
}]
|
|
1250
|
+
}), ...ngDevMode ? [{ debugName: "fieldInjector" }] : /* istanbul ignore next */ []);
|
|
1251
|
+
resolvedFields;
|
|
1252
|
+
submitted$;
|
|
1253
|
+
reset$;
|
|
1254
|
+
cleared$;
|
|
1255
|
+
get ready$() {
|
|
1256
|
+
return this.machine.state$.pipe(filter(isReadyState), take(1), map(() => void 0));
|
|
1257
|
+
}
|
|
1258
|
+
constructor() {
|
|
1259
|
+
this.resolvedFields = derivedFromDeferred(this.fieldsSource, pipe(switchMap((fields) => {
|
|
1260
|
+
if (!fields || fields.length === 0) return of([]);
|
|
1261
|
+
const registry = this.rawFieldRegistry();
|
|
1262
|
+
const injector = this.fieldInjector();
|
|
1263
|
+
const syncContext = {
|
|
1264
|
+
getLoadedComponent: (type) => this.fieldRegistry.getLoadedComponent(type),
|
|
1265
|
+
registry,
|
|
1266
|
+
injector
|
|
1267
|
+
};
|
|
1268
|
+
const results = new Array(fields.length);
|
|
1269
|
+
const asyncIndexes = [];
|
|
1270
|
+
const asyncObs = [];
|
|
1271
|
+
const onError = (fieldDef, error) => {
|
|
1272
|
+
const fieldKey = fieldDef.key || "<no key>";
|
|
1273
|
+
this.fieldLoadingErrors.update((errors) => [...errors, {
|
|
1274
|
+
fieldType: fieldDef.type,
|
|
1275
|
+
fieldKey,
|
|
1276
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
1277
|
+
}]);
|
|
1278
|
+
this.logger.error(`Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}). Ensure the field type is registered in your field registry.`, error);
|
|
1279
|
+
};
|
|
1280
|
+
for (let i = 0; i < fields.length; i++) {
|
|
1281
|
+
const f = fields[i];
|
|
1282
|
+
if (!this.fieldRegistry.getType(f.type)?.loadComponent || this.fieldRegistry.getLoadedComponent(f.type)) results[i] = resolveFieldSync(f, syncContext);
|
|
1283
|
+
else {
|
|
1284
|
+
asyncIndexes.push(i);
|
|
1285
|
+
asyncObs.push(resolveField(f, {
|
|
1286
|
+
loadTypeComponent: (type) => this.fieldRegistry.loadTypeComponent(type),
|
|
1287
|
+
registry,
|
|
1288
|
+
injector,
|
|
1289
|
+
destroyRef: this.destroyRef,
|
|
1290
|
+
onError
|
|
1291
|
+
}));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
if (asyncObs.length === 0) return of(results);
|
|
1295
|
+
return forkJoin(asyncObs).pipe(map((asyncResults) => {
|
|
1296
|
+
for (let j = 0; j < asyncResults.length; j++) results[asyncIndexes[j]] = asyncResults[j];
|
|
1297
|
+
return results;
|
|
1298
|
+
}));
|
|
1299
|
+
}), map((fields) => fields.filter((f) => f !== void 0)), scan(reconcileFields, [])), {
|
|
1300
|
+
initialValue: [],
|
|
1301
|
+
injector: this.injector
|
|
1302
|
+
});
|
|
1303
|
+
this.isFieldPipelineSettled = computed(() => {
|
|
1304
|
+
return this.fieldsSource().length === this.resolvedFields().length;
|
|
1305
|
+
}, ...ngDevMode ? [{ debugName: "isFieldPipelineSettled" }] : /* istanbul ignore next */ []);
|
|
1306
|
+
this.submitted$ = this.eventBus.on("submit").pipe(filter(() => {
|
|
1307
|
+
if (!this.valid()) {
|
|
1308
|
+
this.logger.debug("Form submitted while invalid, not emitting to (submitted) output");
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
return true;
|
|
1312
|
+
}), switchMap(() => {
|
|
1313
|
+
if ((this.activeConfig()?.submission)?.action) {
|
|
1314
|
+
this.logger.warn("Both `submission.action` and `(submitted)` output are configured. When using `submission.action`, the `(submitted)` output will not emit. Use either `submission.action` OR `(submitted)`, not both.");
|
|
1315
|
+
return EMPTY;
|
|
1316
|
+
}
|
|
1317
|
+
return of(this.filteredFormValue());
|
|
1318
|
+
}), takeUntilDestroyed(this.destroyRef));
|
|
1319
|
+
this.reset$ = this.eventBus.on("form-reset").pipe(map(() => void 0), takeUntilDestroyed(this.destroyRef));
|
|
1320
|
+
this.cleared$ = this.eventBus.on("form-clear").pipe(map(() => void 0), takeUntilDestroyed(this.destroyRef));
|
|
1321
|
+
this.eventBus.on("go-to-page").pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => this.pendingPageRequest.set({
|
|
1322
|
+
index: event.pageIndex,
|
|
1323
|
+
options: event.options
|
|
1324
|
+
}));
|
|
1325
|
+
this.machine = createFormStateMachine({
|
|
1326
|
+
injector: this.injector,
|
|
1327
|
+
destroyRef: this.destroyRef,
|
|
1328
|
+
scheduler: this.scheduler,
|
|
1329
|
+
logger: this.logger,
|
|
1330
|
+
createFormSetup: (config) => {
|
|
1331
|
+
this.registerValidatorsFromConfig(config);
|
|
1332
|
+
return this.createFormSetupFromConfig(config.fields ?? [], detectFormMode(config.fields ?? []).mode, this.rawFieldRegistry());
|
|
1333
|
+
},
|
|
1334
|
+
captureValue: () => this.formValue(),
|
|
1335
|
+
isSubmitting: this.submitting,
|
|
1336
|
+
isFieldPipelineSettled: () => this.isFieldPipelineSettled(),
|
|
1337
|
+
restoreValue: (values, validKeys) => {
|
|
1338
|
+
const filtered = {};
|
|
1339
|
+
for (const [key, val] of Object.entries(values)) if (validKeys.has(key)) filtered[key] = val;
|
|
1340
|
+
this.deps.value.update((current) => {
|
|
1341
|
+
const next = {
|
|
1342
|
+
...current,
|
|
1343
|
+
...filtered
|
|
1344
|
+
};
|
|
1345
|
+
for (const key of Object.keys(values)) if (!validKeys.has(key)) delete next[key];
|
|
1346
|
+
return next;
|
|
1347
|
+
});
|
|
1348
|
+
},
|
|
1349
|
+
onTransition: (transition) => {
|
|
1350
|
+
this.logger.debug("State transition:", transition.from.type, "→", transition.to.type, transition.action.type);
|
|
1351
|
+
},
|
|
1352
|
+
onError: (error, action) => {
|
|
1353
|
+
this.logger.error(`State machine error recovery triggered for action '${action.type}':`, error);
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
runInInjectionContext(this.injector, () => {
|
|
1357
|
+
this.setupEffects();
|
|
1358
|
+
this.setupEventHandlers();
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
updateValue(value) {
|
|
1362
|
+
this.deps.value.set(value);
|
|
1363
|
+
}
|
|
1364
|
+
reset() {
|
|
1365
|
+
const rootField = this.form()();
|
|
1366
|
+
this.excludedValueStore.set({});
|
|
1367
|
+
const defaults = this.defaultValues();
|
|
1368
|
+
rootField.value.set(defaults);
|
|
1369
|
+
this.deps.value.set(defaults);
|
|
1370
|
+
rootField.reset();
|
|
1371
|
+
}
|
|
1372
|
+
clear() {
|
|
1373
|
+
const rootField = this.form()();
|
|
1374
|
+
this.excludedValueStore.set({});
|
|
1375
|
+
const emptyValue = {};
|
|
1376
|
+
rootField.value.set(emptyValue);
|
|
1377
|
+
this.deps.value.set(emptyValue);
|
|
1378
|
+
rootField.reset();
|
|
1379
|
+
}
|
|
1380
|
+
submit() {
|
|
1381
|
+
this.eventBus.dispatch(FormSubmitEvent);
|
|
1382
|
+
}
|
|
1383
|
+
setupEffects() {
|
|
1384
|
+
explicitEffect([this.formSchemaResource.error], ([error]) => {
|
|
1385
|
+
if (error) this.logger.error("Failed to compile the form schema; rendering without validation and blocking submission.", error);
|
|
1386
|
+
});
|
|
1387
|
+
explicitEffect([this.deps.config], ([config]) => {
|
|
1388
|
+
this.fieldLoadingErrors.set([]);
|
|
1389
|
+
if (this.machine.currentState.type === LifecycleState.Uninitialized) this.machine.dispatch({
|
|
1390
|
+
type: Action.Initialize,
|
|
1391
|
+
config
|
|
1392
|
+
});
|
|
1393
|
+
else this.machine.dispatch({
|
|
1394
|
+
type: Action.ConfigChange,
|
|
1395
|
+
config
|
|
1396
|
+
});
|
|
1397
|
+
});
|
|
1398
|
+
explicitEffect([this.fieldStateSnapshot], ([snapshot]) => {
|
|
1399
|
+
const currentSaved = this.excludedValueStore();
|
|
1400
|
+
const currentEntity = this.entity();
|
|
1401
|
+
const boundValue = this.boundFormValue();
|
|
1402
|
+
const prev = this.prevFieldStateSnapshot();
|
|
1403
|
+
let newSaved = currentSaved;
|
|
1404
|
+
for (const [path, axes] of Object.entries(snapshot)) if (isFieldExcluded(axes) && !isFieldExcluded(prev[path])) {
|
|
1405
|
+
const segments = path.split(".");
|
|
1406
|
+
const inEntity = getValueAtPath(currentEntity, segments);
|
|
1407
|
+
const inBound = getValueAtPath(boundValue, segments);
|
|
1408
|
+
if (inEntity !== void 0 && inBound === void 0 && isRestoreRelevant(inEntity)) newSaved = setValueAtPath(newSaved, segments, inEntity);
|
|
1409
|
+
}
|
|
1410
|
+
this.prevFieldStateSnapshot.set({ ...snapshot });
|
|
1411
|
+
if (newSaved !== currentSaved && !isEqual(newSaved, currentSaved)) this.excludedValueStore.set(newSaved);
|
|
1412
|
+
});
|
|
1413
|
+
let warnedAboutMissingArrayRegistry = false;
|
|
1414
|
+
explicitEffect([this.formSetup], () => {
|
|
1415
|
+
this.prevFieldStateSnapshot.set({});
|
|
1416
|
+
if (Object.keys(this.excludedValueStore()).length > 0) this.excludedValueStore.set({});
|
|
1417
|
+
if (this.arrayItemRegistry) this.arrayItemRegistry.clearAll();
|
|
1418
|
+
else if (DEV_MODE && !warnedAboutMissingArrayRegistry) {
|
|
1419
|
+
warnedAboutMissingArrayRegistry = true;
|
|
1420
|
+
this.logger.warn("ArrayItemRegistryService was not provided to FormStateManager. Array fields will lose dynamically-added item templates across hide/show cycles. This usually means DynamicForm was bootstrapped without provideDynamicFormDI() — check your providers chain.");
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
explicitEffect([this.boundFormValue], ([currentBound]) => {
|
|
1424
|
+
const currentValue = this.deps.value();
|
|
1425
|
+
if (!isEqual(currentBound, currentValue)) {
|
|
1426
|
+
this.lastOutwardValue.current = currentBound;
|
|
1427
|
+
this.lastOutwardValue.pending = true;
|
|
1428
|
+
this.deps.value.set(currentBound);
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
if (DEV_MODE) explicitEffect([this.activeConfig], ([config]) => {
|
|
1432
|
+
if (!config) return;
|
|
1433
|
+
const validation = FormModeValidator.validateFormConfiguration(config.fields || []);
|
|
1434
|
+
if (!validation.isValid) this.logger.error("Invalid form configuration:", validation.errors);
|
|
1435
|
+
if (validation.warnings.length > 0) this.logger.warn("Form configuration warnings:", validation.warnings);
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
setupEventHandlers() {
|
|
1439
|
+
this.eventBus.on("form-reset").pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.reset());
|
|
1440
|
+
this.eventBus.on("form-clear").pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.clear());
|
|
1441
|
+
}
|
|
1442
|
+
registerValidatorsFromConfig({ customFnConfig, schemas }) {
|
|
1443
|
+
if (schemas) schemas.forEach((schema) => {
|
|
1444
|
+
this.schemaRegistry.registerSchema(schema);
|
|
1445
|
+
});
|
|
1446
|
+
if (!customFnConfig) return;
|
|
1447
|
+
if (customFnConfig.customFunctions) Object.entries(customFnConfig.customFunctions).forEach(([name, fn]) => {
|
|
1448
|
+
this.functionRegistry.registerCustomFunction(name, fn);
|
|
1449
|
+
});
|
|
1450
|
+
if (customFnConfig.derivations) this.functionRegistry.setDerivationFunctions(customFnConfig.derivations);
|
|
1451
|
+
if (customFnConfig.asyncDerivations) this.functionRegistry.setAsyncDerivationFunctions(customFnConfig.asyncDerivations);
|
|
1452
|
+
if (customFnConfig.asyncConditions) this.functionRegistry.setAsyncConditionFunctions(customFnConfig.asyncConditions);
|
|
1453
|
+
this.functionRegistry.setValidators(customFnConfig.validators);
|
|
1454
|
+
this.functionRegistry.setAsyncValidators(customFnConfig.asyncValidators);
|
|
1455
|
+
this.functionRegistry.setHttpValidators(customFnConfig.httpValidators);
|
|
1456
|
+
}
|
|
1457
|
+
createFormSetupFromConfig(fields, mode, registry) {
|
|
1458
|
+
const normalizedFields = normalizeSimplifiedArrays(fields);
|
|
1459
|
+
validateFormConfig(normalizedFields, registry, this.logger);
|
|
1460
|
+
const source = this.deps.source?.() ?? "inline";
|
|
1461
|
+
const { fields: sanitizedFields, warnings: addonWarnings } = walkAndValidateAddons(normalizedFields, registry, this.addonKindRegistry, source);
|
|
1462
|
+
if (DEV_MODE) {
|
|
1463
|
+
const nextKeys = /* @__PURE__ */ new Set();
|
|
1464
|
+
for (const w of addonWarnings) {
|
|
1465
|
+
const key = addonWarningKey(w);
|
|
1466
|
+
nextKeys.add(key);
|
|
1467
|
+
if (!this.lastAddonWarningKeys.has(key)) this.logger.warn(formatAddonWarning(w));
|
|
1468
|
+
}
|
|
1469
|
+
this.lastAddonWarningKeys = nextKeys;
|
|
1470
|
+
}
|
|
1471
|
+
const validatedFields = sanitizedFields;
|
|
1472
|
+
const flattenedFields = this.fieldProcessors.memoizedFlattenFields(validatedFields, registry);
|
|
1473
|
+
const flattenedFieldsForRendering = this.fieldProcessors.memoizedFlattenFieldsForRendering(validatedFields, registry);
|
|
1474
|
+
const fieldsById = this.fieldProcessors.memoizedKeyBy(flattenedFields);
|
|
1475
|
+
const defaultValues = this.fieldProcessors.memoizedDefaultValues(fieldsById, registry);
|
|
1476
|
+
return {
|
|
1477
|
+
fields: mode === "paged" ? [] : flattenedFieldsForRendering,
|
|
1478
|
+
schemaFields: flattenedFields,
|
|
1479
|
+
originalFields: fields,
|
|
1480
|
+
defaultValues,
|
|
1481
|
+
mode,
|
|
1482
|
+
registry
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
createEmptyFormSetup(registry) {
|
|
1486
|
+
return {
|
|
1487
|
+
fields: [],
|
|
1488
|
+
schemaFields: [],
|
|
1489
|
+
defaultValues: {},
|
|
1490
|
+
mode: "non-paged",
|
|
1491
|
+
registry
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
ngOnDestroy() {
|
|
1495
|
+
this.machine.dispatch({ type: Action.Destroy });
|
|
1496
|
+
}
|
|
1497
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1498
|
+
minVersion: "12.0.0",
|
|
1499
|
+
version: "22.2.5",
|
|
1500
|
+
ngImport: i0,
|
|
1501
|
+
type: FormStateManager,
|
|
1502
|
+
deps: [],
|
|
1503
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1504
|
+
});
|
|
1505
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1506
|
+
minVersion: "12.0.0",
|
|
1507
|
+
version: "22.2.5",
|
|
1508
|
+
ngImport: i0,
|
|
1509
|
+
type: FormStateManager
|
|
1510
|
+
});
|
|
1511
|
+
};
|
|
1512
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1513
|
+
minVersion: "12.0.0",
|
|
1514
|
+
version: "22.2.5",
|
|
1515
|
+
ngImport: i0,
|
|
1516
|
+
type: FormStateManager,
|
|
1517
|
+
decorators: [{ type: Injectable }],
|
|
1518
|
+
ctorParameters: () => []
|
|
1519
|
+
});
|
|
1520
|
+
export { walkAndValidateAddons as a, formatAddonWarning as i, FormStateManager as n, normalizeSimplifiedArrays as o, VALUE_EXCLUSION_DEFAULTS as r, FormSubmitEvent as s, FORM_STATE_DEPS as t };
|
|
1521
|
+
|
|
1522
|
+
//# sourceMappingURL=ng-forge-dynamic-forms-form-state-manager-DcOXRFRL.mjs.map
|