@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/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
  */