@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/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
- img.src = thumbnailUrl;
1269
- container.appendChild(img);
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
  */