@uportal/form-builder 1.3.2 → 2.0.0
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 +10 -0
- package/dist/form-builder.js +699 -0
- package/dist/form-builder.js.map +1 -0
- package/dist/form-builder.min.js +317 -0
- package/dist/form-builder.min.js.map +1 -0
- package/package.json +35 -53
- package/src/form-builder.js +673 -0
- package/CHANGELOG.md +0 -219
- package/build/asset-manifest.json +0 -7
- package/build/favicon.ico +0 -0
- package/build/index.html +0 -1
- package/build/manifest.json +0 -15
- package/build/precache-manifest.e7546ad26defe0da5fad7a6ae0d03bdb.js +0 -10
- package/build/sample/communication-preferences/form.json +0 -0
- package/build/service-worker.js +0 -34
- package/build/static/js/form-builder.js +0 -2
- package/build/static/js/form-builder.js.map +0 -1
- package/src/App.js +0 -301
- package/src/App.test.js +0 -9
- package/src/setupProxy.js +0 -8
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import decode from 'jwt-decode';
|
|
3
|
+
|
|
4
|
+
function delay(ms) {
|
|
5
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Dynamic Form Builder Web Component
|
|
10
|
+
* Fetches JSON schema and form data, then renders a dynamic form
|
|
11
|
+
*
|
|
12
|
+
* @element form-builder
|
|
13
|
+
*
|
|
14
|
+
* @attr {string} fbms-base-url - Base URL of the form builder microservice
|
|
15
|
+
* @attr {string} fbms-form-fname - Form name to fetch
|
|
16
|
+
* @attr {string} oidc-url - OpenID Connect URL for authentication
|
|
17
|
+
* @attr {string} styles - Optional custom CSS styles
|
|
18
|
+
*/
|
|
19
|
+
class FormBuilder extends LitElement {
|
|
20
|
+
static properties = {
|
|
21
|
+
fbmsBaseUrl: { type: String, attribute: 'fbms-base-url' },
|
|
22
|
+
fbmsFormFname: { type: String, attribute: 'fbms-form-fname' },
|
|
23
|
+
oidcUrl: { type: String, attribute: 'oidc-url' },
|
|
24
|
+
customStyles: { type: String, attribute: 'styles' },
|
|
25
|
+
|
|
26
|
+
// Internal state
|
|
27
|
+
schema: { type: Object, state: true },
|
|
28
|
+
formData: { type: Object, state: true },
|
|
29
|
+
uiSchema: { type: Object, state: true },
|
|
30
|
+
fbmsFormVersion: { type: String, state: true },
|
|
31
|
+
loading: { type: Boolean, state: true },
|
|
32
|
+
submitting: { type: Boolean, state: true },
|
|
33
|
+
error: { type: String, state: true },
|
|
34
|
+
token: { type: String, state: true },
|
|
35
|
+
decoded: { type: Object, state: true },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
static styles = css`
|
|
39
|
+
:host {
|
|
40
|
+
display: block;
|
|
41
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.container {
|
|
45
|
+
max-width: 800px;
|
|
46
|
+
margin: 0 auto;
|
|
47
|
+
padding: 20px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.loading {
|
|
51
|
+
text-align: center;
|
|
52
|
+
padding: 40px;
|
|
53
|
+
color: #666;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.error {
|
|
57
|
+
background-color: #fee;
|
|
58
|
+
border: 1px solid #fcc;
|
|
59
|
+
border-radius: 4px;
|
|
60
|
+
padding: 15px;
|
|
61
|
+
margin: 20px 0;
|
|
62
|
+
color: #c00;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
form {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
gap: 20px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.form-group {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
gap: 8px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
label {
|
|
78
|
+
font-weight: 500;
|
|
79
|
+
color: #333;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.required::after {
|
|
83
|
+
content: ' *';
|
|
84
|
+
color: #c00;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.description {
|
|
88
|
+
font-size: 0.875rem;
|
|
89
|
+
color: #666;
|
|
90
|
+
margin-top: 4px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
input[type="text"],
|
|
94
|
+
input[type="email"],
|
|
95
|
+
input[type="number"],
|
|
96
|
+
input[type="date"],
|
|
97
|
+
input[type="tel"],
|
|
98
|
+
textarea,
|
|
99
|
+
select {
|
|
100
|
+
padding: 8px 12px;
|
|
101
|
+
border: 1px solid #ccc;
|
|
102
|
+
border-radius: 4px;
|
|
103
|
+
font-size: 1rem;
|
|
104
|
+
font-family: inherit;
|
|
105
|
+
width: 100%;
|
|
106
|
+
box-sizing: border-box;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
input:focus,
|
|
110
|
+
textarea:focus,
|
|
111
|
+
select:focus {
|
|
112
|
+
outline: none;
|
|
113
|
+
border-color: #0066cc;
|
|
114
|
+
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
textarea {
|
|
118
|
+
min-height: 100px;
|
|
119
|
+
resize: vertical;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
input[type="checkbox"],
|
|
123
|
+
input[type="radio"] {
|
|
124
|
+
margin-right: 8px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.checkbox-group,
|
|
128
|
+
.radio-group {
|
|
129
|
+
display: flex;
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
gap: 8px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.checkbox-item,
|
|
135
|
+
.radio-item {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.error-message {
|
|
141
|
+
color: #c00;
|
|
142
|
+
font-size: 0.875rem;
|
|
143
|
+
margin-top: 4px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.buttons {
|
|
147
|
+
display: flex;
|
|
148
|
+
gap: 12px;
|
|
149
|
+
margin-top: 20px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
button {
|
|
153
|
+
padding: 10px 20px;
|
|
154
|
+
border: none;
|
|
155
|
+
border-radius: 4px;
|
|
156
|
+
font-size: 1rem;
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
font-family: inherit;
|
|
159
|
+
transition: background-color 0.2s;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
button[type="submit"] {
|
|
163
|
+
background-color: #0066cc;
|
|
164
|
+
color: white;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
button[type="submit"]:hover {
|
|
168
|
+
background-color: #0052a3;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
button[type="button"] {
|
|
172
|
+
background-color: #c0c0c0;
|
|
173
|
+
color: #333;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
button[type="button"]:hover {
|
|
177
|
+
background-color: #e0e0e0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
button:disabled {
|
|
181
|
+
opacity: 0.5;
|
|
182
|
+
cursor: not-allowed;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.spinner {
|
|
186
|
+
display: inline-block;
|
|
187
|
+
width: 1em;
|
|
188
|
+
height: 1em;
|
|
189
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
190
|
+
border-radius: 50%;
|
|
191
|
+
border-top-color: white;
|
|
192
|
+
animation: spin 0.8s linear infinite;
|
|
193
|
+
margin-right: 8px;
|
|
194
|
+
vertical-align: middle;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@keyframes spin {
|
|
198
|
+
to { transform: rotate(360deg); }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.button-content {
|
|
202
|
+
display: inline-flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: center;
|
|
205
|
+
}
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
constructor() {
|
|
209
|
+
super();
|
|
210
|
+
this.loading = true;
|
|
211
|
+
this.submitting = false;
|
|
212
|
+
this.error = null;
|
|
213
|
+
this.schema = null;
|
|
214
|
+
this.formData = {};
|
|
215
|
+
this.uiSchema = null;
|
|
216
|
+
this.fbmsFormVersion = null;
|
|
217
|
+
this.token = null;
|
|
218
|
+
this.decoded = {sub: 'unknown'};
|
|
219
|
+
this.fieldErrors = {};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async connectedCallback() {
|
|
223
|
+
super.connectedCallback();
|
|
224
|
+
await this.initialize();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async initialize() {
|
|
228
|
+
try {
|
|
229
|
+
this.loading = true;
|
|
230
|
+
this.error = null;
|
|
231
|
+
|
|
232
|
+
// Fetch OIDC token if URL provided
|
|
233
|
+
if (this.oidcUrl) {
|
|
234
|
+
await this.fetchToken();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Fetch form schema and data
|
|
238
|
+
await Promise.all([
|
|
239
|
+
this.fetchSchema(),
|
|
240
|
+
this.fetchFormData(),
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
this.loading = false;
|
|
244
|
+
} catch (err) {
|
|
245
|
+
this.error = err.message || 'Failed to initialize form';
|
|
246
|
+
this.loading = false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async fetchToken() {
|
|
251
|
+
try {
|
|
252
|
+
const response = await fetch(this.oidcUrl, {
|
|
253
|
+
credentials: 'include',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
throw new Error('Failed to authenticate');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const data = await response.text();
|
|
261
|
+
this.token = data
|
|
262
|
+
this.decoded = decode(this.token);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error('Token fetch error:', err);
|
|
265
|
+
throw new Error('Authentication failed');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async fetchSchema() {
|
|
270
|
+
const url = `${this.fbmsBaseUrl}/api/v1/forms/${this.fbmsFormFname}`;
|
|
271
|
+
const headers = {
|
|
272
|
+
'content-type': 'application/jwt',
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (this.token) {
|
|
276
|
+
headers['Authorization'] = `Bearer ${this.token}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const response = await fetch(url, {
|
|
280
|
+
credentials: 'same-origin',
|
|
281
|
+
headers,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
throw new Error(`Failed to fetch schema: ${response.statusText}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const data = await response.json();
|
|
289
|
+
this.fbmsFormVersion = data.version;
|
|
290
|
+
this.schema = data.schema || data;
|
|
291
|
+
this.uiSchema = data.metadata;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async fetchFormData() {
|
|
295
|
+
const url = `${this.fbmsBaseUrl}/api/v1/submissions/${this.fbmsFormFname}?safarifix=${Math.random()}`;
|
|
296
|
+
const headers = {
|
|
297
|
+
'content-type': 'application/jwt',
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
if (this.token) {
|
|
301
|
+
headers['Authorization'] = `Bearer ${this.token}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const response = await fetch(url, {
|
|
306
|
+
credentials: 'same-origin',
|
|
307
|
+
headers,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (response.ok) {
|
|
311
|
+
const payload = await response.json();
|
|
312
|
+
this.formData = payload.answers;
|
|
313
|
+
} else {
|
|
314
|
+
// It's OK if there's no existing data
|
|
315
|
+
this.formData = {};
|
|
316
|
+
}
|
|
317
|
+
} catch (err) {
|
|
318
|
+
// Non-critical error
|
|
319
|
+
console.warn('Could not fetch form data:', err);
|
|
320
|
+
this.formData = {};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
handleInputChange(fieldName, event) {
|
|
325
|
+
const { type, value, checked } = event.target;
|
|
326
|
+
|
|
327
|
+
this.formData = {
|
|
328
|
+
...this.formData,
|
|
329
|
+
[fieldName]: type === 'checkbox' ? checked : value,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Clear field error on change
|
|
333
|
+
if (this.fieldErrors[fieldName]) {
|
|
334
|
+
this.fieldErrors = { ...this.fieldErrors };
|
|
335
|
+
delete this.fieldErrors[fieldName];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
handleArrayChange(fieldName, index, event) {
|
|
340
|
+
const currentArray = this.formData[fieldName] || [];
|
|
341
|
+
const newArray = [...currentArray];
|
|
342
|
+
newArray[index] = event.target.value;
|
|
343
|
+
|
|
344
|
+
this.formData = {
|
|
345
|
+
...this.formData,
|
|
346
|
+
[fieldName]: newArray,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
validateForm() {
|
|
351
|
+
const errors = {};
|
|
352
|
+
const { properties = {}, required = [] } = this.schema;
|
|
353
|
+
|
|
354
|
+
// Check required fields
|
|
355
|
+
required.forEach(fieldName => {
|
|
356
|
+
const value = this.formData[fieldName];
|
|
357
|
+
if (value === undefined || value === null || value === '') {
|
|
358
|
+
errors[fieldName] = 'This field is required';
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Type validation
|
|
363
|
+
Object.entries(properties).forEach(([fieldName, fieldSchema]) => {
|
|
364
|
+
const value = this.formData[fieldName];
|
|
365
|
+
|
|
366
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
367
|
+
// Email validation
|
|
368
|
+
if (fieldSchema.format === 'email') {
|
|
369
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
370
|
+
if (!emailRegex.test(value)) {
|
|
371
|
+
errors[fieldName] = 'Invalid email address';
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Number validation
|
|
376
|
+
if (fieldSchema.type === 'number' || fieldSchema.type === 'integer') {
|
|
377
|
+
const num = Number(value);
|
|
378
|
+
if (isNaN(num)) {
|
|
379
|
+
errors[fieldName] = 'Must be a number';
|
|
380
|
+
} else {
|
|
381
|
+
if (fieldSchema.minimum !== undefined && num < fieldSchema.minimum) {
|
|
382
|
+
errors[fieldName] = `Must be at least ${fieldSchema.minimum}`;
|
|
383
|
+
}
|
|
384
|
+
if (fieldSchema.maximum !== undefined && num > fieldSchema.maximum) {
|
|
385
|
+
errors[fieldName] = `Must be at most ${fieldSchema.maximum}`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// String length validation
|
|
391
|
+
if (fieldSchema.type === 'string') {
|
|
392
|
+
if (fieldSchema.minLength && value.length < fieldSchema.minLength) {
|
|
393
|
+
errors[fieldName] = `Must be at least ${fieldSchema.minLength} characters`;
|
|
394
|
+
}
|
|
395
|
+
if (fieldSchema.maxLength && value.length > fieldSchema.maxLength) {
|
|
396
|
+
errors[fieldName] = `Must be at most ${fieldSchema.maxLength} characters`;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
this.fieldErrors = errors;
|
|
403
|
+
return Object.keys(errors).length === 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async handleSubmit(event) {
|
|
407
|
+
event.preventDefault();
|
|
408
|
+
|
|
409
|
+
if (!this.validateForm()) {
|
|
410
|
+
this.requestUpdate();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Prevent double-submission
|
|
415
|
+
if (this.submitting) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
this.submitting = true;
|
|
421
|
+
this.error = null;
|
|
422
|
+
const body = {
|
|
423
|
+
username: this.decoded.sub,
|
|
424
|
+
formFname: this.fbmsFormFname,
|
|
425
|
+
formVersion: this.fbmsFormVersion,
|
|
426
|
+
timestamp: Date.now(),
|
|
427
|
+
answers: this.formData
|
|
428
|
+
};
|
|
429
|
+
await delay(300);
|
|
430
|
+
|
|
431
|
+
const url = `${this.fbmsBaseUrl}/api/v1/submissions/${this.fbmsFormFname}`;
|
|
432
|
+
const headers = {
|
|
433
|
+
'content-type': 'application/json',
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
if (this.token) {
|
|
437
|
+
headers['Authorization'] = `Bearer ${this.token}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const response = await fetch(url, {
|
|
441
|
+
method: 'POST',
|
|
442
|
+
credentials: 'same-origin',
|
|
443
|
+
headers,
|
|
444
|
+
body: JSON.stringify(body),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (!response.ok) {
|
|
448
|
+
throw new Error(`Failed to submit form: ${response.statusText}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Dispatch success event
|
|
452
|
+
this.dispatchEvent(new CustomEvent('form-submit-success', {
|
|
453
|
+
detail: { data: body },
|
|
454
|
+
bubbles: true,
|
|
455
|
+
composed: true,
|
|
456
|
+
}));
|
|
457
|
+
|
|
458
|
+
// Optional: Reset or show success message
|
|
459
|
+
this.error = null;
|
|
460
|
+
} catch (err) {
|
|
461
|
+
this.error = err.message || 'Failed to submit form';
|
|
462
|
+
|
|
463
|
+
this.dispatchEvent(new CustomEvent('form-submit-error', {
|
|
464
|
+
detail: { error: err.message },
|
|
465
|
+
bubbles: true,
|
|
466
|
+
composed: true,
|
|
467
|
+
}));
|
|
468
|
+
} finally {
|
|
469
|
+
this.submitting = false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
handleReset() {
|
|
474
|
+
this.formData = {};
|
|
475
|
+
this.fieldErrors = {};
|
|
476
|
+
this.requestUpdate();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
renderField(fieldName, fieldSchema) {
|
|
480
|
+
const value = this.formData[fieldName];
|
|
481
|
+
const error = this.fieldErrors[fieldName];
|
|
482
|
+
const required = this.schema.required?.includes(fieldName);
|
|
483
|
+
const uiOptions = this.uiSchema?.[fieldName] || {};
|
|
484
|
+
|
|
485
|
+
return html`
|
|
486
|
+
<div class="form-group">
|
|
487
|
+
<label class="${required ? 'required' : ''}" for="${fieldName}">
|
|
488
|
+
${fieldSchema.title || fieldName}
|
|
489
|
+
</label>
|
|
490
|
+
|
|
491
|
+
${fieldSchema.description ? html`
|
|
492
|
+
<span class="description">${fieldSchema.description}</span>
|
|
493
|
+
` : ''}
|
|
494
|
+
|
|
495
|
+
${this.renderInput(fieldName, fieldSchema, value, uiOptions)}
|
|
496
|
+
|
|
497
|
+
${error ? html`
|
|
498
|
+
<span class="error-message">${error}</span>
|
|
499
|
+
` : ''}
|
|
500
|
+
</div>
|
|
501
|
+
`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
renderInput(fieldName, fieldSchema, value, uiOptions) {
|
|
505
|
+
const { type, enum: enumValues, format } = fieldSchema;
|
|
506
|
+
|
|
507
|
+
// Enum - render as select
|
|
508
|
+
if (enumValues) {
|
|
509
|
+
return html`
|
|
510
|
+
<select
|
|
511
|
+
id="${fieldName}"
|
|
512
|
+
name="${fieldName}"
|
|
513
|
+
.value="${value || ''}"
|
|
514
|
+
@change="${(e) => this.handleInputChange(fieldName, e)}"
|
|
515
|
+
>
|
|
516
|
+
<option value="">-- Select --</option>
|
|
517
|
+
${enumValues.map(opt => html`
|
|
518
|
+
<option value="${opt}" ?selected="${value === opt}">
|
|
519
|
+
${opt}
|
|
520
|
+
</option>
|
|
521
|
+
`)}
|
|
522
|
+
</select>
|
|
523
|
+
`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Boolean - render as checkbox
|
|
527
|
+
if (type === 'boolean') {
|
|
528
|
+
return html`
|
|
529
|
+
<div class="checkbox-item">
|
|
530
|
+
<input
|
|
531
|
+
type="checkbox"
|
|
532
|
+
id="${fieldName}"
|
|
533
|
+
name="${fieldName}"
|
|
534
|
+
.checked="${!!value}"
|
|
535
|
+
@change="${(e) => this.handleInputChange(fieldName, e)}"
|
|
536
|
+
/>
|
|
537
|
+
<label for="${fieldName}">${fieldSchema.title || fieldName}</label>
|
|
538
|
+
</div>
|
|
539
|
+
`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// String with format
|
|
543
|
+
if (type === 'string') {
|
|
544
|
+
if (format === 'email') {
|
|
545
|
+
return html`
|
|
546
|
+
<input
|
|
547
|
+
type="email"
|
|
548
|
+
id="${fieldName}"
|
|
549
|
+
name="${fieldName}"
|
|
550
|
+
.value="${value || ''}"
|
|
551
|
+
@input="${(e) => this.handleInputChange(fieldName, e)}"
|
|
552
|
+
/>
|
|
553
|
+
`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (format === 'date') {
|
|
557
|
+
return html`
|
|
558
|
+
<input
|
|
559
|
+
type="date"
|
|
560
|
+
id="${fieldName}"
|
|
561
|
+
name="${fieldName}"
|
|
562
|
+
.value="${value || ''}"
|
|
563
|
+
@input="${(e) => this.handleInputChange(fieldName, e)}"
|
|
564
|
+
/>
|
|
565
|
+
`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (uiOptions['ui:widget'] === 'textarea') {
|
|
569
|
+
return html`
|
|
570
|
+
<textarea
|
|
571
|
+
id="${fieldName}"
|
|
572
|
+
name="${fieldName}"
|
|
573
|
+
.value="${value || ''}"
|
|
574
|
+
@input="${(e) => this.handleInputChange(fieldName, e)}"
|
|
575
|
+
></textarea>
|
|
576
|
+
`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Default text input
|
|
580
|
+
return html`
|
|
581
|
+
<input
|
|
582
|
+
type="text"
|
|
583
|
+
id="${fieldName}"
|
|
584
|
+
name="${fieldName}"
|
|
585
|
+
.value="${value || ''}"
|
|
586
|
+
@input="${(e) => this.handleInputChange(fieldName, e)}"
|
|
587
|
+
/>
|
|
588
|
+
`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Number
|
|
592
|
+
if (type === 'number' || type === 'integer') {
|
|
593
|
+
return html`
|
|
594
|
+
<input
|
|
595
|
+
type="number"
|
|
596
|
+
id="${fieldName}"
|
|
597
|
+
name="${fieldName}"
|
|
598
|
+
.value="${value || ''}"
|
|
599
|
+
step="${type === 'integer' ? '1' : 'any'}"
|
|
600
|
+
@input="${(e) => this.handleInputChange(fieldName, e)}"
|
|
601
|
+
/>
|
|
602
|
+
`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Fallback
|
|
606
|
+
return html`
|
|
607
|
+
<input
|
|
608
|
+
type="text"
|
|
609
|
+
id="${fieldName}"
|
|
610
|
+
name="${fieldName}"
|
|
611
|
+
.value="${value || ''}"
|
|
612
|
+
@input="${(e) => this.handleInputChange(fieldName, e)}"
|
|
613
|
+
/>
|
|
614
|
+
`;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
render() {
|
|
618
|
+
if (this.loading) {
|
|
619
|
+
return html`
|
|
620
|
+
<div class="container">
|
|
621
|
+
<div class="loading">Loading form...</div>
|
|
622
|
+
</div>
|
|
623
|
+
`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (this.error) {
|
|
627
|
+
return html`
|
|
628
|
+
<div class="container">
|
|
629
|
+
<div class="error">
|
|
630
|
+
<strong>Error:</strong> ${this.error}
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (!this.schema || !this.schema.properties) {
|
|
637
|
+
return html`
|
|
638
|
+
<div class="container">
|
|
639
|
+
<div class="error">Invalid form schema</div>
|
|
640
|
+
</div>
|
|
641
|
+
`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return html`
|
|
645
|
+
${this.customStyles ? html`<style>${this.customStyles}</style>` : ''}
|
|
646
|
+
|
|
647
|
+
<div class="container">
|
|
648
|
+
<form @submit="${this.handleSubmit}">
|
|
649
|
+
${this.schema.title ? html`<h2>${this.schema.title}</h2>` : ''}
|
|
650
|
+
${this.schema.description ? html`<p>${this.schema.description}</p>` : ''}
|
|
651
|
+
|
|
652
|
+
${Object.entries(this.schema.properties).map(([fieldName, fieldSchema]) =>
|
|
653
|
+
this.renderField(fieldName, fieldSchema)
|
|
654
|
+
)}
|
|
655
|
+
|
|
656
|
+
<div class="buttons">
|
|
657
|
+
<button type="submit" ?disabled="${this.submitting}">
|
|
658
|
+
<span class="button-content">
|
|
659
|
+
${this.submitting ? html`<span class="spinner"></span>` : ''}
|
|
660
|
+
${this.submitting ? 'Submitting...' : 'Submit'}
|
|
661
|
+
</span>
|
|
662
|
+
</button>
|
|
663
|
+
<button type="button" @click="${this.handleReset}" ?disabled="${this.submitting}">
|
|
664
|
+
Reset
|
|
665
|
+
</button>
|
|
666
|
+
</div>
|
|
667
|
+
</form>
|
|
668
|
+
</div>
|
|
669
|
+
`;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
customElements.define('form-builder', FormBuilder);
|