@dmitryvim/form-builder 0.2.5 → 0.2.6
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/README.md +121 -2
- package/dist/browser/formbuilder.min.js +31 -31
- package/dist/browser/formbuilder.v0.2.6.min.js +184 -0
- package/dist/cjs/index.cjs +195 -8
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +189 -6
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +31 -31
- package/dist/types/instance/FormBuilderInstance.d.ts +5 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/schema.d.ts +11 -1
- package/dist/types/utils/display-conditions.d.ts +17 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.5.min.js +0 -184
package/dist/esm/index.js
CHANGED
|
@@ -56,9 +56,6 @@ function validateSchema(schema) {
|
|
|
56
56
|
errors.push("Schema must be an object");
|
|
57
57
|
return errors;
|
|
58
58
|
}
|
|
59
|
-
if (!schema.version) {
|
|
60
|
-
errors.push("Schema missing version");
|
|
61
|
-
}
|
|
62
59
|
if (!Array.isArray(schema.elements)) {
|
|
63
60
|
errors.push("Schema missing elements array");
|
|
64
61
|
return errors;
|
|
@@ -72,6 +69,20 @@ function validateSchema(schema) {
|
|
|
72
69
|
if (!element.key) {
|
|
73
70
|
errors.push(`${elementPath}: missing key`);
|
|
74
71
|
}
|
|
72
|
+
if (element.displayIf) {
|
|
73
|
+
const displayIf = element.displayIf;
|
|
74
|
+
if (!displayIf.key || typeof displayIf.key !== "string") {
|
|
75
|
+
errors.push(
|
|
76
|
+
`${elementPath}: displayIf must have a 'key' property of type string`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const hasOperator = "equals" in displayIf;
|
|
80
|
+
if (!hasOperator) {
|
|
81
|
+
errors.push(
|
|
82
|
+
`${elementPath}: displayIf must have at least one operator (equals, etc.)`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
75
86
|
if (element.type === "group" && "elements" in element && element.elements) {
|
|
76
87
|
validateElements(element.elements, `${elementPath}.elements`);
|
|
77
88
|
}
|
|
@@ -109,6 +120,66 @@ function clear(node) {
|
|
|
109
120
|
while (node.firstChild) node.removeChild(node.firstChild);
|
|
110
121
|
}
|
|
111
122
|
|
|
123
|
+
// src/utils/display-conditions.ts
|
|
124
|
+
function getValueByPath(data, path) {
|
|
125
|
+
if (!data || typeof data !== "object") {
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
const segments = path.match(/[^.[\]]+|\[\d+\]/g);
|
|
129
|
+
if (!segments || segments.length === 0) {
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
let current = data;
|
|
133
|
+
for (const segment of segments) {
|
|
134
|
+
if (current === void 0 || current === null) {
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
138
|
+
const index = parseInt(segment.slice(1, -1), 10);
|
|
139
|
+
if (!Array.isArray(current) || isNaN(index)) {
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
current = current[index];
|
|
143
|
+
} else {
|
|
144
|
+
current = current[segment];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return current;
|
|
148
|
+
}
|
|
149
|
+
function evaluateDisplayCondition(condition, formData) {
|
|
150
|
+
if (!condition || !condition.key) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
"Invalid displayIf condition: must have a 'key' property"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const actualValue = getValueByPath(formData, condition.key);
|
|
156
|
+
if ("equals" in condition) {
|
|
157
|
+
return deepEqual(actualValue, condition.equals);
|
|
158
|
+
}
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Invalid displayIf condition: no recognized operator (equals, etc.)`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
function deepEqual(a, b) {
|
|
164
|
+
if (a === b) return true;
|
|
165
|
+
if (a == null || b == null) return a === b;
|
|
166
|
+
if (typeof a !== typeof b) return false;
|
|
167
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
if (e instanceof TypeError && (e.message.includes("circular") || e.message.includes("cyclic"))) {
|
|
172
|
+
console.warn(
|
|
173
|
+
"deepEqual: Circular reference detected in displayIf comparison, using reference equality"
|
|
174
|
+
);
|
|
175
|
+
return a === b;
|
|
176
|
+
}
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return a === b;
|
|
181
|
+
}
|
|
182
|
+
|
|
112
183
|
// src/components/text.ts
|
|
113
184
|
function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
114
185
|
const state = ctx.state;
|
|
@@ -2225,6 +2296,9 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
2225
2296
|
const subCtx = {
|
|
2226
2297
|
path: pathJoin(ctx.path, element.key),
|
|
2227
2298
|
prefill: ctx.prefill?.[element.key] || {},
|
|
2299
|
+
// Sliced data for value population
|
|
2300
|
+
formData: ctx.formData ?? ctx.prefill,
|
|
2301
|
+
// Complete root data for displayIf evaluation
|
|
2228
2302
|
state: ctx.state
|
|
2229
2303
|
};
|
|
2230
2304
|
element.elements.forEach((child) => {
|
|
@@ -2268,7 +2342,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2268
2342
|
const subCtx = {
|
|
2269
2343
|
state: ctx.state,
|
|
2270
2344
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2271
|
-
prefill: {}
|
|
2345
|
+
prefill: {},
|
|
2346
|
+
formData: ctx.formData ?? ctx.prefill
|
|
2347
|
+
// Complete root data for displayIf
|
|
2272
2348
|
};
|
|
2273
2349
|
const item = document.createElement("div");
|
|
2274
2350
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2313,7 +2389,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2313
2389
|
const subCtx = {
|
|
2314
2390
|
state: ctx.state,
|
|
2315
2391
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2316
|
-
prefill: prefillObj || {}
|
|
2392
|
+
prefill: prefillObj || {},
|
|
2393
|
+
formData: ctx.formData ?? ctx.prefill
|
|
2394
|
+
// Complete root data for displayIf
|
|
2317
2395
|
};
|
|
2318
2396
|
const item = document.createElement("div");
|
|
2319
2397
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2344,7 +2422,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2344
2422
|
const subCtx = {
|
|
2345
2423
|
state: ctx.state,
|
|
2346
2424
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2347
|
-
prefill: {}
|
|
2425
|
+
prefill: {},
|
|
2426
|
+
formData: ctx.formData ?? ctx.prefill
|
|
2427
|
+
// Complete root data for displayIf
|
|
2348
2428
|
};
|
|
2349
2429
|
const item = document.createElement("div");
|
|
2350
2430
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2625,8 +2705,31 @@ if (typeof document !== "undefined") {
|
|
|
2625
2705
|
});
|
|
2626
2706
|
}
|
|
2627
2707
|
function renderElement2(element, ctx) {
|
|
2708
|
+
if (element.displayIf) {
|
|
2709
|
+
try {
|
|
2710
|
+
const dataForCondition = ctx.formData ?? ctx.prefill;
|
|
2711
|
+
const shouldDisplay = evaluateDisplayCondition(
|
|
2712
|
+
element.displayIf,
|
|
2713
|
+
dataForCondition
|
|
2714
|
+
);
|
|
2715
|
+
if (!shouldDisplay) {
|
|
2716
|
+
const hiddenWrapper = document.createElement("div");
|
|
2717
|
+
hiddenWrapper.className = "fb-field-wrapper-hidden";
|
|
2718
|
+
hiddenWrapper.style.display = "none";
|
|
2719
|
+
hiddenWrapper.setAttribute("data-field-key", element.key);
|
|
2720
|
+
hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
|
|
2721
|
+
return hiddenWrapper;
|
|
2722
|
+
}
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
console.error(
|
|
2725
|
+
`Error evaluating displayIf for field "${element.key}":`,
|
|
2726
|
+
error
|
|
2727
|
+
);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2628
2730
|
const wrapper = document.createElement("div");
|
|
2629
2731
|
wrapper.className = "mb-6 fb-field-wrapper";
|
|
2732
|
+
wrapper.setAttribute("data-field-key", element.key);
|
|
2630
2733
|
const label = document.createElement("div");
|
|
2631
2734
|
label.className = "flex items-center mb-2";
|
|
2632
2735
|
const title = document.createElement("label");
|
|
@@ -3119,6 +3222,7 @@ var FormBuilderInstance = class {
|
|
|
3119
3222
|
}
|
|
3120
3223
|
this.state.debounceTimer = setTimeout(() => {
|
|
3121
3224
|
const formData = this.validateForm(true);
|
|
3225
|
+
this.reevaluateConditionalFields();
|
|
3122
3226
|
if (this.state.config.onChange) {
|
|
3123
3227
|
this.state.config.onChange(formData);
|
|
3124
3228
|
}
|
|
@@ -3378,6 +3482,8 @@ var FormBuilderInstance = class {
|
|
|
3378
3482
|
const block = renderElement2(element, {
|
|
3379
3483
|
path: "",
|
|
3380
3484
|
prefill: prefill || {},
|
|
3485
|
+
formData: prefill || {},
|
|
3486
|
+
// Pass complete root data for displayIf evaluation
|
|
3381
3487
|
state: this.state,
|
|
3382
3488
|
instance: this
|
|
3383
3489
|
});
|
|
@@ -3422,6 +3528,19 @@ var FormBuilderInstance = class {
|
|
|
3422
3528
|
};
|
|
3423
3529
|
setValidateElement(validateElement2);
|
|
3424
3530
|
this.state.schema.elements.forEach((element) => {
|
|
3531
|
+
if (element.displayIf) {
|
|
3532
|
+
try {
|
|
3533
|
+
const shouldDisplay = evaluateDisplayCondition(element.displayIf, data);
|
|
3534
|
+
if (!shouldDisplay) {
|
|
3535
|
+
return;
|
|
3536
|
+
}
|
|
3537
|
+
} catch (error) {
|
|
3538
|
+
console.error(
|
|
3539
|
+
`Error evaluating displayIf for field "${element.key}" during validation:`,
|
|
3540
|
+
error
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3425
3544
|
if (element.hidden) {
|
|
3426
3545
|
data[element.key] = element.default !== void 0 ? element.default : null;
|
|
3427
3546
|
} else {
|
|
@@ -3578,6 +3697,70 @@ var FormBuilderInstance = class {
|
|
|
3578
3697
|
);
|
|
3579
3698
|
}
|
|
3580
3699
|
}
|
|
3700
|
+
/**
|
|
3701
|
+
* Re-evaluate all conditional fields (displayIf) based on current form data
|
|
3702
|
+
* This is called automatically when form data changes (via onChange events)
|
|
3703
|
+
*/
|
|
3704
|
+
reevaluateConditionalFields() {
|
|
3705
|
+
if (!this.state.schema || !this.state.formRoot) return;
|
|
3706
|
+
const formData = this.validateForm(true).data;
|
|
3707
|
+
const checkElements = (elements, currentPath) => {
|
|
3708
|
+
elements.forEach((element) => {
|
|
3709
|
+
const fullPath = currentPath ? `${currentPath}.${element.key}` : element.key;
|
|
3710
|
+
if (element.displayIf) {
|
|
3711
|
+
const fieldWrappers = this.state.formRoot.querySelectorAll(
|
|
3712
|
+
`[data-field-key="${element.key}"]`
|
|
3713
|
+
);
|
|
3714
|
+
fieldWrappers.forEach((wrapper) => {
|
|
3715
|
+
try {
|
|
3716
|
+
const shouldDisplay = evaluateDisplayCondition(
|
|
3717
|
+
element.displayIf,
|
|
3718
|
+
formData
|
|
3719
|
+
// Use complete formData for condition evaluation
|
|
3720
|
+
);
|
|
3721
|
+
const isCurrentlyHidden = wrapper.getAttribute("data-conditionally-hidden") === "true";
|
|
3722
|
+
if (shouldDisplay && isCurrentlyHidden) {
|
|
3723
|
+
const newWrapper = renderElement2(element, {
|
|
3724
|
+
path: fullPath,
|
|
3725
|
+
// Use accumulated path
|
|
3726
|
+
prefill: formData,
|
|
3727
|
+
// Use complete formData for root-level elements
|
|
3728
|
+
formData,
|
|
3729
|
+
// Pass complete formData for displayIf evaluation
|
|
3730
|
+
state: this.state,
|
|
3731
|
+
instance: this
|
|
3732
|
+
});
|
|
3733
|
+
wrapper.parentNode?.replaceChild(newWrapper, wrapper);
|
|
3734
|
+
} else if (!shouldDisplay && !isCurrentlyHidden) {
|
|
3735
|
+
const hiddenWrapper = document.createElement("div");
|
|
3736
|
+
hiddenWrapper.className = "fb-field-wrapper-hidden";
|
|
3737
|
+
hiddenWrapper.style.display = "none";
|
|
3738
|
+
hiddenWrapper.setAttribute("data-field-key", element.key);
|
|
3739
|
+
hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
|
|
3740
|
+
wrapper.parentNode?.replaceChild(hiddenWrapper, wrapper);
|
|
3741
|
+
}
|
|
3742
|
+
} catch (error) {
|
|
3743
|
+
console.error(
|
|
3744
|
+
`Error re-evaluating displayIf for field "${element.key}":`,
|
|
3745
|
+
error
|
|
3746
|
+
);
|
|
3747
|
+
}
|
|
3748
|
+
});
|
|
3749
|
+
}
|
|
3750
|
+
if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
|
|
3751
|
+
const containerData = formData?.[element.key];
|
|
3752
|
+
if (Array.isArray(containerData)) {
|
|
3753
|
+
containerData.forEach((_, index) => {
|
|
3754
|
+
checkElements(element.elements, `${fullPath}[${index}]`);
|
|
3755
|
+
});
|
|
3756
|
+
} else {
|
|
3757
|
+
checkElements(element.elements, fullPath);
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
});
|
|
3761
|
+
};
|
|
3762
|
+
checkElements(this.state.schema.elements, "");
|
|
3763
|
+
}
|
|
3581
3764
|
/**
|
|
3582
3765
|
* Destroy instance and clean up resources
|
|
3583
3766
|
*/
|