@dmitryvim/form-builder 0.1.35 → 0.1.38

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 CHANGED
@@ -105,9 +105,9 @@ npm install @dmitryvim/form-builder
105
105
  "extensions": ["jpg", "png", "webp"]
106
106
  },
107
107
  "actions": [
108
- { "value": "enhance", "label": "Enhance Quality" },
109
- { "value": "crop", "label": "Auto Crop" },
110
- { "value": "retry", "label": "Try Again" }
108
+ { "key": "enhance", "label": "Enhance Quality" },
109
+ { "key": "crop", "label": "Auto Crop" },
110
+ { "key": "retry", "label": "Try Again" }
111
111
  ]
112
112
  },
113
113
  {
@@ -152,9 +152,14 @@ FormBuilder.setUploadHandler(async (file) => {
152
152
  // Your upload logic - return resource ID
153
153
  return "resource-123";
154
154
  });
155
- FormBuilder.setActionHandler((value) => {
155
+ FormBuilder.setActionHandler((value, key, relatedField) => {
156
156
  // Handle action button clicks
157
- console.log("Action clicked:", value);
157
+ console.log("Action clicked:", { value, key, relatedField });
158
+ if (relatedField) {
159
+ // Field-level action
160
+ } else {
161
+ // Form-level action
162
+ }
158
163
  });
159
164
  FormBuilder.renderForm(schema, prefillData);
160
165
  ```
package/dist/demo.js CHANGED
@@ -19,9 +19,9 @@ const EXAMPLE_SCHEMA = {
19
19
  },
20
20
  maxSizeMB: 10,
21
21
  actions: [
22
- { value: "cover1.retry", label: "А давай ещё разок" },
23
- { value: "cover1.enhance", label: "Улучшить качество" },
24
- { value: "cover1.crop", label: "Обрезать изображение" },
22
+ { key: "retry", label: "А давай ещё разок" },
23
+ { key: "enhance", label: "Улучшить качество" },
24
+ { key: "crop", label: "Обрезать изображение" },
25
25
  ],
26
26
  },
27
27
  {
@@ -113,7 +113,7 @@ const EXAMPLE_SCHEMA = {
113
113
  maxCount: 5,
114
114
  minLength: 2,
115
115
  maxLength: 20,
116
- default: "popular"
116
+ default: "popular",
117
117
  },
118
118
  {
119
119
  type: "textarea",
@@ -127,13 +127,14 @@ const EXAMPLE_SCHEMA = {
127
127
  maxCount: 4,
128
128
  minLength: 10,
129
129
  maxLength: 200,
130
- rows: 3
130
+ rows: 3,
131
131
  },
132
132
  {
133
133
  type: "number",
134
134
  key: "dimensions",
135
135
  label: "Размеры (см)",
136
- description: "Укажите размеры товара в сантиметрах: длина, ширина, высота",
136
+ description:
137
+ "Укажите размеры товара в сантиметрах: длина, ширина, высота",
137
138
  placeholder: "0",
138
139
  required: false,
139
140
  multiple: true,
@@ -142,7 +143,7 @@ const EXAMPLE_SCHEMA = {
142
143
  min: 0,
143
144
  max: 500,
144
145
  step: 0.1,
145
- decimals: 1
146
+ decimals: 1,
146
147
  },
147
148
  {
148
149
  type: "select",
@@ -159,9 +160,9 @@ const EXAMPLE_SCHEMA = {
159
160
  { value: "red", label: "Красный" },
160
161
  { value: "blue", label: "Синий" },
161
162
  { value: "green", label: "Зеленый" },
162
- { value: "gray", label: "Серый" }
163
+ { value: "gray", label: "Серый" },
163
164
  ],
164
- default: "white"
165
+ default: "white",
165
166
  },
166
167
  {
167
168
  type: "file",
@@ -214,7 +215,7 @@ const EXAMPLE_SCHEMA = {
214
215
  label: "Session ID",
215
216
  description: "Hidden session identifier for tracking purposes",
216
217
  hidden: true,
217
- default: "session_" + Math.random().toString(36).substr(2, 9),
218
+ default: `session_${Math.random().toString(36).substr(2, 9)}`,
218
219
  },
219
220
  ],
220
221
  };
@@ -322,38 +323,6 @@ class InMemoryFileStorage {
322
323
  // Initialize file storage
323
324
  const fileStorage = new InMemoryFileStorage();
324
325
 
325
- // Cache for action value -> label mapping for efficient lookup
326
- let actionLabelMap = new Map();
327
-
328
- // Build action value -> label mapping from schema for efficient lookup
329
- function buildActionLabelMap(schema) {
330
- const map = new Map();
331
-
332
- function processElements(elements) {
333
- if (!Array.isArray(elements)) return;
334
-
335
- for (const element of elements) {
336
- if (element.actions && Array.isArray(element.actions)) {
337
- for (const action of element.actions) {
338
- if (action.value && action.label) {
339
- map.set(action.value, action.label);
340
- }
341
- }
342
- }
343
-
344
- // Process nested group elements
345
- if (element.elements && Array.isArray(element.elements)) {
346
- processElements(element.elements);
347
- }
348
- }
349
- }
350
-
351
- if (schema && schema.elements) {
352
- processElements(schema.elements);
353
- }
354
-
355
- return map;
356
- }
357
326
 
358
327
  // DOM element references
359
328
  const el = {
@@ -372,6 +341,10 @@ const el = {
372
341
  copyDataBtn: document.getElementById("copyDataBtn"),
373
342
  downloadDataBtn: document.getElementById("downloadDataBtn"),
374
343
  dataErrors: document.getElementById("dataErrors"),
344
+ actionsTextarea: document.getElementById("actionsTextarea"),
345
+ formatActionsBtn: document.getElementById("formatActionsBtn"),
346
+ clearActionsBtn: document.getElementById("clearActionsBtn"),
347
+ actionsErrors: document.getElementById("actionsErrors"),
375
348
  };
376
349
 
377
350
  // Utility functions
@@ -405,6 +378,49 @@ function downloadFile(filename, content) {
405
378
  URL.revokeObjectURL(url);
406
379
  }
407
380
 
381
+ // Parse and validate external actions
382
+ function parseActions(actionsText) {
383
+ if (!actionsText || actionsText.trim() === "") {
384
+ return [];
385
+ }
386
+
387
+ try {
388
+ const actions = JSON.parse(actionsText);
389
+
390
+ if (!Array.isArray(actions)) {
391
+ throw new Error("Actions must be an array");
392
+ }
393
+
394
+ // Validate each action
395
+ for (let i = 0; i < actions.length; i++) {
396
+ const action = actions[i];
397
+ if (!action || typeof action !== "object") {
398
+ throw new Error(`Action at index ${i} must be an object`);
399
+ }
400
+ if (!action.key || typeof action.key !== "string") {
401
+ throw new Error(
402
+ `Action at index ${i} missing valid 'key' property`,
403
+ );
404
+ }
405
+ if (!action.value || typeof action.value !== "string") {
406
+ throw new Error(`Action at index ${i} missing valid 'value' property`);
407
+ }
408
+ // related_field is optional - if not provided, action is form-level
409
+ if (action.related_field && typeof action.related_field !== "string") {
410
+ throw new Error(`Action at index ${i} has invalid 'related_field' property type`);
411
+ }
412
+ // Label is optional - will be resolved from schema or key
413
+ if (action.label && typeof action.label !== "string") {
414
+ throw new Error(`Action at index ${i} has invalid 'label' property type`);
415
+ }
416
+ }
417
+
418
+ return actions;
419
+ } catch (error) {
420
+ throw new Error(`Actions JSON error: ${error.message}`);
421
+ }
422
+ }
423
+
408
424
  // Configure FormBuilder with in-memory handlers
409
425
  function setupFormBuilder() {
410
426
  // Set form container
@@ -437,17 +453,29 @@ function setupFormBuilder() {
437
453
  });
438
454
 
439
455
  // Action handler - display message when action button is clicked
440
- FormBuilder.setActionHandler((value) => {
441
- // Use cached action map for O(1) lookup instead of re-parsing schema
442
- const actionLabel = actionLabelMap.get(value) || value; // fallback to value
456
+ // New system: value, key, related_field parameters
457
+ FormBuilder.setActionHandler((value, key, relatedField) => {
458
+ console.log("Action clicked:", {
459
+ value,
460
+ key,
461
+ relatedField,
462
+ type: relatedField ? "field-level" : "form-level",
463
+ });
443
464
 
444
- console.log("Action clicked:", { label: actionLabel, value });
445
-
446
465
  // Show message to user (compatible with all environments)
447
466
  if (typeof window !== "undefined" && window.alert) {
448
- window.alert(`${actionLabel} clicked: ${value}`);
467
+ if (relatedField) {
468
+ window.alert(
469
+ `Field Action: "${key}" clicked for field "${relatedField}" with value: ${value}`,
470
+ );
471
+ } else {
472
+ window.alert(`Form Action: "${key}" clicked with value: ${value}`);
473
+ }
449
474
  } else {
450
- console.log(`Demo action: ${actionLabel} clicked: ${value}`);
475
+ console.log(
476
+ `Demo action: ${key} clicked with value: ${value}`,
477
+ relatedField ? ` for field: ${relatedField}` : " (form-level)",
478
+ );
451
479
  }
452
480
  });
453
481
 
@@ -460,6 +488,7 @@ function setupFormBuilder() {
460
488
  function applyCurrentSchema() {
461
489
  clearError(el.schemaErrors);
462
490
  clearError(el.formErrors);
491
+ clearError(el.actionsErrors);
463
492
 
464
493
  try {
465
494
  const schema = JSON.parse(el.schemaInput.value);
@@ -473,8 +502,15 @@ function applyCurrentSchema() {
473
502
  return false;
474
503
  }
475
504
 
476
- // Build action value -> label map for efficient lookup
477
- actionLabelMap = buildActionLabelMap(schema);
505
+ // Parse external actions
506
+ let externalActions = [];
507
+ try {
508
+ externalActions = parseActions(el.actionsTextarea.value);
509
+ } catch (error) {
510
+ showError(el.actionsErrors, error.message);
511
+ return false;
512
+ }
513
+
478
514
 
479
515
  // Set mode based on toggle
480
516
  const isReadOnly = el.readOnlyToggle.checked;
@@ -491,10 +527,12 @@ function applyCurrentSchema() {
491
527
  // Ignore errors when getting current data
492
528
  }
493
529
 
494
- // Render form with current data
495
- FormBuilder.renderForm(schema, currentData);
530
+ // Render form with current data and external actions
531
+ FormBuilder.renderForm(schema, currentData, externalActions);
496
532
 
497
533
  console.log(`Form rendered in ${isReadOnly ? "readonly" : "edit"} mode`);
534
+ console.log(`External actions:`, externalActions);
535
+
498
536
  return true;
499
537
  } catch (e) {
500
538
  showError(el.schemaErrors, `JSON parse error: ${e.message}`);
@@ -512,7 +550,9 @@ el.resetSchemaBtn.addEventListener("click", () => {
512
550
  clearError(el.schemaErrors);
513
551
  clearError(el.formErrors);
514
552
  clearError(el.dataErrors);
553
+ clearError(el.actionsErrors);
515
554
  el.dataTextarea.value = "";
555
+ el.actionsTextarea.value = "";
516
556
 
517
557
  // Clear file storage
518
558
  fileStorage.clear();
@@ -617,6 +657,15 @@ el.prefillBtn.addEventListener("click", () => {
617
657
  const prefillData = JSON.parse(el.dataTextarea.value || "{}");
618
658
  const currentSchema = JSON.parse(el.schemaInput.value);
619
659
 
660
+ // Parse external actions
661
+ let externalActions = [];
662
+ try {
663
+ externalActions = parseActions(el.actionsTextarea.value);
664
+ } catch (error) {
665
+ showError(el.actionsErrors, error.message);
666
+ return;
667
+ }
668
+
620
669
  // Convert enhanced data back to raw format for prefilling
621
670
  const processedData = JSON.parse(JSON.stringify(prefillData));
622
671
 
@@ -657,9 +706,10 @@ el.prefillBtn.addEventListener("click", () => {
657
706
  const isReadOnly = el.readOnlyToggle.checked;
658
707
  FormBuilder.setMode(isReadOnly ? "readonly" : "edit");
659
708
 
660
- FormBuilder.renderForm(currentSchema, processedData);
709
+ FormBuilder.renderForm(currentSchema, processedData, externalActions);
661
710
  console.log("Form prefilled with data");
662
711
  console.log("Processed prefill data:", processedData);
712
+ console.log("External actions:", externalActions);
663
713
  } catch (e) {
664
714
  showError(el.dataErrors, `JSON parse error: ${e.message}`);
665
715
  console.error("Prefill error:", e);
@@ -684,6 +734,81 @@ el.downloadDataBtn.addEventListener("click", () => {
684
734
  console.log("Data downloaded");
685
735
  });
686
736
 
737
+ // Actions management handlers
738
+ el.formatActionsBtn.addEventListener("click", () => {
739
+ try {
740
+ const actions = parseActions(el.actionsTextarea.value);
741
+ el.actionsTextarea.value = pretty(actions);
742
+ clearError(el.actionsErrors);
743
+ console.log("Actions formatted");
744
+ } catch (e) {
745
+ showError(el.actionsErrors, e.message);
746
+ console.error("Format actions error:", e);
747
+ }
748
+ });
749
+
750
+ el.clearActionsBtn.addEventListener("click", () => {
751
+ el.actionsTextarea.value = "";
752
+ clearError(el.actionsErrors);
753
+ // Re-apply schema to remove external actions from the form
754
+ applyCurrentSchema();
755
+ console.log("Actions cleared");
756
+ });
757
+
758
+ // Example external actions for demonstration
759
+ const EXAMPLE_ACTIONS = [
760
+ // Field-level actions using predefined labels from schema
761
+ {
762
+ related_field: "cover",
763
+ key: "retry",
764
+ value: "regenerate_cover_image", // Specific action value for handler
765
+ },
766
+ {
767
+ related_field: "cover",
768
+ key: "enhance",
769
+ value: "enhance_cover_quality", // Specific action value for handler
770
+ },
771
+ {
772
+ related_field: "cover",
773
+ key: "crop",
774
+ value: "auto_crop_cover", // Specific action value for handler
775
+ },
776
+ // Field-level actions with custom labels
777
+ {
778
+ related_field: "title[0]",
779
+ key: "generate",
780
+ value: "ai_generate_title",
781
+ label: "🤖 Generate Title", // Custom label overrides schema
782
+ },
783
+ {
784
+ related_field: "description",
785
+ key: "improve",
786
+ value: "ai_improve_description", // Key will be used as fallback label
787
+ },
788
+ {
789
+ related_field: "slides[0].title",
790
+ key: "optimize",
791
+ value: "ai_optimize_slide_title",
792
+ label: "🎯 Optimize Slide Title", // Custom label
793
+ },
794
+ // Form-level actions (no related_field)
795
+ {
796
+ key: "save-draft",
797
+ value: "save_form_draft",
798
+ label: "💾 Save Draft",
799
+ },
800
+ {
801
+ key: "preview",
802
+ value: "preview_infographic",
803
+ label: "👁️ Preview",
804
+ },
805
+ {
806
+ key: "export",
807
+ value: "export_json_data",
808
+ label: "📄 Export JSON",
809
+ },
810
+ ];
811
+
687
812
  // Initialize demo application
688
813
  function initDemo() {
689
814
  // Set up FormBuilder
@@ -692,6 +817,9 @@ function initDemo() {
692
817
  // Initialize with example schema
693
818
  el.schemaInput.value = pretty(EXAMPLE_SCHEMA);
694
819
 
820
+ // Initialize with example actions
821
+ el.actionsTextarea.value = pretty(EXAMPLE_ACTIONS);
822
+
695
823
  // Apply initial schema
696
824
  applyCurrentSchema();
697
825