vanilla-schema-forms
v0.1.8
Published
A JSON schema driven form generator for vanilla JS/TS applications.
Downloads
252
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.
Web Awesome Helper
If you want a Web Awesome-based variant of the vanilla renderer, you can layer the helper on top of the existing DOM renderer instead of rewriting everything from scratch.
import {
applyWebAwesomeTheme,
createAdvancedOptionsRenderer,
createOptionalRenderer,
createTypeSelectArrayRenderer,
setCustomRenderers
} from "vanilla-schema-forms";
applyWebAwesomeTheme();
setCustomRenderers({
connection: createAdvancedOptionsRenderer(["protocol", "host", "port"]),
tls: createOptionalRenderer("enabled"),
middlewares: createTypeSelectArrayRenderer({
buttonLabel: "Add Middleware",
itemLabel: "Middleware"
})
});See examples/vanilla-web-awesome/ for a complete working example that uses the official Web Awesome CDN assets.
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.
