vanjs-jsf 0.1.0 → 0.2.1
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 +4 -0
- package/dist/VanJsfField.js +130 -1
- package/dist/VanJsfForm.js +6 -0
- package/dist/index.js.map +3 -3
- package/package.json +1 -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,10 @@ 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 file metadata to formValues */
|
|
18
|
+
fileNameValue: string;
|
|
19
|
+
fileSizeValue: string;
|
|
20
|
+
fileTypeValue: string;
|
|
17
21
|
constructor(field: Record<string, unknown>, initVal: MultiType, handleChange: (field: VanJsfField, value: MultiType) => void);
|
|
18
22
|
get inputType(): string;
|
|
19
23
|
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, 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,10 @@ export class VanJsfField extends VanJSComponent {
|
|
|
41
42
|
handleChange;
|
|
42
43
|
isVisibleState;
|
|
43
44
|
errorState;
|
|
45
|
+
/** Used by file fields to pass file metadata to formValues */
|
|
46
|
+
fileNameValue = "";
|
|
47
|
+
fileSizeValue = "";
|
|
48
|
+
fileTypeValue = "";
|
|
44
49
|
constructor(field, initVal, handleChange) {
|
|
45
50
|
super();
|
|
46
51
|
this.field = field;
|
|
@@ -244,6 +249,130 @@ export class VanJsfField extends VanJSComponent {
|
|
|
244
249
|
onchange: (e) => this.handleChange(this, e.target.value),
|
|
245
250
|
}), opt.label, opt.description))), p({ class: this.errorClass }, () => this.error));
|
|
246
251
|
break;
|
|
252
|
+
case FieldType.file: {
|
|
253
|
+
const accept = this.field.accept || "";
|
|
254
|
+
const maxSizeMB = this.field.maxSizeMB;
|
|
255
|
+
const readAs = this.field.readAs || "text";
|
|
256
|
+
// Reactive states
|
|
257
|
+
const fileNameState = van.state("");
|
|
258
|
+
const fileSizeState = van.state("");
|
|
259
|
+
const dragOverState = van.state(false);
|
|
260
|
+
const readingState = van.state(false);
|
|
261
|
+
const formatSize = (bytes) => {
|
|
262
|
+
if (bytes < 1024)
|
|
263
|
+
return `${bytes} B`;
|
|
264
|
+
if (bytes < 1024 * 1024)
|
|
265
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
266
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
267
|
+
};
|
|
268
|
+
const readFile = (file) => {
|
|
269
|
+
this.error = "";
|
|
270
|
+
// Validate size
|
|
271
|
+
if (maxSizeMB && file.size > maxSizeMB * 1024 * 1024) {
|
|
272
|
+
this.error = `File exceeds maximum size of ${maxSizeMB} MB`;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
fileNameState.val = file.name;
|
|
276
|
+
fileSizeState.val = formatSize(file.size);
|
|
277
|
+
readingState.val = true;
|
|
278
|
+
// Store metadata
|
|
279
|
+
this.fileNameValue = file.name;
|
|
280
|
+
this.fileSizeValue = String(file.size);
|
|
281
|
+
this.fileTypeValue = file.type;
|
|
282
|
+
const reader = new FileReader();
|
|
283
|
+
reader.onload = () => {
|
|
284
|
+
readingState.val = false;
|
|
285
|
+
let result = reader.result;
|
|
286
|
+
if (readAs === "arrayBuffer" && reader.result instanceof ArrayBuffer) {
|
|
287
|
+
// Base64-encode the ArrayBuffer
|
|
288
|
+
const bytes = new Uint8Array(reader.result);
|
|
289
|
+
let binary = "";
|
|
290
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
291
|
+
binary += String.fromCharCode(bytes[i]);
|
|
292
|
+
}
|
|
293
|
+
result = btoa(binary);
|
|
294
|
+
}
|
|
295
|
+
this.handleChange(this, result);
|
|
296
|
+
};
|
|
297
|
+
reader.onerror = () => {
|
|
298
|
+
readingState.val = false;
|
|
299
|
+
this.error = "Error reading file";
|
|
300
|
+
};
|
|
301
|
+
if (readAs === "dataURL") {
|
|
302
|
+
reader.readAsDataURL(file);
|
|
303
|
+
}
|
|
304
|
+
else if (readAs === "arrayBuffer") {
|
|
305
|
+
reader.readAsArrayBuffer(file);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
reader.readAsText(file);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
const clearFile = () => {
|
|
312
|
+
fileNameState.val = "";
|
|
313
|
+
fileSizeState.val = "";
|
|
314
|
+
readingState.val = false;
|
|
315
|
+
this.fileNameValue = "";
|
|
316
|
+
this.fileSizeValue = "";
|
|
317
|
+
this.fileTypeValue = "";
|
|
318
|
+
this.error = "";
|
|
319
|
+
this.handleChange(this, "");
|
|
320
|
+
};
|
|
321
|
+
const fileInput = input({
|
|
322
|
+
type: "file",
|
|
323
|
+
accept,
|
|
324
|
+
style: "display: none;",
|
|
325
|
+
onchange: (e) => {
|
|
326
|
+
const files = e.target.files;
|
|
327
|
+
if (files && files[0])
|
|
328
|
+
readFile(files[0]);
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
const dropZone = div({
|
|
332
|
+
style: () => {
|
|
333
|
+
const over = dragOverState.val;
|
|
334
|
+
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"};`;
|
|
335
|
+
},
|
|
336
|
+
ondragover: (e) => { e.preventDefault(); dragOverState.val = true; },
|
|
337
|
+
ondragleave: () => { dragOverState.val = false; },
|
|
338
|
+
ondrop: (e) => {
|
|
339
|
+
e.preventDefault();
|
|
340
|
+
dragOverState.val = false;
|
|
341
|
+
const files = e.dataTransfer?.files;
|
|
342
|
+
if (files && files[0])
|
|
343
|
+
readFile(files[0]);
|
|
344
|
+
},
|
|
345
|
+
onclick: () => fileInput.click(),
|
|
346
|
+
}, p({ style: "margin: 0; color: #666;" }, accept
|
|
347
|
+
? `Drop a file here or click to browse (${accept})`
|
|
348
|
+
: "Drop a file here or click to browse"));
|
|
349
|
+
const fileInfoBar = () => {
|
|
350
|
+
return div(() => {
|
|
351
|
+
const name = fileNameState.val;
|
|
352
|
+
if (!name)
|
|
353
|
+
return div();
|
|
354
|
+
return div({ style: "margin-top: 8px; display: flex; align-items: center; gap: 8px;" }, strong(name), small({ style: "color: #888;" }, `(${fileSizeState.val})`), button({
|
|
355
|
+
type: "button",
|
|
356
|
+
style: "cursor: pointer; background: none; border: 1px solid #ccc; border-radius: 4px; padding: 2px 8px; font-size: 0.85em;",
|
|
357
|
+
onclick: (e) => {
|
|
358
|
+
e.stopPropagation();
|
|
359
|
+
clearFile();
|
|
360
|
+
fileInput.value = "";
|
|
361
|
+
},
|
|
362
|
+
}, "Clear"));
|
|
363
|
+
});
|
|
364
|
+
};
|
|
365
|
+
const readingIndicator = () => {
|
|
366
|
+
return div(() => {
|
|
367
|
+
if (!readingState.val)
|
|
368
|
+
return div();
|
|
369
|
+
return div({ style: "margin-top: 8px; color: #666;" }, "Reading file...");
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
|
|
373
|
+
div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), fileInput, dropZone, fileInfoBar(), readingIndicator(), p({ class: this.errorClass }, () => this.error));
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
247
376
|
default:
|
|
248
377
|
el = div({ style: "border: 1px dashed gray; padding: 8px;" }, `Field "${this.name}" unsupported: The type "${this.inputType}" has no UI component built yet.`);
|
|
249
378
|
}
|
package/dist/VanJsfForm.js
CHANGED
|
@@ -60,6 +60,12 @@ class VanJsfForm {
|
|
|
60
60
|
}
|
|
61
61
|
handleFieldChange(field, value) {
|
|
62
62
|
this.formValues[field.name] = value;
|
|
63
|
+
// For file fields, also store file metadata
|
|
64
|
+
if (field.inputType === "file") {
|
|
65
|
+
this.formValues[field.name + "__fileName"] = field.fileNameValue;
|
|
66
|
+
this.formValues[field.name + "__fileSize"] = field.fileSizeValue;
|
|
67
|
+
this.formValues[field.name + "__fileType"] = field.fileTypeValue;
|
|
68
|
+
}
|
|
63
69
|
this.config.formValues = this.formValues;
|
|
64
70
|
const { formErrors } = this.headlessForm.handleValidation(this.formValues);
|
|
65
71
|
let extraError = false;
|