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 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 it in your project & define your JSON schema + config:
43
+ 2. Import and define your JSON Schema with `x-jsf-presentation` hints:
46
44
 
47
45
  ```typescript
48
- import { jsform } from 'vanjs-jsf';
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: 'object',
52
+ type: "object",
52
53
  properties: {
53
- userName: { type: 'string', title: 'Name' },
54
- age: { type: 'number', title: 'Age' },
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
- // Initial values to fill the form
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 = jsfConfig.formValues;
76
- alert(`Submitted successfully: ${JSON.stringify(values, null, 2)}`);
77
- console.log("Submitted!", values);
82
+ const values = config.formValues;
83
+ alert(`Submitted: ${JSON.stringify(values, null, 2)}`);
78
84
  };
79
85
 
80
- // Add the JSON Schema Form to an VanJS element
81
- div(
86
+ van.add(
87
+ document.body,
88
+ div(
82
89
  h1("json-schema-form + VanJS"),
83
- p("This demo uses VanJS without any other form library."),
90
+ p("Dynamic form generated from JSON Schema."),
84
91
  jsform(
85
92
  {
86
93
  name: "my-jsf-form",
87
- schema: schema, // JSON Schema defined previously
88
- config: config, // JSON Schema Form configuration
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
@@ -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;
@@ -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
  }
@@ -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;