@dmitryvim/form-builder 0.2.4 → 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 +38 -38
- package/dist/browser/formbuilder.v0.2.6.min.js +184 -0
- package/dist/cjs/index.cjs +211 -10
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +205 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +38 -38
- 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.4.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;
|
|
@@ -1265,8 +1336,22 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
|
|
|
1265
1336
|
const thumbnailUrl = await state.config.getThumbnail(resourceId);
|
|
1266
1337
|
if (thumbnailUrl) {
|
|
1267
1338
|
clear(container);
|
|
1268
|
-
|
|
1269
|
-
|
|
1339
|
+
if (meta && meta.type && meta.type.startsWith("video/")) {
|
|
1340
|
+
const video = document.createElement("video");
|
|
1341
|
+
video.className = "w-full h-full object-contain";
|
|
1342
|
+
video.controls = true;
|
|
1343
|
+
video.preload = "metadata";
|
|
1344
|
+
video.muted = true;
|
|
1345
|
+
const source = document.createElement("source");
|
|
1346
|
+
source.src = thumbnailUrl;
|
|
1347
|
+
source.type = meta.type;
|
|
1348
|
+
video.appendChild(source);
|
|
1349
|
+
video.appendChild(document.createTextNode("Your browser does not support the video tag."));
|
|
1350
|
+
container.appendChild(video);
|
|
1351
|
+
} else {
|
|
1352
|
+
img.src = thumbnailUrl;
|
|
1353
|
+
container.appendChild(img);
|
|
1354
|
+
}
|
|
1270
1355
|
} else {
|
|
1271
1356
|
setEmptyFileContainer(container, state);
|
|
1272
1357
|
}
|
|
@@ -2211,6 +2296,9 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
2211
2296
|
const subCtx = {
|
|
2212
2297
|
path: pathJoin(ctx.path, element.key),
|
|
2213
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
|
|
2214
2302
|
state: ctx.state
|
|
2215
2303
|
};
|
|
2216
2304
|
element.elements.forEach((child) => {
|
|
@@ -2254,7 +2342,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2254
2342
|
const subCtx = {
|
|
2255
2343
|
state: ctx.state,
|
|
2256
2344
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2257
|
-
prefill: {}
|
|
2345
|
+
prefill: {},
|
|
2346
|
+
formData: ctx.formData ?? ctx.prefill
|
|
2347
|
+
// Complete root data for displayIf
|
|
2258
2348
|
};
|
|
2259
2349
|
const item = document.createElement("div");
|
|
2260
2350
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2299,7 +2389,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2299
2389
|
const subCtx = {
|
|
2300
2390
|
state: ctx.state,
|
|
2301
2391
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2302
|
-
prefill: prefillObj || {}
|
|
2392
|
+
prefill: prefillObj || {},
|
|
2393
|
+
formData: ctx.formData ?? ctx.prefill
|
|
2394
|
+
// Complete root data for displayIf
|
|
2303
2395
|
};
|
|
2304
2396
|
const item = document.createElement("div");
|
|
2305
2397
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2330,7 +2422,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2330
2422
|
const subCtx = {
|
|
2331
2423
|
state: ctx.state,
|
|
2332
2424
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2333
|
-
prefill: {}
|
|
2425
|
+
prefill: {},
|
|
2426
|
+
formData: ctx.formData ?? ctx.prefill
|
|
2427
|
+
// Complete root data for displayIf
|
|
2334
2428
|
};
|
|
2335
2429
|
const item = document.createElement("div");
|
|
2336
2430
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2611,8 +2705,31 @@ if (typeof document !== "undefined") {
|
|
|
2611
2705
|
});
|
|
2612
2706
|
}
|
|
2613
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
|
+
}
|
|
2614
2730
|
const wrapper = document.createElement("div");
|
|
2615
2731
|
wrapper.className = "mb-6 fb-field-wrapper";
|
|
2732
|
+
wrapper.setAttribute("data-field-key", element.key);
|
|
2616
2733
|
const label = document.createElement("div");
|
|
2617
2734
|
label.className = "flex items-center mb-2";
|
|
2618
2735
|
const title = document.createElement("label");
|
|
@@ -3105,6 +3222,7 @@ var FormBuilderInstance = class {
|
|
|
3105
3222
|
}
|
|
3106
3223
|
this.state.debounceTimer = setTimeout(() => {
|
|
3107
3224
|
const formData = this.validateForm(true);
|
|
3225
|
+
this.reevaluateConditionalFields();
|
|
3108
3226
|
if (this.state.config.onChange) {
|
|
3109
3227
|
this.state.config.onChange(formData);
|
|
3110
3228
|
}
|
|
@@ -3364,6 +3482,8 @@ var FormBuilderInstance = class {
|
|
|
3364
3482
|
const block = renderElement2(element, {
|
|
3365
3483
|
path: "",
|
|
3366
3484
|
prefill: prefill || {},
|
|
3485
|
+
formData: prefill || {},
|
|
3486
|
+
// Pass complete root data for displayIf evaluation
|
|
3367
3487
|
state: this.state,
|
|
3368
3488
|
instance: this
|
|
3369
3489
|
});
|
|
@@ -3408,6 +3528,19 @@ var FormBuilderInstance = class {
|
|
|
3408
3528
|
};
|
|
3409
3529
|
setValidateElement(validateElement2);
|
|
3410
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
|
+
}
|
|
3411
3544
|
if (element.hidden) {
|
|
3412
3545
|
data[element.key] = element.default !== void 0 ? element.default : null;
|
|
3413
3546
|
} else {
|
|
@@ -3564,6 +3697,70 @@ var FormBuilderInstance = class {
|
|
|
3564
3697
|
);
|
|
3565
3698
|
}
|
|
3566
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
|
+
}
|
|
3567
3764
|
/**
|
|
3568
3765
|
* Destroy instance and clean up resources
|
|
3569
3766
|
*/
|