vanjs-jsf 0.3.0 → 0.3.2

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
@@ -1,14 +1,15 @@
1
- # 📦 VanJS-JSF a JSON Schema Form Library for VanJS
1
+ # VanJS-JSF a JSON Schema Form Library for VanJS
2
2
 
3
3
  **Generate dynamic UI forms effortlessly with TypeScript and JSON Schema, powered by the JSON Schema Form Headless UI framework.**
4
4
 
5
5
  ## Description
6
6
 
7
- This library aims to provide a robust and flexible solution for dynamically generating user interface (UI) forms using JSON Schema definitions. It is built on **TypeScript** for type safety and leverages the powerful [JSON Schema Form Headless UI](https://github.com/remoteoss/json-schema-form) framework, which you can find documented [here](https://json-schema-form.vercel.app). It ensures a lightweight, modern, accessible, and customizable form-building experience.
7
+ This library provides a robust and flexible solution for dynamically generating user interface (UI) forms using JSON Schema definitions. It is built on **TypeScript** for type safety and leverages the powerful [JSON Schema Form Headless UI](https://github.com/remoteoss/json-schema-form) framework, which you can find documented [here](https://json-schema-form.vercel.app). It ensures a lightweight, modern, accessible, and customizable form-building experience.
8
8
 
9
9
  ### Features
10
10
 
11
11
  - [x] **Dynamic Form Generation**: Create forms directly from JSON Schema, reducing repetitive coding tasks.
12
+ - [x] **Theming API**: Inject CSS classes for every form element via a `theme` object — works with Tailwind, Bootstrap, or any CSS framework.
12
13
  - [x] **Customizable**: Tailor form styles, layouts, and behaviours to meet specific UI/UX requirements.
13
14
  - [x] **Headless UI Integration**: Utilize Headless UI's components for accessible and modern interfaces.
14
15
  - [x] **TypeScript-first Approach**: Enjoy strong typing and enhanced developer experience with TypeScript.
@@ -16,90 +17,227 @@ This library aims to provide a robust and flexible solution for dynamically gene
16
17
  - [ ] **Extensible Architecture**: Add custom widgets, field types, and behaviours as needed.
17
18
 
18
19
  ### Available components
19
- The currently supported form element types are:
20
- - text = "text"
21
- - number = "number"
22
- - textarea = "textarea"
23
- - select = "select"
24
- - radio = "radio"
25
- - date = "date" (Pikaday)
26
- - code = "code" (CodeMirror with JSON, JavaScript, TypeScript support)
27
- - fieldset = "fieldset"
28
20
 
29
- ### Use Cases
21
+ The currently supported form element types are:
30
22
 
31
- - Quickly generate forms for dashboards, admin panels, and dynamic web applications.
32
- - Prototype or test form-based UIs with minimal setup.
33
- - Build reusable form components for your projects.
23
+ - text = "text"
24
+ - number = "number"
25
+ - textarea = "textarea"
26
+ - select = "select"
27
+ - radio = "radio"
28
+ - date = "date" (Pikaday)
29
+ - code = "code" (CodeMirror with JSON, JavaScript, TypeScript support)
30
+ - fieldset = "fieldset"
31
+ - file = "file" (drag & drop, with configurable `readAs` mode and size validation)
34
32
 
35
33
  ## Getting Started
36
34
 
37
35
  1. Install the library:
38
36
 
39
- ```bash
40
- npm install vanjs-jsf
41
- ```
37
+ ```bash
38
+ npm install vanjs-jsf
39
+ ```
42
40
 
43
41
  2. Import and define your JSON Schema with `x-jsf-presentation` hints:
44
42
 
45
- ```typescript
46
- import van from "vanjs-core";
47
- import { jsform } from "vanjs-jsf";
43
+ ```typescript
44
+ import van from "vanjs-core";
45
+ import { jsform } from "vanjs-jsf";
46
+
47
+ const { div, h1, p, button } = van.tags;
48
+
49
+ const schema = {
50
+ type: "object",
51
+ properties: {
52
+ userName: {
53
+ type: "string",
54
+ title: "Name",
55
+ "x-jsf-presentation": { inputType: "text" },
56
+ },
57
+ age: {
58
+ type: "number",
59
+ title: "Age",
60
+ "x-jsf-presentation": { inputType: "number" },
61
+ },
62
+ },
63
+ required: ["userName"],
64
+ "x-jsf-order": ["userName", "age"],
65
+ };
66
+ ```
67
+
68
+ 3. Create a config with initial values and render the form:
48
69
 
49
- const { div, h1, p, button } = van.tags;
70
+ ```typescript
71
+ const initialValues = { userName: "Simon" };
72
+ const config = {
73
+ strictInputType: false,
74
+ initialValues: initialValues,
75
+ formValues: initialValues,
76
+ };
50
77
 
51
- const schema = {
52
- type: "object",
53
- properties: {
54
- userName: {
55
- type: "string",
56
- title: "Name",
57
- "x-jsf-presentation": { inputType: "text" },
78
+ const handleOnSubmit = (e: Event) => {
79
+ e.preventDefault();
80
+ const values = config.formValues;
81
+ alert(`Submitted: ${JSON.stringify(values, null, 2)}`);
82
+ };
83
+
84
+ van.add(
85
+ document.body,
86
+ div(
87
+ h1("json-schema-form + VanJS"),
88
+ p("Dynamic form generated from JSON Schema."),
89
+ jsform(
90
+ {
91
+ name: "my-jsf-form",
92
+ schema: schema,
93
+ config: config,
94
+ onsubmit: handleOnSubmit,
58
95
  },
59
- age: {
60
- type: "number",
61
- title: "Age",
62
- "x-jsf-presentation": { inputType: "number" },
96
+ button({ type: "submit" }, "Submit"),
97
+ ),
98
+ ),
99
+ );
100
+ ```
101
+
102
+ ## File Upload Field
103
+
104
+ The `file` field type renders a drag & drop zone with file info display. Define it in your schema:
105
+
106
+ ```typescript
107
+ const schema = {
108
+ type: "object",
109
+ properties: {
110
+ document: {
111
+ type: "string",
112
+ title: "Upload Document",
113
+ description: "PDF or image, max 5 MB",
114
+ "x-jsf-presentation": {
115
+ inputType: "file",
116
+ accept: ".pdf,.png,.jpg",
117
+ maxSizeMB: 5,
118
+ readAs: "auto",
63
119
  },
64
120
  },
65
- required: ["userName"],
66
- "x-jsf-order": ["userName", "age"],
67
- };
68
- ```
121
+ },
122
+ "x-jsf-order": ["document"],
123
+ };
124
+ ```
69
125
 
70
- 3. Create a config with initial values and render the form:
126
+ ### `readAs` modes
127
+
128
+ | Value | Behaviour |
129
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
130
+ | `"auto"` (default) | Text extensions (json, csv, txt, xml, yaml, md, html, css, js, ts, sql, etc.) are read as text. All other files are read as binary and returned as base64. |
131
+ | `"text"` | Always reads the file as text (`readAsText`). |
132
+ | `"dataURL"` | Returns a data URL string (`readAsDataURL`). |
133
+ | `"arrayBuffer"` | Reads as ArrayBuffer and returns base64 (`readAsArrayBuffer`). |
134
+
135
+ The file content is stored in `formValues` under the field name. Additional metadata is stored as `<fieldName>__fileName`, `<fieldName>__fileSize`, and `<fieldName>__fileType`.
136
+
137
+ ## Theming
138
+
139
+ By default, vanjs-jsf applies minimal semantic CSS classes (e.g. `jsf-dropzone`, `jsf-file-info`). You can import the default styles:
140
+
141
+ ```typescript
142
+ import "vanjs-jsf/dist/jsf-defaults.css";
143
+ ```
144
+
145
+ To fully customize the appearance, pass a `theme` object to `jsform()`. Theme classes apply to every element type in the form. Per-field classes defined in `x-jsf-presentation` (e.g. `class`, `titleClass`, `containerClass`) take priority over theme classes.
146
+
147
+ ```typescript
148
+ import { jsform, JsfTheme } from "vanjs-jsf";
149
+
150
+ const myTheme: JsfTheme = {
151
+ // Structure
152
+ container: "mb-4",
153
+ label: "block text-sm font-medium text-gray-700 mb-1",
154
+ description: "text-gray-500 text-xs mb-1",
155
+ error: "text-red-500 text-xs mt-1",
156
+ requiredIndicator: "text-red-500 ml-1",
157
+
158
+ // Inputs
159
+ input: "w-full border border-gray-300 rounded px-3 py-2",
160
+ textarea: "w-full border border-gray-300 rounded px-3 py-2 resize-y",
161
+ select: "w-full border border-gray-300 rounded px-3 py-2",
162
+ option: "bg-white",
163
+
164
+ // Radio
165
+ radioGroup: "flex flex-col gap-2",
166
+ radioLabel: "flex items-center gap-2 cursor-pointer",
167
+ radioInput: "accent-blue-500",
168
+
169
+ // Fieldset
170
+ fieldset: "border border-gray-200 rounded p-4",
171
+ legend: "text-sm font-semibold text-gray-700",
172
+
173
+ // File upload
174
+ dropZone:
175
+ "border-2 border-dashed border-gray-300 rounded-lg p-6 text-center cursor-pointer hover:border-blue-400 transition-colors",
176
+ dropZoneActive:
177
+ "border-2 border-dashed border-blue-500 rounded-lg p-6 text-center cursor-pointer bg-blue-50",
178
+ dropZoneText: "text-gray-500 text-sm m-0",
179
+ fileInfoBar: "mt-2 flex items-center gap-2",
180
+ fileName: "font-semibold",
181
+ fileSize: "text-gray-400 text-sm",
182
+ fileClearButton:
183
+ "text-sm border border-gray-300 rounded px-2 py-0.5 hover:bg-gray-100",
184
+ fileReading: "mt-2 text-gray-400 text-sm",
185
+ };
186
+
187
+ const formEl = jsform({
188
+ schema: mySchema,
189
+ config: { initialValues: {}, formValues: {} },
190
+ theme: myTheme,
191
+ onsubmit: handleSubmit,
192
+ });
193
+ ```
194
+
195
+ ### JsfTheme properties
196
+
197
+ | Property | Applies to |
198
+ | ------------------- | ------------------------------------------------------ |
199
+ | `container` | Wrapper `<div>` of each field |
200
+ | `label` | `<label>` elements |
201
+ | `description` | Description `<div>` below the label |
202
+ | `error` | Error `<p>` below the input |
203
+ | `requiredIndicator` | `<span>` with "\*" next to required field labels |
204
+ | `input` | `<input>` for text, number, and date fields |
205
+ | `textarea` | `<textarea>` elements |
206
+ | `select` | `<select>` elements |
207
+ | `option` | `<option>` elements inside selects |
208
+ | `radioGroup` | Container `<div>` for radio options |
209
+ | `radioLabel` | `<label>` wrapping each radio option |
210
+ | `radioInput` | `<input type="radio">` elements |
211
+ | `fieldset` | `<fieldset>` elements |
212
+ | `legend` | `<legend>` elements (falls back to `label` if not set) |
213
+ | `dropZone` | File drop zone container (normal state) |
214
+ | `dropZoneActive` | File drop zone during dragover |
215
+ | `dropZoneText` | Text inside the drop zone |
216
+ | `fileInfoBar` | Container for uploaded file info |
217
+ | `fileName` | File name `<strong>` |
218
+ | `fileSize` | File size `<small>` |
219
+ | `fileClearButton` | "Clear" button |
220
+ | `fileReading` | "Reading file..." indicator |
221
+
222
+ ### Class resolution order
223
+
224
+ For each element, the resolved class follows this priority:
225
+
226
+ 1. **Per-field class** from `x-jsf-presentation` (e.g. `class`, `titleClass`, `containerClass`)
227
+ 2. **Theme class** from the `theme` object
228
+ 3. **Empty string** (no class applied)
229
+
230
+ ### Visibility
231
+
232
+ Hidden fields receive the `jsf-hidden` CSS class instead of inline `display: none`. Make sure your CSS includes:
233
+
234
+ ```css
235
+ .jsf-hidden {
236
+ display: none;
237
+ }
238
+ ```
71
239
 
72
- ```typescript
73
- const initialValues = { userName: "Simon" };
74
- const config = {
75
- strictInputType: false,
76
- initialValues: initialValues,
77
- formValues: initialValues,
78
- };
79
-
80
- const handleOnSubmit = (e: Event) => {
81
- e.preventDefault();
82
- const values = config.formValues;
83
- alert(`Submitted: ${JSON.stringify(values, null, 2)}`);
84
- };
85
-
86
- van.add(
87
- document.body,
88
- div(
89
- h1("json-schema-form + VanJS"),
90
- p("Dynamic form generated from JSON Schema."),
91
- jsform(
92
- {
93
- name: "my-jsf-form",
94
- schema: schema,
95
- config: config,
96
- onsubmit: handleOnSubmit,
97
- },
98
- button({ type: "submit" }, "Submit")
99
- )
100
- )
101
- );
102
- ```
240
+ This is included in `jsf-defaults.css`.
103
241
 
104
242
  ## Development
105
243
 
@@ -115,13 +253,13 @@ npm run lint:fix # Run ESLint with auto-fix
115
253
  ## Publishing
116
254
 
117
255
  1. Update the version in `package.json`
118
- 2. Run the publish script:
256
+ 2. Run the publish script with your npm OTP:
119
257
 
120
258
  ```bash
121
- ./publish.sh
259
+ ./publish.sh <otp>
122
260
  ```
123
261
 
124
- This cleans `dist/`, rebuilds the bundle and type declarations, and publishes to npm.
262
+ This cleans `dist/`, rebuilds the bundle and type declarations, copies `jsf-defaults.css` to `dist/`, and publishes to npm.
125
263
 
126
264
  The package is available at: https://www.npmjs.com/package/vanjs-jsf
127
265
 
@@ -16,11 +16,12 @@ export declare class VanJsfField extends VanJSComponent {
16
16
  isVisibleState: State<boolean>;
17
17
  errorState: State<string>;
18
18
  theme: JsfTheme;
19
+ layoutClass: string;
19
20
  /** Used by file fields to pass file metadata to formValues */
20
21
  fileNameValue: string;
21
22
  fileSizeValue: string;
22
23
  fileTypeValue: string;
23
- constructor(field: Record<string, unknown>, initVal: MultiType, handleChange: (field: VanJsfField, value: MultiType) => void, theme?: JsfTheme);
24
+ constructor(field: Record<string, unknown>, initVal: MultiType, handleChange: (field: VanJsfField, value: MultiType) => void, theme?: JsfTheme, layoutClass?: string);
24
25
  get inputType(): string;
25
26
  get label(): string;
26
27
  get class(): string;
@@ -44,17 +44,19 @@ export class VanJsfField extends VanJSComponent {
44
44
  isVisibleState;
45
45
  errorState;
46
46
  theme;
47
+ layoutClass;
47
48
  /** Used by file fields to pass file metadata to formValues */
48
49
  fileNameValue = "";
49
50
  fileSizeValue = "";
50
51
  fileTypeValue = "";
51
- constructor(field, initVal, handleChange, theme = {}) {
52
+ constructor(field, initVal, handleChange, theme = {}, layoutClass = "") {
52
53
  super();
53
54
  this.field = field;
54
55
  this.name = field.name;
55
56
  this.iniVal = initVal;
56
57
  this.handleChange = handleChange;
57
58
  this.theme = theme;
59
+ this.layoutClass = layoutClass;
58
60
  this.isVisibleState = van.state(this.field.isVisible);
59
61
  this.errorState = van.state("");
60
62
  }
@@ -167,9 +169,10 @@ export class VanJsfField extends VanJSComponent {
167
169
  render() {
168
170
  let el;
169
171
  const baseContainer = resolve(this.containerClass, this.theme.container);
172
+ const containerCls = this.layoutClass ? `${baseContainer} ${this.layoutClass}` : baseContainer;
170
173
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
174
  const props = {
172
- class: () => this.isVisible ? baseContainer : `${baseContainer} jsf-hidden`.trim(),
175
+ class: () => this.isVisible ? containerCls : `${containerCls} jsf-hidden`.trim(),
173
176
  };
174
177
  switch (this.inputType) {
175
178
  case FieldType.text:
@@ -10,7 +10,8 @@ class VanJsfForm {
10
10
  formFields;
11
11
  formValues;
12
12
  theme;
13
- constructor(jsonSchema, config, isValid, theme = {}) {
13
+ layout;
14
+ constructor(jsonSchema, config, isValid, theme = {}, layout = {}) {
14
15
  // Bind methods to instance. Needed to pass functions as props to child components
15
16
  //this.handleSubmit = this.handleSubmit.bind(this);
16
17
  this.handleFieldChange = this.handleFieldChange.bind(this);
@@ -19,6 +20,7 @@ class VanJsfForm {
19
20
  this.config = config;
20
21
  this.isValid = isValid || undefined;
21
22
  this.theme = theme;
23
+ this.layout = layout;
22
24
  // Working with parameters
23
25
  const initialValues = { ...config?.initialValues };
24
26
  this.headlessForm = createHeadlessForm(jsonSchema, config);
@@ -100,7 +102,8 @@ class VanJsfForm {
100
102
  field.fields = this.processFields(field.fields, initialValues, formValues, fieldPath);
101
103
  }
102
104
  // Create and return a new VanJsfField instance for this field
103
- return new VanJsfField(field, initVal, this.handleFieldChange, this.theme);
105
+ const fieldLayout = this.layout[fieldPath] || this.layout[field.name] || "";
106
+ return new VanJsfField(field, initVal, this.handleFieldChange, this.theme, fieldLayout);
104
107
  });
105
108
  }
106
109
  }
@@ -115,7 +118,8 @@ export function jsform(attributes, ...children) {
115
118
  config.formValues = {};
116
119
  const isValid = attributes.isValid;
117
120
  const theme = attributes.theme ?? {};
118
- const vanJsfForm = new VanJsfForm(attributes.schema, config, isValid, theme);
121
+ const layout = attributes.layout ?? {};
122
+ const vanJsfForm = new VanJsfForm(attributes.schema, config, isValid, theme, layout);
119
123
  const fields = vanJsfForm.formFields.map((field) => field.render());
120
124
  const childrenWithFields = [...fields, ...children]; // Concatenate fields with other children
121
125
  const originalOnSubmit = attributes.onsubmit;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { jsform } from "./VanJsfForm";
2
- export type { JsfTheme } from "./theme";
2
+ export type { JsfTheme, JsfLayout } from "./theme";