vanjs-jsf 0.1.0 → 0.2.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 +32 -25
- package/dist/VanJsfField.d.ts +2 -0
- package/dist/VanJsfField.js +310 -1
- package/dist/VanJsfForm.js +4 -0
- package/dist/index.js.map +3 -3
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -36,62 +36,69 @@ The currently supported form element types are:
|
|
|
36
36
|
|
|
37
37
|
1. Install the library:
|
|
38
38
|
|
|
39
|
-
Install the library from the npm registry:
|
|
40
|
-
|
|
41
39
|
```bash
|
|
42
40
|
npm install vanjs-jsf
|
|
43
41
|
```
|
|
44
42
|
|
|
45
|
-
2. Import
|
|
43
|
+
2. Import and define your JSON Schema with `x-jsf-presentation` hints:
|
|
46
44
|
|
|
47
45
|
```typescript
|
|
48
|
-
import
|
|
46
|
+
import van from "vanjs-core";
|
|
47
|
+
import { jsform } from "vanjs-jsf";
|
|
48
|
+
|
|
49
|
+
const { div, h1, p, button } = van.tags;
|
|
49
50
|
|
|
50
51
|
const schema = {
|
|
51
|
-
type:
|
|
52
|
+
type: "object",
|
|
52
53
|
properties: {
|
|
53
|
-
userName: {
|
|
54
|
-
|
|
54
|
+
userName: {
|
|
55
|
+
type: "string",
|
|
56
|
+
title: "Name",
|
|
57
|
+
"x-jsf-presentation": { inputType: "text" },
|
|
58
|
+
},
|
|
59
|
+
age: {
|
|
60
|
+
type: "number",
|
|
61
|
+
title: "Age",
|
|
62
|
+
"x-jsf-presentation": { inputType: "number" },
|
|
63
|
+
},
|
|
55
64
|
},
|
|
65
|
+
required: ["userName"],
|
|
66
|
+
"x-jsf-order": ["userName", "age"],
|
|
56
67
|
};
|
|
68
|
+
```
|
|
57
69
|
|
|
58
|
-
|
|
70
|
+
3. Create a config with initial values and render the form:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
59
73
|
const initialValues = { userName: "Simon" };
|
|
60
|
-
// JSON Schema Form config
|
|
61
74
|
const config = {
|
|
62
75
|
strictInputType: false,
|
|
63
76
|
initialValues: initialValues,
|
|
64
77
|
formValues: initialValues,
|
|
65
78
|
};
|
|
66
|
-
```
|
|
67
79
|
|
|
68
|
-
3. Render the form using VanJS UI framework & handle form submit.
|
|
69
|
-
|
|
70
|
-
This library will call the `onsubmit` handler set using the VanJS `props` passing the original source event.
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
80
|
const handleOnSubmit = (e: Event) => {
|
|
74
81
|
e.preventDefault();
|
|
75
|
-
const values =
|
|
76
|
-
alert(`Submitted
|
|
77
|
-
console.log("Submitted!", values);
|
|
82
|
+
const values = config.formValues;
|
|
83
|
+
alert(`Submitted: ${JSON.stringify(values, null, 2)}`);
|
|
78
84
|
};
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
van.add(
|
|
87
|
+
document.body,
|
|
88
|
+
div(
|
|
82
89
|
h1("json-schema-form + VanJS"),
|
|
83
|
-
p("
|
|
90
|
+
p("Dynamic form generated from JSON Schema."),
|
|
84
91
|
jsform(
|
|
85
92
|
{
|
|
86
93
|
name: "my-jsf-form",
|
|
87
|
-
schema: schema,
|
|
88
|
-
config: config,
|
|
94
|
+
schema: schema,
|
|
95
|
+
config: config,
|
|
89
96
|
onsubmit: handleOnSubmit,
|
|
90
97
|
},
|
|
91
98
|
button({ type: "submit" }, "Submit")
|
|
92
99
|
)
|
|
93
|
-
)
|
|
94
|
-
|
|
100
|
+
)
|
|
101
|
+
);
|
|
95
102
|
```
|
|
96
103
|
|
|
97
104
|
## Development
|
package/dist/VanJsfField.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export declare class VanJsfField extends VanJSComponent {
|
|
|
14
14
|
handleChange: (field: VanJsfField, value: MultiType) => void;
|
|
15
15
|
isVisibleState: State<boolean>;
|
|
16
16
|
errorState: State<string>;
|
|
17
|
+
/** Used by file fields to pass the selected arrayPath key to formValues */
|
|
18
|
+
arrayPathValue: string;
|
|
17
19
|
constructor(field: Record<string, unknown>, initVal: MultiType, handleChange: (field: VanJsfField, value: MultiType) => void);
|
|
18
20
|
get inputType(): string;
|
|
19
21
|
get label(): string;
|
package/dist/VanJsfField.js
CHANGED
|
@@ -7,7 +7,7 @@ import { json, jsonParseLinter } from "@codemirror/lang-json";
|
|
|
7
7
|
import { lintGutter, linter, forEachDiagnostic } from "@codemirror/lint";
|
|
8
8
|
import * as eslint from "eslint-linter-browserify";
|
|
9
9
|
import globals from "globals";
|
|
10
|
-
const { div, p, input, label, textarea, legend, link, fieldset, span, select, option } = van.tags;
|
|
10
|
+
const { div, p, input, label, textarea, legend, link, fieldset, span, select, option, button, table, tr, th, td, strong, small } = van.tags;
|
|
11
11
|
var FieldType;
|
|
12
12
|
(function (FieldType) {
|
|
13
13
|
FieldType["text"] = "text";
|
|
@@ -18,6 +18,7 @@ var FieldType;
|
|
|
18
18
|
FieldType["radio"] = "radio";
|
|
19
19
|
FieldType["date"] = "date";
|
|
20
20
|
FieldType["fieldset"] = "fieldset";
|
|
21
|
+
FieldType["file"] = "file";
|
|
21
22
|
})(FieldType || (FieldType = {}));
|
|
22
23
|
const eslintConfig = {
|
|
23
24
|
// eslint configuration
|
|
@@ -41,6 +42,8 @@ export class VanJsfField extends VanJSComponent {
|
|
|
41
42
|
handleChange;
|
|
42
43
|
isVisibleState;
|
|
43
44
|
errorState;
|
|
45
|
+
/** Used by file fields to pass the selected arrayPath key to formValues */
|
|
46
|
+
arrayPathValue = "";
|
|
44
47
|
constructor(field, initVal, handleChange) {
|
|
45
48
|
super();
|
|
46
49
|
this.field = field;
|
|
@@ -244,6 +247,312 @@ export class VanJsfField extends VanJSComponent {
|
|
|
244
247
|
onchange: (e) => this.handleChange(this, e.target.value),
|
|
245
248
|
}), opt.label, opt.description))), p({ class: this.errorClass }, () => this.error));
|
|
246
249
|
break;
|
|
250
|
+
case FieldType.file: {
|
|
251
|
+
const accept = this.field.accept || ".json,.csv,.xlsx";
|
|
252
|
+
const maxSizeMB = this.field.maxSizeMB || 50;
|
|
253
|
+
const previewRows = this.field.previewRows || 5;
|
|
254
|
+
// Reactive states
|
|
255
|
+
const fileNameState = van.state("");
|
|
256
|
+
const fileSizeState = van.state("");
|
|
257
|
+
const parsingState = van.state(false);
|
|
258
|
+
const parsedDataState = van.state(null);
|
|
259
|
+
const arrayPathOptionsState = van.state([]);
|
|
260
|
+
const selectedArrayPathState = van.state("");
|
|
261
|
+
const dragOverState = van.state(false);
|
|
262
|
+
const formatSize = (bytes) => {
|
|
263
|
+
if (bytes < 1024)
|
|
264
|
+
return `${bytes} B`;
|
|
265
|
+
if (bytes < 1024 * 1024)
|
|
266
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
267
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
268
|
+
};
|
|
269
|
+
const formatNumber = (n) => n.toLocaleString();
|
|
270
|
+
const resolveArrayFromJson = (parsed) => {
|
|
271
|
+
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "object") {
|
|
272
|
+
return { data: parsed, paths: [] };
|
|
273
|
+
}
|
|
274
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
275
|
+
const candidates = [];
|
|
276
|
+
for (const [key, val] of Object.entries(parsed)) {
|
|
277
|
+
if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object") {
|
|
278
|
+
candidates.push(key);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (candidates.length === 1) {
|
|
282
|
+
return { data: parsed[candidates[0]], paths: [] };
|
|
283
|
+
}
|
|
284
|
+
if (candidates.length > 1) {
|
|
285
|
+
return { data: null, paths: candidates };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return { data: null, paths: [] };
|
|
289
|
+
};
|
|
290
|
+
const setData = (data, arrayPath) => {
|
|
291
|
+
parsedDataState.val = data;
|
|
292
|
+
this.arrayPathValue = arrayPath;
|
|
293
|
+
selectedArrayPathState.val = arrayPath;
|
|
294
|
+
parsingState.val = false;
|
|
295
|
+
this.handleChange(this, JSON.stringify(data));
|
|
296
|
+
};
|
|
297
|
+
const setError = (msg) => {
|
|
298
|
+
parsingState.val = false;
|
|
299
|
+
parsedDataState.val = null;
|
|
300
|
+
this.error = msg;
|
|
301
|
+
};
|
|
302
|
+
const processFile = async (file) => {
|
|
303
|
+
// Reset state
|
|
304
|
+
this.error = "";
|
|
305
|
+
parsedDataState.val = null;
|
|
306
|
+
arrayPathOptionsState.val = [];
|
|
307
|
+
selectedArrayPathState.val = "";
|
|
308
|
+
this.arrayPathValue = "";
|
|
309
|
+
// Validate size
|
|
310
|
+
if (file.size > maxSizeMB * 1024 * 1024) {
|
|
311
|
+
setError(`File exceeds maximum size of ${maxSizeMB} MB`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
fileNameState.val = file.name;
|
|
315
|
+
fileSizeState.val = formatSize(file.size);
|
|
316
|
+
parsingState.val = true;
|
|
317
|
+
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
|
318
|
+
try {
|
|
319
|
+
if (ext === "json") {
|
|
320
|
+
const text = await file.text();
|
|
321
|
+
let parsed;
|
|
322
|
+
try {
|
|
323
|
+
parsed = JSON.parse(text);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
setError("Invalid JSON file");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const { data, paths } = resolveArrayFromJson(parsed);
|
|
330
|
+
if (data) {
|
|
331
|
+
setData(data, "");
|
|
332
|
+
}
|
|
333
|
+
else if (paths.length > 1) {
|
|
334
|
+
arrayPathOptionsState.val = paths;
|
|
335
|
+
parsingState.val = false;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
setError("JSON does not contain an array of objects");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else if (ext === "csv") {
|
|
342
|
+
try {
|
|
343
|
+
const Papa = await import("papaparse");
|
|
344
|
+
const text = await file.text();
|
|
345
|
+
const result = Papa.default.parse(text, { header: true, skipEmptyLines: true });
|
|
346
|
+
if (result.errors.length > 0) {
|
|
347
|
+
setError(`CSV parse error: ${result.errors[0].message}`);
|
|
348
|
+
}
|
|
349
|
+
else if (!result.data || result.data.length === 0) {
|
|
350
|
+
setError("CSV file is empty or has no data rows");
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
setData(result.data, "");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
setError("Install papaparse to support CSV files: npm install papaparse");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (ext === "xlsx" || ext === "xls") {
|
|
361
|
+
try {
|
|
362
|
+
const XLSX = await import("xlsx");
|
|
363
|
+
const buffer = await file.arrayBuffer();
|
|
364
|
+
const workbook = XLSX.read(buffer);
|
|
365
|
+
const firstSheetName = workbook.SheetNames[0];
|
|
366
|
+
if (!firstSheetName) {
|
|
367
|
+
setError("XLSX file has no sheets");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const sheet = workbook.Sheets[firstSheetName];
|
|
371
|
+
const data = XLSX.utils.sheet_to_json(sheet);
|
|
372
|
+
if (data.length === 0) {
|
|
373
|
+
setError("XLSX sheet is empty");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
setData(data, "");
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
if (e instanceof Error && e.message.includes("Failed to fetch dynamically imported module")) {
|
|
380
|
+
setError("Install xlsx to support XLSX files: npm install xlsx");
|
|
381
|
+
}
|
|
382
|
+
else if (e instanceof Error) {
|
|
383
|
+
setError(`XLSX parse error: ${e.message}`);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
setError("Install xlsx to support XLSX files: npm install xlsx");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
setError(`Unsupported file extension: .${ext}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
setError(e instanceof Error ? e.message : "Error processing file");
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const clearFile = () => {
|
|
399
|
+
fileNameState.val = "";
|
|
400
|
+
fileSizeState.val = "";
|
|
401
|
+
parsedDataState.val = null;
|
|
402
|
+
parsingState.val = false;
|
|
403
|
+
arrayPathOptionsState.val = [];
|
|
404
|
+
selectedArrayPathState.val = "";
|
|
405
|
+
this.arrayPathValue = "";
|
|
406
|
+
this.error = "";
|
|
407
|
+
this.handleChange(this, "");
|
|
408
|
+
};
|
|
409
|
+
const fileInput = input({
|
|
410
|
+
type: "file",
|
|
411
|
+
accept,
|
|
412
|
+
style: "display: none;",
|
|
413
|
+
onchange: (e) => {
|
|
414
|
+
const files = e.target.files;
|
|
415
|
+
if (files && files[0])
|
|
416
|
+
processFile(files[0]);
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
const dropZone = div({
|
|
420
|
+
style: () => {
|
|
421
|
+
const over = dragOverState.val;
|
|
422
|
+
return `border: 2px dashed ${over ? "#4a90d9" : "#ccc"}; border-radius: 8px; padding: 24px; text-align: center; cursor: pointer; transition: border-color 0.2s; background: ${over ? "#f0f7ff" : "transparent"};`;
|
|
423
|
+
},
|
|
424
|
+
ondragover: (e) => { e.preventDefault(); dragOverState.val = true; },
|
|
425
|
+
ondragleave: () => { dragOverState.val = false; },
|
|
426
|
+
ondrop: (e) => {
|
|
427
|
+
e.preventDefault();
|
|
428
|
+
dragOverState.val = false;
|
|
429
|
+
const files = e.dataTransfer?.files;
|
|
430
|
+
if (files && files[0])
|
|
431
|
+
processFile(files[0]);
|
|
432
|
+
},
|
|
433
|
+
onclick: () => fileInput.click(),
|
|
434
|
+
}, p({ style: "margin: 0; color: #666;" }, `Drop a file here or click to browse (${accept})`));
|
|
435
|
+
// We need to store the raw JSON for arrayPath selection
|
|
436
|
+
const rawJsonState = van.state(null);
|
|
437
|
+
// Override processFile's JSON branch to also store raw JSON
|
|
438
|
+
const originalProcessFile = processFile;
|
|
439
|
+
const processFileWrapped = async (file) => {
|
|
440
|
+
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
|
441
|
+
if (ext === "json") {
|
|
442
|
+
this.error = "";
|
|
443
|
+
parsedDataState.val = null;
|
|
444
|
+
arrayPathOptionsState.val = [];
|
|
445
|
+
selectedArrayPathState.val = "";
|
|
446
|
+
this.arrayPathValue = "";
|
|
447
|
+
if (file.size > maxSizeMB * 1024 * 1024) {
|
|
448
|
+
setError(`File exceeds maximum size of ${maxSizeMB} MB`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
fileNameState.val = file.name;
|
|
452
|
+
fileSizeState.val = formatSize(file.size);
|
|
453
|
+
parsingState.val = true;
|
|
454
|
+
try {
|
|
455
|
+
const text = await file.text();
|
|
456
|
+
let parsed;
|
|
457
|
+
try {
|
|
458
|
+
parsed = JSON.parse(text);
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
setError("Invalid JSON file");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const { data, paths } = resolveArrayFromJson(parsed);
|
|
465
|
+
if (data) {
|
|
466
|
+
setData(data, "");
|
|
467
|
+
}
|
|
468
|
+
else if (paths.length > 1) {
|
|
469
|
+
rawJsonState.val = parsed;
|
|
470
|
+
arrayPathOptionsState.val = paths;
|
|
471
|
+
parsingState.val = false;
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
setError("JSON does not contain an array of objects");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (e) {
|
|
478
|
+
setError(e instanceof Error ? e.message : "Error processing file");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
await originalProcessFile(file);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
// Re-bind event handlers to use wrapped version
|
|
486
|
+
fileInput.onchange = (e) => {
|
|
487
|
+
const files = e.target.files;
|
|
488
|
+
if (files && files[0])
|
|
489
|
+
processFileWrapped(files[0]);
|
|
490
|
+
};
|
|
491
|
+
dropZone.ondrop = (e) => {
|
|
492
|
+
e.preventDefault();
|
|
493
|
+
dragOverState.val = false;
|
|
494
|
+
const files = e.dataTransfer?.files;
|
|
495
|
+
if (files && files[0])
|
|
496
|
+
processFileWrapped(files[0]);
|
|
497
|
+
};
|
|
498
|
+
// Preview table
|
|
499
|
+
const previewTable = () => {
|
|
500
|
+
return div(() => {
|
|
501
|
+
const data = parsedDataState.val;
|
|
502
|
+
if (!data || data.length === 0)
|
|
503
|
+
return div();
|
|
504
|
+
const columns = Object.keys(data[0]);
|
|
505
|
+
const rows = data.slice(0, previewRows);
|
|
506
|
+
return div({ style: "margin-top: 8px; overflow-x: auto;" }, div({ style: "margin-bottom: 4px; font-size: 0.9em; color: #666;" }, `Showing ${Math.min(previewRows, data.length)} of ${formatNumber(data.length)} rows`), table({ style: "border-collapse: collapse; width: 100%; font-size: 0.85em;" }, tr(...columns.map((col) => th({ style: "border: 1px solid #ddd; padding: 4px 8px; background: #f5f5f5; text-align: left; white-space: nowrap;" }, col))), ...rows.map((row) => tr(...columns.map((col) => td({ style: "border: 1px solid #ddd; padding: 4px 8px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" }, String(row[col] ?? "")))))));
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
// arrayPath select handler — when user picks a path, extract data
|
|
510
|
+
const arrayPathSelectorEl = div(() => {
|
|
511
|
+
const options = arrayPathOptionsState.val;
|
|
512
|
+
if (options.length === 0)
|
|
513
|
+
return div();
|
|
514
|
+
return div({ style: "margin-top: 8px;" }, label({ style: "margin-right: 5px;" }, "Select data array:"), select({
|
|
515
|
+
onchange: (e) => {
|
|
516
|
+
const key = e.target.value;
|
|
517
|
+
if (!key)
|
|
518
|
+
return;
|
|
519
|
+
const raw = rawJsonState.val;
|
|
520
|
+
if (raw && Array.isArray(raw[key])) {
|
|
521
|
+
setData(raw[key], key);
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
}, option({ value: "" }, "-- choose --"), ...options.map((k) => option({ value: k }, k))));
|
|
525
|
+
});
|
|
526
|
+
// File info bar + clear button
|
|
527
|
+
const fileInfoBar = () => {
|
|
528
|
+
return div(() => {
|
|
529
|
+
const name = fileNameState.val;
|
|
530
|
+
if (!name)
|
|
531
|
+
return div();
|
|
532
|
+
return div({ style: "margin-top: 8px; display: flex; align-items: center; gap: 8px;" }, strong(name), small({ style: "color: #888;" }, `(${fileSizeState.val})`), button({
|
|
533
|
+
type: "button",
|
|
534
|
+
style: "cursor: pointer; background: none; border: 1px solid #ccc; border-radius: 4px; padding: 2px 8px; font-size: 0.85em;",
|
|
535
|
+
onclick: (e) => {
|
|
536
|
+
e.stopPropagation();
|
|
537
|
+
clearFile();
|
|
538
|
+
// Reset the file input so the same file can be re-selected
|
|
539
|
+
fileInput.value = "";
|
|
540
|
+
},
|
|
541
|
+
}, "Clear"));
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
// Parsing indicator
|
|
545
|
+
const parsingIndicator = () => {
|
|
546
|
+
return div(() => {
|
|
547
|
+
if (!parsingState.val)
|
|
548
|
+
return div();
|
|
549
|
+
return div({ style: "margin-top: 8px; color: #666;" }, "Parsing file...");
|
|
550
|
+
});
|
|
551
|
+
};
|
|
552
|
+
el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
|
|
553
|
+
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), fileInput, dropZone, fileInfoBar(), parsingIndicator(), arrayPathSelectorEl, previewTable(), p({ class: this.errorClass }, () => this.error));
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
247
556
|
default:
|
|
248
557
|
el = div({ style: "border: 1px dashed gray; padding: 8px;" }, `Field "${this.name}" unsupported: The type "${this.inputType}" has no UI component built yet.`);
|
|
249
558
|
}
|
package/dist/VanJsfForm.js
CHANGED
|
@@ -60,6 +60,10 @@ class VanJsfForm {
|
|
|
60
60
|
}
|
|
61
61
|
handleFieldChange(field, value) {
|
|
62
62
|
this.formValues[field.name] = value;
|
|
63
|
+
// For file fields, also store the selected arrayPath key
|
|
64
|
+
if (field.inputType === "file") {
|
|
65
|
+
this.formValues[field.name + "__arrayPath"] = field.arrayPathValue;
|
|
66
|
+
}
|
|
63
67
|
this.config.formValues = this.formValues;
|
|
64
68
|
const { formErrors } = this.headlessForm.handleValidation(this.formValues);
|
|
65
69
|
let extraError = false;
|