@dmitryvim/form-builder 0.1.5 → 0.1.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/README.md +120 -29
- package/dist/example.html +40 -40
- package/dist/form-builder.js +91 -23
- package/dist/index.html +1 -1
- package/docs/schema.md +10 -8
- package/package.json +1 -1
package/dist/README.md
CHANGED
|
@@ -64,19 +64,40 @@ Renders a form in the specified container.
|
|
|
64
64
|
**Parameters:**
|
|
65
65
|
- `schema` - JSON schema object (v0.3)
|
|
66
66
|
- `container` - DOM element to render form in
|
|
67
|
-
- `options` - Configuration object
|
|
67
|
+
- `options` - Configuration object (optional)
|
|
68
68
|
|
|
69
69
|
**Options:**
|
|
70
70
|
- `prefill` - Object with prefilled values
|
|
71
|
-
- `readonly` - Boolean, renders form in read-only mode
|
|
72
|
-
- `
|
|
73
|
-
- `
|
|
74
|
-
- `
|
|
71
|
+
- `readonly` - Boolean, renders form in read-only mode (hides buttons)
|
|
72
|
+
- `debug` - Boolean, enables debug logging to console
|
|
73
|
+
- `onSubmit` - Callback for form submission `(data) => {}`
|
|
74
|
+
- `onDraft` - Callback for draft saving `(data) => {}`
|
|
75
|
+
- `onError` - Callback for validation errors `(errors) => {}`
|
|
76
|
+
- `buttons` - Custom button text: `{ submit: "Start Workflow", draft: "Save Progress" }`
|
|
77
|
+
|
|
78
|
+
**Important:** Upload handlers must return a **string** resourceId, not an object.
|
|
75
79
|
|
|
76
80
|
### FormBuilder.validateSchema(schema)
|
|
77
81
|
|
|
78
82
|
Validates a JSON schema and returns array of errors.
|
|
79
83
|
|
|
84
|
+
**Returns:** `string[]` - Array of error messages (empty if valid)
|
|
85
|
+
|
|
86
|
+
### FormBuilder.collectAndValidate(schema, formElement, skipValidation)
|
|
87
|
+
|
|
88
|
+
⚠️ **Direct API for advanced users only**
|
|
89
|
+
|
|
90
|
+
Collects form data and validates it. Most users should use `onSubmit`/`onDraft` callbacks instead.
|
|
91
|
+
|
|
92
|
+
**Parameters:**
|
|
93
|
+
- `schema` - The form schema
|
|
94
|
+
- `formElement` - The form DOM element
|
|
95
|
+
- `skipValidation` - Boolean, skip validation (for drafts)
|
|
96
|
+
|
|
97
|
+
**Returns:** `{ result: object, errors: string[] }`
|
|
98
|
+
|
|
99
|
+
**Important:** This is the correct destructuring format - there is no `.valid` or `.data` property.
|
|
100
|
+
|
|
80
101
|
### FormBuilder.setUploadHandler(uploadFn)
|
|
81
102
|
|
|
82
103
|
Sets custom file upload handler:
|
|
@@ -109,47 +130,117 @@ FormBuilder.setDownloadHandler(async (resourceId, fileName) => {
|
|
|
109
130
|
- `select` - Dropdown selection
|
|
110
131
|
- `file` - Single file upload
|
|
111
132
|
- `files` - Multiple file upload
|
|
112
|
-
- `group` - Nested objects with optional repeat
|
|
133
|
+
- `group` - Nested objects with optional repeat and custom element titles
|
|
134
|
+
|
|
135
|
+
## Troubleshooting Common Issues
|
|
136
|
+
|
|
137
|
+
### ❌ Upload Handler Returns Object
|
|
138
|
+
```javascript
|
|
139
|
+
// Wrong - causes "resourceId.slice is not a function"
|
|
140
|
+
FormBuilder.setUploadHandler(async (file) => {
|
|
141
|
+
const response = await uploadFile(file);
|
|
142
|
+
return { reference: response.id, name: file.name }; // ❌ Object
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Correct - return string only
|
|
146
|
+
FormBuilder.setUploadHandler(async (file) => {
|
|
147
|
+
const response = await uploadFile(file);
|
|
148
|
+
return response.id; // ✅ String resourceId
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### ❌ Wrong API Usage
|
|
153
|
+
```javascript
|
|
154
|
+
// Wrong - no .valid or .data properties
|
|
155
|
+
const result = FormBuilder.collectAndValidate(schema, form);
|
|
156
|
+
if (result.valid) { // ❌ Property doesn't exist
|
|
157
|
+
console.log(result.data); // ❌ Property doesn't exist
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Correct - use destructuring
|
|
161
|
+
const { result, errors } = FormBuilder.collectAndValidate(schema, form);
|
|
162
|
+
if (errors.length === 0) { // ✅ Check errors array
|
|
163
|
+
console.log(result); // ✅ Data is in result
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### ❌ Group Elements Missing Elements Array
|
|
168
|
+
```javascript
|
|
169
|
+
// Wrong - causes "group.elements must be array"
|
|
170
|
+
{
|
|
171
|
+
type: 'group',
|
|
172
|
+
key: 'address',
|
|
173
|
+
// Missing elements array ❌
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Correct - always include elements
|
|
177
|
+
{
|
|
178
|
+
type: 'group',
|
|
179
|
+
key: 'address',
|
|
180
|
+
label: 'Address Information',
|
|
181
|
+
element_label: 'Address #$index', // ✅ Optional custom title
|
|
182
|
+
elements: [ // ✅ Required array
|
|
183
|
+
{ type: 'text', key: 'street', label: 'Street' }
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### ❌ Readonly Mode Confusion
|
|
189
|
+
```javascript
|
|
190
|
+
// This only hides the default buttons, fields remain editable
|
|
191
|
+
FormBuilder.renderForm(schema, container, { readonly: true });
|
|
192
|
+
|
|
193
|
+
// For true read-only, you need to disable inputs manually or use a different approach
|
|
194
|
+
```
|
|
113
195
|
|
|
114
196
|
## Complete Example
|
|
115
197
|
|
|
116
198
|
```javascript
|
|
117
199
|
const schema = {
|
|
118
200
|
"version": "0.3",
|
|
119
|
-
"title": "
|
|
201
|
+
"title": "Video Cover Generation",
|
|
120
202
|
"elements": [
|
|
121
|
-
{
|
|
122
|
-
"type": "file",
|
|
123
|
-
"key": "cover",
|
|
124
|
-
"label": "Cover Image",
|
|
125
|
-
"required": true,
|
|
126
|
-
"accept": {
|
|
127
|
-
"extensions": ["jpg", "png"],
|
|
128
|
-
"mime": ["image/jpeg", "image/png"]
|
|
129
|
-
},
|
|
130
|
-
"maxSizeMB": 5
|
|
131
|
-
},
|
|
132
203
|
{
|
|
133
204
|
"type": "text",
|
|
134
|
-
"key": "
|
|
135
|
-
"label": "
|
|
136
|
-
"required":
|
|
137
|
-
"maxLength": 100
|
|
205
|
+
"key": "concept",
|
|
206
|
+
"label": "Animation Concept (Optional)",
|
|
207
|
+
"required": false,
|
|
208
|
+
"maxLength": 100,
|
|
209
|
+
"placeholder": "e.g., fun, elegant, dynamic, cozy..."
|
|
138
210
|
},
|
|
139
211
|
{
|
|
140
212
|
"type": "group",
|
|
141
|
-
"key": "
|
|
142
|
-
"label": "
|
|
213
|
+
"key": "slides_input",
|
|
214
|
+
"label": "Video Slides",
|
|
215
|
+
"element_label": "Slide $index", // 🆕 Custom title for each item
|
|
143
216
|
"repeat": {
|
|
144
217
|
"min": 1,
|
|
145
|
-
"max":
|
|
218
|
+
"max": 10
|
|
146
219
|
},
|
|
147
220
|
"elements": [
|
|
148
221
|
{
|
|
149
|
-
"type": "
|
|
150
|
-
"key": "
|
|
151
|
-
"label": "
|
|
152
|
-
"required": true
|
|
222
|
+
"type": "file",
|
|
223
|
+
"key": "main_image",
|
|
224
|
+
"label": "Main Image",
|
|
225
|
+
"required": true,
|
|
226
|
+
"accept": {
|
|
227
|
+
"extensions": ["png", "jpg", "jpeg", "webp"],
|
|
228
|
+
"mime": ["image/png", "image/jpeg", "image/webp"]
|
|
229
|
+
},
|
|
230
|
+
"maxSizeMB": 25
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"type": "files",
|
|
234
|
+
"key": "elements",
|
|
235
|
+
"label": "Element Images (Optional)",
|
|
236
|
+
"required": false,
|
|
237
|
+
"accept": {
|
|
238
|
+
"extensions": ["png", "jpg", "jpeg", "webp"],
|
|
239
|
+
"mime": ["image/png", "image/jpeg", "image/webp"]
|
|
240
|
+
},
|
|
241
|
+
"minCount": 0,
|
|
242
|
+
"maxCount": 10,
|
|
243
|
+
"maxSizeMB": 10
|
|
153
244
|
}
|
|
154
245
|
]
|
|
155
246
|
}
|
package/dist/example.html
CHANGED
|
@@ -23,54 +23,54 @@
|
|
|
23
23
|
|
|
24
24
|
<script src="form-builder.js"></script>
|
|
25
25
|
<script>
|
|
26
|
-
// Example schema
|
|
26
|
+
// Example schema demonstrating element_label feature
|
|
27
27
|
const schema = {
|
|
28
28
|
"version": "0.3",
|
|
29
|
-
"title": "
|
|
29
|
+
"title": "Video Cover Generation",
|
|
30
30
|
"elements": [
|
|
31
31
|
{
|
|
32
32
|
"type": "text",
|
|
33
|
-
"key": "
|
|
34
|
-
"label": "
|
|
35
|
-
"required":
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"type": "text",
|
|
41
|
-
"key": "email",
|
|
42
|
-
"label": "Email Address",
|
|
43
|
-
"required": true,
|
|
44
|
-
"pattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"type": "select",
|
|
48
|
-
"key": "category",
|
|
49
|
-
"label": "Category",
|
|
50
|
-
"required": true,
|
|
51
|
-
"options": [
|
|
52
|
-
{ "value": "general", "label": "General Inquiry" },
|
|
53
|
-
{ "value": "support", "label": "Technical Support" },
|
|
54
|
-
{ "value": "billing", "label": "Billing Question" }
|
|
55
|
-
]
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"type": "textarea",
|
|
59
|
-
"key": "message",
|
|
60
|
-
"label": "Message",
|
|
61
|
-
"required": true,
|
|
62
|
-
"maxLength": 1000
|
|
33
|
+
"key": "concept",
|
|
34
|
+
"label": "Animation Concept (Optional)",
|
|
35
|
+
"required": false,
|
|
36
|
+
"maxLength": 100,
|
|
37
|
+
"placeholder": "e.g., fun, elegant, dynamic, cozy..."
|
|
63
38
|
},
|
|
64
39
|
{
|
|
65
|
-
"type": "
|
|
66
|
-
"key": "
|
|
67
|
-
"label": "
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
40
|
+
"type": "group",
|
|
41
|
+
"key": "slides_input",
|
|
42
|
+
"label": "Video Slides",
|
|
43
|
+
"element_label": "Slide $index",
|
|
44
|
+
"repeat": {
|
|
45
|
+
"min": 1,
|
|
46
|
+
"max": 5
|
|
72
47
|
},
|
|
73
|
-
"
|
|
48
|
+
"elements": [
|
|
49
|
+
{
|
|
50
|
+
"type": "file",
|
|
51
|
+
"key": "main_image",
|
|
52
|
+
"label": "Main Image",
|
|
53
|
+
"required": true,
|
|
54
|
+
"accept": {
|
|
55
|
+
"extensions": ["png", "jpg", "jpeg", "webp"],
|
|
56
|
+
"mime": ["image/png", "image/jpeg", "image/webp"]
|
|
57
|
+
},
|
|
58
|
+
"maxSizeMB": 25
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "files",
|
|
62
|
+
"key": "elements",
|
|
63
|
+
"label": "Element Images (Optional)",
|
|
64
|
+
"required": false,
|
|
65
|
+
"accept": {
|
|
66
|
+
"extensions": ["png", "jpg", "jpeg", "webp"],
|
|
67
|
+
"mime": ["image/png", "image/jpeg", "image/webp"]
|
|
68
|
+
},
|
|
69
|
+
"minCount": 0,
|
|
70
|
+
"maxCount": 5,
|
|
71
|
+
"maxSizeMB": 10
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
74
|
}
|
|
75
75
|
]
|
|
76
76
|
};
|
package/dist/form-builder.js
CHANGED
|
@@ -119,6 +119,10 @@
|
|
|
119
119
|
errors.push(`${here}: repeat.min > repeat.max`);
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
// Validate element_label if provided (optional)
|
|
123
|
+
if (el.element_label != null && typeof el.element_label !== 'string') {
|
|
124
|
+
errors.push(`${here}: element_label must be a string`);
|
|
125
|
+
}
|
|
122
126
|
if (Array.isArray(el.elements)) validateElements(el.elements, pathJoin(path, el.key));
|
|
123
127
|
}
|
|
124
128
|
});
|
|
@@ -590,11 +594,17 @@
|
|
|
590
594
|
wrapper.dataset.groupPath = pathKey;
|
|
591
595
|
|
|
592
596
|
const groupWrap = document.createElement('div');
|
|
597
|
+
|
|
598
|
+
// Group title (above the whole group)
|
|
599
|
+
const groupTitle = document.createElement('div');
|
|
600
|
+
groupTitle.className = 'text-lg font-semibold text-gray-900 mb-4';
|
|
601
|
+
groupTitle.textContent = element.label || element.key;
|
|
602
|
+
groupWrap.appendChild(groupTitle);
|
|
603
|
+
|
|
593
604
|
const header = document.createElement('div');
|
|
594
605
|
header.className = 'flex items-center justify-between my-2 pb-2 border-b border-gray-200';
|
|
595
606
|
|
|
596
607
|
const left = document.createElement('div');
|
|
597
|
-
left.innerHTML = `<span>${element.label || element.key}</span>`;
|
|
598
608
|
header.appendChild(left);
|
|
599
609
|
|
|
600
610
|
const right = document.createElement('div');
|
|
@@ -619,18 +629,48 @@
|
|
|
619
629
|
const refreshControls = () => {
|
|
620
630
|
const n = countItems();
|
|
621
631
|
addBtn.disabled = n >= max;
|
|
622
|
-
left.innerHTML = `<span
|
|
632
|
+
left.innerHTML = `<span class="text-sm text-gray-600">Items: ${n} / ${max === Infinity ? '∞' : max} (min: ${min})</span>`;
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const updateItemIndexes = () => {
|
|
636
|
+
const items = itemsWrap.querySelectorAll(':scope > .groupItem');
|
|
637
|
+
items.forEach((item, index) => {
|
|
638
|
+
const titleElement = item.querySelector('h4');
|
|
639
|
+
if (titleElement) {
|
|
640
|
+
let labelText;
|
|
641
|
+
if (element.element_label) {
|
|
642
|
+
labelText = element.element_label.replace('$index', index + 1);
|
|
643
|
+
} else {
|
|
644
|
+
labelText = `${element.label || element.key} #${index + 1}`;
|
|
645
|
+
}
|
|
646
|
+
titleElement.textContent = labelText;
|
|
647
|
+
}
|
|
648
|
+
});
|
|
623
649
|
};
|
|
624
650
|
|
|
625
651
|
const addItem = (prefillObj) => {
|
|
652
|
+
const itemIndex = countItems() + 1;
|
|
626
653
|
const item = document.createElement('div');
|
|
627
|
-
item.className = 'groupItem border border-dashed border-gray-300 rounded-lg p-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
element.elements.forEach(child => item.appendChild(renderElement(child, subCtx, options)));
|
|
654
|
+
item.className = 'groupItem border border-dashed border-gray-300 rounded-lg p-4 mb-3 bg-blue-50/30';
|
|
655
|
+
|
|
656
|
+
// Individual item title with index
|
|
657
|
+
const itemTitle = document.createElement('div');
|
|
658
|
+
itemTitle.className = 'flex items-center justify-between mb-4 pb-2 border-b border-gray-300';
|
|
633
659
|
|
|
660
|
+
const itemLabel = document.createElement('h4');
|
|
661
|
+
itemLabel.className = 'text-md font-medium text-gray-800';
|
|
662
|
+
|
|
663
|
+
// Use element_label if provided, with $index placeholder support
|
|
664
|
+
let labelText;
|
|
665
|
+
if (element.element_label) {
|
|
666
|
+
labelText = element.element_label.replace('$index', itemIndex);
|
|
667
|
+
} else {
|
|
668
|
+
labelText = `${element.label || element.key} #${itemIndex}`;
|
|
669
|
+
}
|
|
670
|
+
itemLabel.textContent = labelText;
|
|
671
|
+
itemTitle.appendChild(itemLabel);
|
|
672
|
+
|
|
673
|
+
// Add remove button to title
|
|
634
674
|
const rem = document.createElement('button');
|
|
635
675
|
rem.type = 'button';
|
|
636
676
|
rem.className = 'bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-xs font-medium transition-colors';
|
|
@@ -639,8 +679,18 @@
|
|
|
639
679
|
if (countItems() <= (element.repeat.min ?? 0)) return;
|
|
640
680
|
itemsWrap.removeChild(item);
|
|
641
681
|
refreshControls();
|
|
682
|
+
// Re-index remaining items
|
|
683
|
+
updateItemIndexes();
|
|
642
684
|
});
|
|
643
|
-
|
|
685
|
+
itemTitle.appendChild(rem);
|
|
686
|
+
|
|
687
|
+
const subCtx = {
|
|
688
|
+
path: pathJoin(ctx.path, element.key + `[${countItems()}]`),
|
|
689
|
+
prefill: prefillObj || {}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
item.appendChild(itemTitle);
|
|
693
|
+
element.elements.forEach(child => item.appendChild(renderElement(child, subCtx, options)));
|
|
644
694
|
itemsWrap.appendChild(item);
|
|
645
695
|
refreshControls();
|
|
646
696
|
};
|
|
@@ -681,8 +731,9 @@
|
|
|
681
731
|
function collectAndValidate(schema, formRoot, skipValidation = false) {
|
|
682
732
|
const errors = [];
|
|
683
733
|
|
|
684
|
-
function collectElement(element, scopeRoot) {
|
|
734
|
+
function collectElement(element, scopeRoot, elementPath = '') {
|
|
685
735
|
const key = element.key;
|
|
736
|
+
const fullPath = elementPath ? `${elementPath}.${key}` : key;
|
|
686
737
|
|
|
687
738
|
switch (element.type) {
|
|
688
739
|
case 'text':
|
|
@@ -690,26 +741,26 @@
|
|
|
690
741
|
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
691
742
|
const val = (input?.value ?? '').trim();
|
|
692
743
|
if (!skipValidation && element.required && val === '') {
|
|
693
|
-
errors.push(
|
|
744
|
+
errors.push(`Field "${fullPath}" is required`);
|
|
694
745
|
markValidity(input, 'required');
|
|
695
746
|
} else if (!skipValidation && val !== '') {
|
|
696
747
|
if (element.minLength != null && val.length < element.minLength) {
|
|
697
|
-
errors.push(
|
|
748
|
+
errors.push(`Field "${fullPath}" must be at least ${element.minLength} characters`);
|
|
698
749
|
markValidity(input, `minLength=${element.minLength}`);
|
|
699
750
|
}
|
|
700
751
|
if (element.maxLength != null && val.length > element.maxLength) {
|
|
701
|
-
errors.push(
|
|
752
|
+
errors.push(`Field "${fullPath}" must be at most ${element.maxLength} characters`);
|
|
702
753
|
markValidity(input, `maxLength=${element.maxLength}`);
|
|
703
754
|
}
|
|
704
755
|
if (element.pattern) {
|
|
705
756
|
try {
|
|
706
757
|
const re = new RegExp(element.pattern);
|
|
707
758
|
if (!re.test(val)) {
|
|
708
|
-
errors.push(
|
|
759
|
+
errors.push(`Field "${fullPath}" does not match required format`);
|
|
709
760
|
markValidity(input, 'pattern mismatch');
|
|
710
761
|
}
|
|
711
762
|
} catch {
|
|
712
|
-
errors.push(
|
|
763
|
+
errors.push(`Field "${fullPath}" has invalid validation pattern`);
|
|
713
764
|
markValidity(input, 'invalid pattern');
|
|
714
765
|
}
|
|
715
766
|
}
|
|
@@ -815,10 +866,10 @@
|
|
|
815
866
|
const max = element.repeat.max ?? Infinity;
|
|
816
867
|
if (!skipValidation && n < min) errors.push(`${key}: count < min=${min}`);
|
|
817
868
|
if (!skipValidation && n > max) errors.push(`${key}: count > max=${max}`);
|
|
818
|
-
items.forEach(item => {
|
|
869
|
+
items.forEach((item, index) => {
|
|
819
870
|
const obj = {};
|
|
820
871
|
element.elements.forEach(child => {
|
|
821
|
-
obj[child.key] = collectElement(child, item);
|
|
872
|
+
obj[child.key] = collectElement(child, item, `${fullPath}[${index}]`);
|
|
822
873
|
});
|
|
823
874
|
out.push(obj);
|
|
824
875
|
});
|
|
@@ -826,7 +877,7 @@
|
|
|
826
877
|
} else {
|
|
827
878
|
const obj = {};
|
|
828
879
|
element.elements.forEach(child => {
|
|
829
|
-
obj[child.key] = collectElement(child, itemsWrap);
|
|
880
|
+
obj[child.key] = collectElement(child, itemsWrap, fullPath);
|
|
830
881
|
});
|
|
831
882
|
return obj;
|
|
832
883
|
}
|
|
@@ -839,7 +890,7 @@
|
|
|
839
890
|
|
|
840
891
|
const result = {};
|
|
841
892
|
schema.elements.forEach(element => {
|
|
842
|
-
result[element.key] = collectElement(element, formRoot);
|
|
893
|
+
result[element.key] = collectElement(element, formRoot, '');
|
|
843
894
|
});
|
|
844
895
|
|
|
845
896
|
return { result, errors };
|
|
@@ -847,7 +898,20 @@
|
|
|
847
898
|
|
|
848
899
|
// Main form rendering function
|
|
849
900
|
function renderForm(schema, container, options = {}) {
|
|
850
|
-
const {
|
|
901
|
+
const {
|
|
902
|
+
prefill = {},
|
|
903
|
+
readonly = false,
|
|
904
|
+
debug = false,
|
|
905
|
+
onSubmit,
|
|
906
|
+
onDraft,
|
|
907
|
+
onError,
|
|
908
|
+
buttons = {}
|
|
909
|
+
} = options;
|
|
910
|
+
|
|
911
|
+
if (debug) {
|
|
912
|
+
console.log('[FormBuilder Debug] Rendering form with schema:', schema);
|
|
913
|
+
console.log('[FormBuilder Debug] Options:', options);
|
|
914
|
+
}
|
|
851
915
|
|
|
852
916
|
// Validate schema first
|
|
853
917
|
const schemaErrors = validateSchema(schema);
|
|
@@ -877,9 +941,11 @@
|
|
|
877
941
|
const submitBtn = document.createElement('button');
|
|
878
942
|
submitBtn.type = 'button';
|
|
879
943
|
submitBtn.className = 'bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors';
|
|
880
|
-
submitBtn.textContent = 'Submit Form';
|
|
944
|
+
submitBtn.textContent = buttons.submit || 'Submit Form';
|
|
881
945
|
submitBtn.addEventListener('click', () => {
|
|
946
|
+
if (debug) console.log('[FormBuilder Debug] Submit button clicked');
|
|
882
947
|
const { result, errors } = collectAndValidate(schema, formEl, false);
|
|
948
|
+
if (debug) console.log('[FormBuilder Debug] Validation result:', { result, errors });
|
|
883
949
|
if (errors.length > 0) {
|
|
884
950
|
if (onError) onError(errors);
|
|
885
951
|
} else {
|
|
@@ -890,9 +956,11 @@
|
|
|
890
956
|
const draftBtn = document.createElement('button');
|
|
891
957
|
draftBtn.type = 'button';
|
|
892
958
|
draftBtn.className = 'bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors';
|
|
893
|
-
draftBtn.textContent = 'Save Draft';
|
|
959
|
+
draftBtn.textContent = buttons.draft || 'Save Draft';
|
|
894
960
|
draftBtn.addEventListener('click', () => {
|
|
961
|
+
if (debug) console.log('[FormBuilder Debug] Draft button clicked');
|
|
895
962
|
const { result } = collectAndValidate(schema, formEl, true); // Skip validation for drafts
|
|
963
|
+
if (debug) console.log('[FormBuilder Debug] Draft result:', result);
|
|
896
964
|
if (onDraft) onDraft(result);
|
|
897
965
|
});
|
|
898
966
|
|
|
@@ -967,6 +1035,6 @@
|
|
|
967
1035
|
exports.setUploadHandler = setUploadHandler;
|
|
968
1036
|
exports.setDownloadHandler = setDownloadHandler;
|
|
969
1037
|
exports.setThumbnailHandler = setThumbnailHandler;
|
|
970
|
-
exports.version = '0.1.
|
|
1038
|
+
exports.version = '0.1.5';
|
|
971
1039
|
|
|
972
1040
|
}));
|
package/dist/index.html
CHANGED
package/docs/schema.md
CHANGED
|
@@ -175,27 +175,28 @@ Nested object structure with optional repetition.
|
|
|
175
175
|
}
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
**With Repetition:**
|
|
178
|
+
**With Repetition and Custom Element Titles:**
|
|
179
179
|
```json
|
|
180
180
|
{
|
|
181
181
|
"type": "group",
|
|
182
|
-
"key": "
|
|
183
|
-
"label": "
|
|
182
|
+
"key": "slides",
|
|
183
|
+
"label": "Video Slides",
|
|
184
|
+
"element_label": "Slide $index",
|
|
184
185
|
"repeat": {
|
|
185
186
|
"min": 1,
|
|
186
187
|
"max": 5
|
|
187
188
|
},
|
|
188
189
|
"elements": [
|
|
189
190
|
{
|
|
190
|
-
"type": "
|
|
191
|
-
"key": "
|
|
192
|
-
"label": "
|
|
191
|
+
"type": "file",
|
|
192
|
+
"key": "image",
|
|
193
|
+
"label": "Slide Image",
|
|
193
194
|
"required": true
|
|
194
195
|
},
|
|
195
196
|
{
|
|
196
197
|
"type": "text",
|
|
197
|
-
"key": "
|
|
198
|
-
"label": "
|
|
198
|
+
"key": "title",
|
|
199
|
+
"label": "Slide Title",
|
|
199
200
|
"required": true
|
|
200
201
|
}
|
|
201
202
|
]
|
|
@@ -204,6 +205,7 @@ Nested object structure with optional repetition.
|
|
|
204
205
|
|
|
205
206
|
**Properties:**
|
|
206
207
|
- `elements` - Array of nested form elements
|
|
208
|
+
- `element_label` - Optional custom title for each group item (supports `$index` placeholder)
|
|
207
209
|
- `repeat.min` - Minimum number of repetitions
|
|
208
210
|
- `repeat.max` - Maximum number of repetitions
|
|
209
211
|
|