@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/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;
|
|
@@ -2249,7 +2320,7 @@ function renderElement(element, ctx) {
|
|
|
2249
2320
|
return renderElementFunc(element, ctx);
|
|
2250
2321
|
}
|
|
2251
2322
|
function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
2252
|
-
var _a;
|
|
2323
|
+
var _a, _b;
|
|
2253
2324
|
const containerWrap = document.createElement("div");
|
|
2254
2325
|
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
2255
2326
|
containerWrap.setAttribute("data-container", pathKey);
|
|
@@ -2264,6 +2335,9 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
2264
2335
|
const subCtx = {
|
|
2265
2336
|
path: pathJoin(ctx.path, element.key),
|
|
2266
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
|
|
2267
2341
|
state: ctx.state
|
|
2268
2342
|
};
|
|
2269
2343
|
element.elements.forEach((child) => {
|
|
@@ -2276,7 +2350,7 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
2276
2350
|
wrapper.appendChild(containerWrap);
|
|
2277
2351
|
}
|
|
2278
2352
|
function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
2279
|
-
var _a, _b, _c;
|
|
2353
|
+
var _a, _b, _c, _d;
|
|
2280
2354
|
const state = ctx.state;
|
|
2281
2355
|
const containerWrap = document.createElement("div");
|
|
2282
2356
|
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
@@ -2303,12 +2377,15 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2303
2377
|
add.className = "px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors";
|
|
2304
2378
|
add.textContent = t("addElement", state);
|
|
2305
2379
|
add.onclick = () => {
|
|
2380
|
+
var _a2;
|
|
2306
2381
|
if (countItems() < max) {
|
|
2307
2382
|
const idx = countItems();
|
|
2308
2383
|
const subCtx = {
|
|
2309
2384
|
state: ctx.state,
|
|
2310
2385
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2311
|
-
prefill: {}
|
|
2386
|
+
prefill: {},
|
|
2387
|
+
formData: (_a2 = ctx.formData) != null ? _a2 : ctx.prefill
|
|
2388
|
+
// Complete root data for displayIf
|
|
2312
2389
|
};
|
|
2313
2390
|
const item = document.createElement("div");
|
|
2314
2391
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2350,10 +2427,13 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2350
2427
|
}
|
|
2351
2428
|
if (pre && Array.isArray(pre)) {
|
|
2352
2429
|
pre.forEach((prefillObj, idx) => {
|
|
2430
|
+
var _a2;
|
|
2353
2431
|
const subCtx = {
|
|
2354
2432
|
state: ctx.state,
|
|
2355
2433
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2356
|
-
prefill: prefillObj || {}
|
|
2434
|
+
prefill: prefillObj || {},
|
|
2435
|
+
formData: (_a2 = ctx.formData) != null ? _a2 : ctx.prefill
|
|
2436
|
+
// Complete root data for displayIf
|
|
2357
2437
|
};
|
|
2358
2438
|
const item = document.createElement("div");
|
|
2359
2439
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2384,7 +2464,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2384
2464
|
const subCtx = {
|
|
2385
2465
|
state: ctx.state,
|
|
2386
2466
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2387
|
-
prefill: {}
|
|
2467
|
+
prefill: {},
|
|
2468
|
+
formData: (_d = ctx.formData) != null ? _d : ctx.prefill
|
|
2469
|
+
// Complete root data for displayIf
|
|
2388
2470
|
};
|
|
2389
2471
|
const item = document.createElement("div");
|
|
2390
2472
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2668,8 +2750,32 @@ if (typeof document !== "undefined") {
|
|
|
2668
2750
|
});
|
|
2669
2751
|
}
|
|
2670
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
|
+
}
|
|
2671
2776
|
const wrapper = document.createElement("div");
|
|
2672
2777
|
wrapper.className = "mb-6 fb-field-wrapper";
|
|
2778
|
+
wrapper.setAttribute("data-field-key", element.key);
|
|
2673
2779
|
const label = document.createElement("div");
|
|
2674
2780
|
label.className = "flex items-center mb-2";
|
|
2675
2781
|
const title = document.createElement("label");
|
|
@@ -3162,6 +3268,7 @@ var FormBuilderInstance = class {
|
|
|
3162
3268
|
}
|
|
3163
3269
|
this.state.debounceTimer = setTimeout(() => {
|
|
3164
3270
|
const formData = this.validateForm(true);
|
|
3271
|
+
this.reevaluateConditionalFields();
|
|
3165
3272
|
if (this.state.config.onChange) {
|
|
3166
3273
|
this.state.config.onChange(formData);
|
|
3167
3274
|
}
|
|
@@ -3421,6 +3528,8 @@ var FormBuilderInstance = class {
|
|
|
3421
3528
|
const block = renderElement2(element, {
|
|
3422
3529
|
path: "",
|
|
3423
3530
|
prefill: prefill || {},
|
|
3531
|
+
formData: prefill || {},
|
|
3532
|
+
// Pass complete root data for displayIf evaluation
|
|
3424
3533
|
state: this.state,
|
|
3425
3534
|
instance: this
|
|
3426
3535
|
});
|
|
@@ -3465,6 +3574,19 @@ var FormBuilderInstance = class {
|
|
|
3465
3574
|
};
|
|
3466
3575
|
setValidateElement(validateElement2);
|
|
3467
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
|
+
}
|
|
3468
3590
|
if (element.hidden) {
|
|
3469
3591
|
data[element.key] = element.default !== void 0 ? element.default : null;
|
|
3470
3592
|
} else {
|
|
@@ -3621,6 +3743,71 @@ var FormBuilderInstance = class {
|
|
|
3621
3743
|
);
|
|
3622
3744
|
}
|
|
3623
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
|
+
}
|
|
3624
3811
|
/**
|
|
3625
3812
|
* Destroy instance and clean up resources
|
|
3626
3813
|
*/
|