@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/cjs/index.cjs
CHANGED
|
@@ -62,9 +62,6 @@ function validateSchema(schema) {
|
|
|
62
62
|
errors.push("Schema must be an object");
|
|
63
63
|
return errors;
|
|
64
64
|
}
|
|
65
|
-
if (!schema.version) {
|
|
66
|
-
errors.push("Schema missing version");
|
|
67
|
-
}
|
|
68
65
|
if (!Array.isArray(schema.elements)) {
|
|
69
66
|
errors.push("Schema missing elements array");
|
|
70
67
|
return errors;
|
|
@@ -78,6 +75,20 @@ function validateSchema(schema) {
|
|
|
78
75
|
if (!element.key) {
|
|
79
76
|
errors.push(`${elementPath}: missing key`);
|
|
80
77
|
}
|
|
78
|
+
if (element.displayIf) {
|
|
79
|
+
const displayIf = element.displayIf;
|
|
80
|
+
if (!displayIf.key || typeof displayIf.key !== "string") {
|
|
81
|
+
errors.push(
|
|
82
|
+
`${elementPath}: displayIf must have a 'key' property of type string`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const hasOperator = "equals" in displayIf;
|
|
86
|
+
if (!hasOperator) {
|
|
87
|
+
errors.push(
|
|
88
|
+
`${elementPath}: displayIf must have at least one operator (equals, etc.)`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
81
92
|
if (element.type === "group" && "elements" in element && element.elements) {
|
|
82
93
|
validateElements(element.elements, `${elementPath}.elements`);
|
|
83
94
|
}
|
|
@@ -115,6 +126,66 @@ function clear(node) {
|
|
|
115
126
|
while (node.firstChild) node.removeChild(node.firstChild);
|
|
116
127
|
}
|
|
117
128
|
|
|
129
|
+
// src/utils/display-conditions.ts
|
|
130
|
+
function getValueByPath(data, path) {
|
|
131
|
+
if (!data || typeof data !== "object") {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
const segments = path.match(/[^.[\]]+|\[\d+\]/g);
|
|
135
|
+
if (!segments || segments.length === 0) {
|
|
136
|
+
return void 0;
|
|
137
|
+
}
|
|
138
|
+
let current = data;
|
|
139
|
+
for (const segment of segments) {
|
|
140
|
+
if (current === void 0 || current === null) {
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
144
|
+
const index = parseInt(segment.slice(1, -1), 10);
|
|
145
|
+
if (!Array.isArray(current) || isNaN(index)) {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
current = current[index];
|
|
149
|
+
} else {
|
|
150
|
+
current = current[segment];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return current;
|
|
154
|
+
}
|
|
155
|
+
function evaluateDisplayCondition(condition, formData) {
|
|
156
|
+
if (!condition || !condition.key) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"Invalid displayIf condition: must have a 'key' property"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const actualValue = getValueByPath(formData, condition.key);
|
|
162
|
+
if ("equals" in condition) {
|
|
163
|
+
return deepEqual(actualValue, condition.equals);
|
|
164
|
+
}
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Invalid displayIf condition: no recognized operator (equals, etc.)`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
function deepEqual(a, b) {
|
|
170
|
+
if (a === b) return true;
|
|
171
|
+
if (a == null || b == null) return a === b;
|
|
172
|
+
if (typeof a !== typeof b) return false;
|
|
173
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
174
|
+
try {
|
|
175
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
if (e instanceof TypeError && (e.message.includes("circular") || e.message.includes("cyclic"))) {
|
|
178
|
+
console.warn(
|
|
179
|
+
"deepEqual: Circular reference detected in displayIf comparison, using reference equality"
|
|
180
|
+
);
|
|
181
|
+
return a === b;
|
|
182
|
+
}
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return a === b;
|
|
187
|
+
}
|
|
188
|
+
|
|
118
189
|
// src/components/text.ts
|
|
119
190
|
function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
120
191
|
const state = ctx.state;
|
|
@@ -1289,8 +1360,22 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
|
|
|
1289
1360
|
const thumbnailUrl = await state.config.getThumbnail(resourceId);
|
|
1290
1361
|
if (thumbnailUrl) {
|
|
1291
1362
|
clear(container);
|
|
1292
|
-
|
|
1293
|
-
|
|
1363
|
+
if (meta && meta.type && meta.type.startsWith("video/")) {
|
|
1364
|
+
const video = document.createElement("video");
|
|
1365
|
+
video.className = "w-full h-full object-contain";
|
|
1366
|
+
video.controls = true;
|
|
1367
|
+
video.preload = "metadata";
|
|
1368
|
+
video.muted = true;
|
|
1369
|
+
const source = document.createElement("source");
|
|
1370
|
+
source.src = thumbnailUrl;
|
|
1371
|
+
source.type = meta.type;
|
|
1372
|
+
video.appendChild(source);
|
|
1373
|
+
video.appendChild(document.createTextNode("Your browser does not support the video tag."));
|
|
1374
|
+
container.appendChild(video);
|
|
1375
|
+
} else {
|
|
1376
|
+
img.src = thumbnailUrl;
|
|
1377
|
+
container.appendChild(img);
|
|
1378
|
+
}
|
|
1294
1379
|
} else {
|
|
1295
1380
|
setEmptyFileContainer(container, state);
|
|
1296
1381
|
}
|
|
@@ -2235,7 +2320,7 @@ function renderElement(element, ctx) {
|
|
|
2235
2320
|
return renderElementFunc(element, ctx);
|
|
2236
2321
|
}
|
|
2237
2322
|
function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
2238
|
-
var _a;
|
|
2323
|
+
var _a, _b;
|
|
2239
2324
|
const containerWrap = document.createElement("div");
|
|
2240
2325
|
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
2241
2326
|
containerWrap.setAttribute("data-container", pathKey);
|
|
@@ -2250,6 +2335,9 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
2250
2335
|
const subCtx = {
|
|
2251
2336
|
path: pathJoin(ctx.path, element.key),
|
|
2252
2337
|
prefill: ((_a = ctx.prefill) == null ? void 0 : _a[element.key]) || {},
|
|
2338
|
+
// Sliced data for value population
|
|
2339
|
+
formData: (_b = ctx.formData) != null ? _b : ctx.prefill,
|
|
2340
|
+
// Complete root data for displayIf evaluation
|
|
2253
2341
|
state: ctx.state
|
|
2254
2342
|
};
|
|
2255
2343
|
element.elements.forEach((child) => {
|
|
@@ -2262,7 +2350,7 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
2262
2350
|
wrapper.appendChild(containerWrap);
|
|
2263
2351
|
}
|
|
2264
2352
|
function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
2265
|
-
var _a, _b, _c;
|
|
2353
|
+
var _a, _b, _c, _d;
|
|
2266
2354
|
const state = ctx.state;
|
|
2267
2355
|
const containerWrap = document.createElement("div");
|
|
2268
2356
|
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
@@ -2289,12 +2377,15 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2289
2377
|
add.className = "px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors";
|
|
2290
2378
|
add.textContent = t("addElement", state);
|
|
2291
2379
|
add.onclick = () => {
|
|
2380
|
+
var _a2;
|
|
2292
2381
|
if (countItems() < max) {
|
|
2293
2382
|
const idx = countItems();
|
|
2294
2383
|
const subCtx = {
|
|
2295
2384
|
state: ctx.state,
|
|
2296
2385
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2297
|
-
prefill: {}
|
|
2386
|
+
prefill: {},
|
|
2387
|
+
formData: (_a2 = ctx.formData) != null ? _a2 : ctx.prefill
|
|
2388
|
+
// Complete root data for displayIf
|
|
2298
2389
|
};
|
|
2299
2390
|
const item = document.createElement("div");
|
|
2300
2391
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2336,10 +2427,13 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2336
2427
|
}
|
|
2337
2428
|
if (pre && Array.isArray(pre)) {
|
|
2338
2429
|
pre.forEach((prefillObj, idx) => {
|
|
2430
|
+
var _a2;
|
|
2339
2431
|
const subCtx = {
|
|
2340
2432
|
state: ctx.state,
|
|
2341
2433
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2342
|
-
prefill: prefillObj || {}
|
|
2434
|
+
prefill: prefillObj || {},
|
|
2435
|
+
formData: (_a2 = ctx.formData) != null ? _a2 : ctx.prefill
|
|
2436
|
+
// Complete root data for displayIf
|
|
2343
2437
|
};
|
|
2344
2438
|
const item = document.createElement("div");
|
|
2345
2439
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2370,7 +2464,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2370
2464
|
const subCtx = {
|
|
2371
2465
|
state: ctx.state,
|
|
2372
2466
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2373
|
-
prefill: {}
|
|
2467
|
+
prefill: {},
|
|
2468
|
+
formData: (_d = ctx.formData) != null ? _d : ctx.prefill
|
|
2469
|
+
// Complete root data for displayIf
|
|
2374
2470
|
};
|
|
2375
2471
|
const item = document.createElement("div");
|
|
2376
2472
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2654,8 +2750,32 @@ if (typeof document !== "undefined") {
|
|
|
2654
2750
|
});
|
|
2655
2751
|
}
|
|
2656
2752
|
function renderElement2(element, ctx) {
|
|
2753
|
+
var _a;
|
|
2754
|
+
if (element.displayIf) {
|
|
2755
|
+
try {
|
|
2756
|
+
const dataForCondition = (_a = ctx.formData) != null ? _a : ctx.prefill;
|
|
2757
|
+
const shouldDisplay = evaluateDisplayCondition(
|
|
2758
|
+
element.displayIf,
|
|
2759
|
+
dataForCondition
|
|
2760
|
+
);
|
|
2761
|
+
if (!shouldDisplay) {
|
|
2762
|
+
const hiddenWrapper = document.createElement("div");
|
|
2763
|
+
hiddenWrapper.className = "fb-field-wrapper-hidden";
|
|
2764
|
+
hiddenWrapper.style.display = "none";
|
|
2765
|
+
hiddenWrapper.setAttribute("data-field-key", element.key);
|
|
2766
|
+
hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
|
|
2767
|
+
return hiddenWrapper;
|
|
2768
|
+
}
|
|
2769
|
+
} catch (error) {
|
|
2770
|
+
console.error(
|
|
2771
|
+
`Error evaluating displayIf for field "${element.key}":`,
|
|
2772
|
+
error
|
|
2773
|
+
);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2657
2776
|
const wrapper = document.createElement("div");
|
|
2658
2777
|
wrapper.className = "mb-6 fb-field-wrapper";
|
|
2778
|
+
wrapper.setAttribute("data-field-key", element.key);
|
|
2659
2779
|
const label = document.createElement("div");
|
|
2660
2780
|
label.className = "flex items-center mb-2";
|
|
2661
2781
|
const title = document.createElement("label");
|
|
@@ -3148,6 +3268,7 @@ var FormBuilderInstance = class {
|
|
|
3148
3268
|
}
|
|
3149
3269
|
this.state.debounceTimer = setTimeout(() => {
|
|
3150
3270
|
const formData = this.validateForm(true);
|
|
3271
|
+
this.reevaluateConditionalFields();
|
|
3151
3272
|
if (this.state.config.onChange) {
|
|
3152
3273
|
this.state.config.onChange(formData);
|
|
3153
3274
|
}
|
|
@@ -3407,6 +3528,8 @@ var FormBuilderInstance = class {
|
|
|
3407
3528
|
const block = renderElement2(element, {
|
|
3408
3529
|
path: "",
|
|
3409
3530
|
prefill: prefill || {},
|
|
3531
|
+
formData: prefill || {},
|
|
3532
|
+
// Pass complete root data for displayIf evaluation
|
|
3410
3533
|
state: this.state,
|
|
3411
3534
|
instance: this
|
|
3412
3535
|
});
|
|
@@ -3451,6 +3574,19 @@ var FormBuilderInstance = class {
|
|
|
3451
3574
|
};
|
|
3452
3575
|
setValidateElement(validateElement2);
|
|
3453
3576
|
this.state.schema.elements.forEach((element) => {
|
|
3577
|
+
if (element.displayIf) {
|
|
3578
|
+
try {
|
|
3579
|
+
const shouldDisplay = evaluateDisplayCondition(element.displayIf, data);
|
|
3580
|
+
if (!shouldDisplay) {
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
} catch (error) {
|
|
3584
|
+
console.error(
|
|
3585
|
+
`Error evaluating displayIf for field "${element.key}" during validation:`,
|
|
3586
|
+
error
|
|
3587
|
+
);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3454
3590
|
if (element.hidden) {
|
|
3455
3591
|
data[element.key] = element.default !== void 0 ? element.default : null;
|
|
3456
3592
|
} else {
|
|
@@ -3607,6 +3743,71 @@ var FormBuilderInstance = class {
|
|
|
3607
3743
|
);
|
|
3608
3744
|
}
|
|
3609
3745
|
}
|
|
3746
|
+
/**
|
|
3747
|
+
* Re-evaluate all conditional fields (displayIf) based on current form data
|
|
3748
|
+
* This is called automatically when form data changes (via onChange events)
|
|
3749
|
+
*/
|
|
3750
|
+
reevaluateConditionalFields() {
|
|
3751
|
+
if (!this.state.schema || !this.state.formRoot) return;
|
|
3752
|
+
const formData = this.validateForm(true).data;
|
|
3753
|
+
const checkElements = (elements, currentPath) => {
|
|
3754
|
+
elements.forEach((element) => {
|
|
3755
|
+
const fullPath = currentPath ? `${currentPath}.${element.key}` : element.key;
|
|
3756
|
+
if (element.displayIf) {
|
|
3757
|
+
const fieldWrappers = this.state.formRoot.querySelectorAll(
|
|
3758
|
+
`[data-field-key="${element.key}"]`
|
|
3759
|
+
);
|
|
3760
|
+
fieldWrappers.forEach((wrapper) => {
|
|
3761
|
+
var _a, _b;
|
|
3762
|
+
try {
|
|
3763
|
+
const shouldDisplay = evaluateDisplayCondition(
|
|
3764
|
+
element.displayIf,
|
|
3765
|
+
formData
|
|
3766
|
+
// Use complete formData for condition evaluation
|
|
3767
|
+
);
|
|
3768
|
+
const isCurrentlyHidden = wrapper.getAttribute("data-conditionally-hidden") === "true";
|
|
3769
|
+
if (shouldDisplay && isCurrentlyHidden) {
|
|
3770
|
+
const newWrapper = renderElement2(element, {
|
|
3771
|
+
path: fullPath,
|
|
3772
|
+
// Use accumulated path
|
|
3773
|
+
prefill: formData,
|
|
3774
|
+
// Use complete formData for root-level elements
|
|
3775
|
+
formData,
|
|
3776
|
+
// Pass complete formData for displayIf evaluation
|
|
3777
|
+
state: this.state,
|
|
3778
|
+
instance: this
|
|
3779
|
+
});
|
|
3780
|
+
(_a = wrapper.parentNode) == null ? void 0 : _a.replaceChild(newWrapper, wrapper);
|
|
3781
|
+
} else if (!shouldDisplay && !isCurrentlyHidden) {
|
|
3782
|
+
const hiddenWrapper = document.createElement("div");
|
|
3783
|
+
hiddenWrapper.className = "fb-field-wrapper-hidden";
|
|
3784
|
+
hiddenWrapper.style.display = "none";
|
|
3785
|
+
hiddenWrapper.setAttribute("data-field-key", element.key);
|
|
3786
|
+
hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
|
|
3787
|
+
(_b = wrapper.parentNode) == null ? void 0 : _b.replaceChild(hiddenWrapper, wrapper);
|
|
3788
|
+
}
|
|
3789
|
+
} catch (error) {
|
|
3790
|
+
console.error(
|
|
3791
|
+
`Error re-evaluating displayIf for field "${element.key}":`,
|
|
3792
|
+
error
|
|
3793
|
+
);
|
|
3794
|
+
}
|
|
3795
|
+
});
|
|
3796
|
+
}
|
|
3797
|
+
if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
|
|
3798
|
+
const containerData = formData == null ? void 0 : formData[element.key];
|
|
3799
|
+
if (Array.isArray(containerData)) {
|
|
3800
|
+
containerData.forEach((_, index) => {
|
|
3801
|
+
checkElements(element.elements, `${fullPath}[${index}]`);
|
|
3802
|
+
});
|
|
3803
|
+
} else {
|
|
3804
|
+
checkElements(element.elements, fullPath);
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
});
|
|
3808
|
+
};
|
|
3809
|
+
checkElements(this.state.schema.elements, "");
|
|
3810
|
+
}
|
|
3610
3811
|
/**
|
|
3611
3812
|
* Destroy instance and clean up resources
|
|
3612
3813
|
*/
|