vanilla-schema-forms
v0.1.5
Published
A JSON schema driven form generator for vanilla JS/TS applications.
Downloads
618
Maintainers
Readme
Vanilla Schema Forms - A JSON schema driven form generator
Vanilla Schema Forms is a web-based application that dynamically generates HTML forms from a given JSON Schema. It is built using Vanilla TypeScript (no heavy frameworks) and Vite for a fast development experience.
Features
- Dynamic Form Generation: Automatically creates HTML forms based on JSON Schema definitions.
- Hyperscript Rendering: Lightweight DOM generation using a hyperscript helper, avoiding innerHTML injection risks.
- Schema Dereferencing: Resolves
$refpointers within your JSON Schema using@apidevtools/json-schema-ref-parser. - Type-Safe Development: Written in TypeScript for robust and maintainable code.
- Modern Tooling: Utilizes Vite for development and bundling, providing a fast and efficient workflow.
- Customizable UI: Configurable field sorting, visibility, and text overrides via configuration files.
- Unit Testing: Includes tests for core parsing logic using Vitest.
Status
Vanilla Schema Forms is in a very early state. I am not using it yet and it may be changed in the future.
Playground
Check out the playground here
How it Works
The application follows a clear pipeline to transform a JSON Schema into an interactive form:
- Schema Loading: The
index.htmlfile (located in the project root) is the entry point. It loads the main application script (src/index.ts). - Schema Fetching & Parsing: The
src/index.tsscript fetches theschema.jsonfile. It then usessrc/parser.tsto parse this schema. - Schema Dereferencing and Transformation:
src/parser.tsutilizes the@apidevtools/json-schema-ref-parserlibrary to dereference any$refpointers in the JSON Schema. It then transforms the raw JSON Schema into a simplified, UI-friendlyFormNodetree structure. - Form Rendering:
src/index.tspasses theFormNodetree tosrc/renderer.ts. This orchestrator delegates the actual DOM creation tosrc/dom-renderer.ts, which produces the final HTML structure. - Live JSON Output: As the user interacts with the generated form, the application dynamically updates a live JSON output, reflecting the current state of the form data.
Validation
The application uses AJV (Another JSON Schema Validator) for robust, standard-compliant validation.
Architecture
- Initialization: When a schema is parsed in
src/parser.ts, a bundled version (resolving external refs but keeping internal refs to avoid recursion issues) is passed tosrc/validator.ts. - Compilation:
src/validator.tsinitializes anAjv2020instance, registers standard formats (viaajv-formats) and custom formats (e.g.,uint64), and compiles the schema into a validation function. - Mapping: During rendering (
src/renderer.ts), the application builds a registry mapping AJV data paths (JSON pointers like/server/port) to HTML Element IDs. - Real-time Validation:
- When the user modifies the form, the current data is extracted from the DOM.
- The data is passed to the compiled AJV validator.
- If errors occur, they are mapped back to specific DOM elements using the registry, and error messages are displayed inline.
Supported Formats
In addition to standard JSON Schema formats (email, date-time, etc.), the validator supports the following custom integer formats often found in systems like Protobuf or Go:
uint,uint8,uint16,uint32,uint64
Project Structure
.
├── index.html # Main HTML entry point
├── package.json # Project dependencies and scripts
├── schema.json # Example JSON Schema
├── tsconfig.json # TypeScript configuration
├── vite.config.ts # Vite configuration (including polyfills for Node.js modules)
└── src/
├── config.ts # Configuration for sorting, visibility, and heuristics
├── dom-renderer.ts # DOM generation logic using hyperscript
├── i18n.ts # Text overrides and internationalization mappings
├── index.ts # Main application logic, orchestrates parsing and rendering
├── form-data-reader.ts # Logic to read data back from the DOM
├── ui-schema-adapter.ts # Adapter for JSON Forms UI Schemas
├── parser.ts # Parses JSON Schema, dereferences, and transforms to FormNode tree
├── parser.test.ts # Unit tests for the parser
└── renderer.ts # Renders the FormNode tree into HTML form elementsSetup
- Install Dependencies:
npm install - Development Server:
To start the development server and view the application in your browser:
The application will typically be available atnpm run devhttp://localhost:5173/(or a similar port if 5173 is in use).
Troubleshooting
- "Buffer is not defined" or "Module 'path' has been externalized" errors: This project uses Node.js polyfills via
vite-plugin-node-polyfillsto make@apidevtools/json-schema-ref-parsercompatible with browser environments. Ensure this plugin is correctly configured invite.config.tsandbufferandpathare included in itsincludeoption. - 404 errors for
index.html: Ensureindex.htmlis located in the root of your project directory.
Customization
Vanilla Schema Forms allows extensive customization through configuration and custom renderers.
Global Configuration
You can configure global behavior like sorting and visibility using setConfig.
import { setConfig } from "./src/index";
setConfig({
sorting: {
defaultPriority: ["name", "id", "type"], // These keys always appear at the top
defaultRenderLast: ["metadata", "tags"] // These keys always appear at the bottom
},
visibility: {
hiddenPaths: ["root.internalId"], // Hide specific paths
hiddenKeys: ["password"] // Hide keys globally
}
});Custom Renderers
You can override the rendering of specific fields based on their key or path.
import { setCustomRenderers, domRenderer } from "./src/index";
setCustomRenderers({
// Set Custom Renderer: Complete control over the DOM element
"color": {
render: (node, path, elementId, dataPath, context) => {
const input = document.createElement("input");
input.type = "color";
input.id = elementId;
input.value = node.defaultValue || "#000000";
// Use the default wrapper for consistent labeling/layout
return domRenderer.renderFieldWrapper(node, elementId, input);
}
}
});Customization Guidelines & Best Practices
Customizing the rendering logic provides great flexibility but can lead to maintenance issues if not structured correctly.
- Preserve Event Bubbling: The core architecture relies on
changeandinputevents bubbling up from form elements. If you build a custom widget (e.g., adiv-based switch), ensure it dispatches achangeevent or updates a hidden<input>element. Without this, validation and data retrieval will fail. - Use
domRendererHelpers: Avoid re-implementing standard logic. UsedomRenderer.renderFieldWrapperto wrap your custom inputs. This ensures consistent rendering of labels, descriptions, and error placeholders. - Avoid Monolithic Renderers: Don't create a single "Master Renderer" with huge
if/elseblocks. Register small, specific renderers for specific keys or types usingsetCustomRenderers. - Security: Avoid using
innerHTMLto prevent XSS vulnerabilities. Usedocument.createElementor the internalh()hyperscript helper.
Layout Grouping
You can group fields together (e.g., for horizontal layout) using the layout.groups configuration in src/config.ts.
import { setConfig } from "./config";
setConfig({
layout: {
groups: {
// Key is the ID of the parent object (usually the title)
"Connection": [
{
keys: ["host", "port"], // Fields to group
className: "d-flex gap-3", // CSS classes for the container
title: "Endpoint" // Optional title for the group
}
]
}
}
});JSON Forms Adapter
If you prefer using a JSON Forms compatible UI Schema, you can use the built-in adapter.
import { adaptUiSchema } from "./src/index";
const uiSchema = {
type: "HorizontalLayout",
elements: [
{ type: "Control", scope: "#/properties/host" },
{ type: "Control", scope: "#/properties/port" }
]
};
// Applies the UI Schema to the object with ID "Connection"
adaptUiSchema(uiSchema, "Connection");Architectural Vision: Supporting UI Frameworks (React, etc.)
While the current implementation is a lightweight, dependency-free DOM renderer, a primary goal is to evolve the architecture to seamlessly support modern UI frameworks like React. The current approach of mounting framework components inside a vanilla renderer can be complex and inefficient for full-form customizations.
To make this easier and more robust, the plan is to refactor the library into a "headless" core engine with separate, optional renderer packages.
The Proposed Structure:
@vanilla-schema-forms/core: A framework-agnostic engine that handles:- Schema parsing, dereferencing, and transformation into the
FormNodetree. - AJV-based validation.
- State management (data and errors).
- Configuration and UI schema adaptation.
- It will have zero DOM rendering logic.
- Schema parsing, dereferencing, and transformation into the
@vanilla-schema-forms/vanilla-renderer: The default, lightweight renderer (the current implementation), which consumes the core engine to render standard HTML elements.@vanilla-schema-forms/react-renderer: A new package that provides React hooks and components to build forms.- A
useForm()hook that connects to the core engine. - A
<Field />component that can dynamically render the correct input based on theFormNode. - A system for providing a custom set of components (e.g., a Material UI renderer set).
- A
What this means for you:
With this architecture, building a fully custom form with React and Material UI would become straightforward:
// Future-state example
import { VanillaSchemaForm } from '@vanilla-schema-forms/react-renderer';
import { materialRenderers } from '@vanilla-schema-forms/material-renderer';
function MyForm({ schema }) {
return (
<VanillaSchemaForm
schema={schema}
renderers={materialRenderers}
onChange={(data, errors) => console.log(data, errors)}
/>
);
}This approach separates the complex business logic of schema processing from the presentation layer, making the library far more flexible and easier to integrate into any project, while still offering a zero-dependency option for those who need it.
Testing
This project uses Vitest for unit testing.
