@engler/schemaform
v0.1.0
Published
Zero-config, schema-driven form library for JSON Schema with a unified UX paradigm
Maintainers
Readme
schemaform
Zero-config, schema-driven form library for JSON Schema. Transform any JSON Schema into interactive, accessible forms with a unified UX paradigm.
Features
- Zero Configuration — Just provide a JSON Schema, get a working form
- Framework Agnostic — Vanilla TypeScript core with optional React wrapper
- Full JSON Schema Support — Types, formats, validation, composition (
oneOf/anyOf/allOf), conditionals (if/then/else),$ref - Unified UX Patterns — Consistent, predictable layouts for every schema construct
- Accessible — Full keyboard navigation, ARIA labels, screen reader support
- Themeable — CSS custom properties for complete visual customization
- Lightweight — Zero runtime dependencies
Installation
npm install schemaformQuick Start
Vanilla TypeScript
import { SchemaForm } from '@engler/schemaform';
import '@engler/schemaform/styles.css';
const form = new SchemaForm({
schema: {
type: 'object',
properties: {
name: { type: 'string', title: 'Name' },
email: { type: 'string', format: 'email', title: 'Email' },
age: { type: 'integer', title: 'Age', minimum: 0 }
},
required: ['name', 'email']
},
container: document.getElementById('form-container'),
onChange: (value) => console.log('Form value:', value)
});
// Get value
const data = form.getValue();
// Validate
const errors = form.validate();
if (form.isValid()) {
submitForm(data);
}React
import { SchemaForm, useSchemaForm } from '@engler/schemaform/react';
import '@engler/schemaform/styles.css';
function MyForm() {
const { formRef, getValue, validate, isValid, reset } = useSchemaForm();
const [value, setValue] = useState({});
const handleSubmit = () => {
if (isValid()) {
console.log('Submitting:', getValue());
}
};
return (
<>
<SchemaForm
ref={formRef}
schema={{
type: 'object',
properties: {
name: { type: 'string', title: 'Name' },
email: { type: 'string', format: 'email', title: 'Email' }
},
required: ['name', 'email']
}}
value={value}
onChange={setValue}
showErrors="blur"
/>
<button onClick={handleSubmit}>Submit</button>
<button onClick={reset}>Reset</button>
</>
);
}Field Types
String Fields
| Schema | Renders As |
|--------|------------|
| { type: 'string' } | Text input |
| { type: 'string', maxLength: 500 } | Textarea (when > 200) |
| { type: 'string', format: 'textarea' } | Textarea |
| { type: 'string', format: 'email' } | Email input |
| { type: 'string', format: 'uri' } | URL input |
| { type: 'string', format: 'date' } | Date picker |
| { type: 'string', format: 'time' } | Time picker |
| { type: 'string', format: 'datetime-local' } | Datetime picker |
| { type: 'string', format: 'color' } | Color picker |
| { type: 'string', format: 'password' } | Password input |
| { type: 'string', enum: [...] } | Select dropdown |
| { type: 'string', enum: [...], format: 'radio' } | Radio buttons |
| { type: 'string', const: 'value' } | Read-only display |
Number Fields
| Schema | Renders As |
|--------|------------|
| { type: 'number' } | Number input |
| { type: 'integer' } | Number input (step=1) |
| { type: 'number', format: 'range', minimum: 0, maximum: 100 } | Range slider |
| { type: 'number', enum: [...] } | Select dropdown |
Boolean Fields
{ "type": "boolean", "title": "Accept Terms" }Renders as a checkbox.
Array Fields
Array of Objects (Tabs Layout)
{
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" }
}
}
}Renders as a two-panel layout with tabs on the left and content on the right. Supports add, delete, and reorder operations.
Array of Enum Strings (Checkboxes)
{
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"enum": ["red", "green", "blue"]
}
}Renders as a checkbox group. Use format: 'select' for a multi-select dropdown instead.
Array Layout Formats
| Format | Description |
|--------|-------------|
| tabs | Two-panel tabs layout (default for objects) |
| list | Stacked cards layout |
| table | Table rows layout |
| checkbox | Checkbox group (for enum arrays) |
| select | Multi-select dropdown (for enum arrays) |
Nested Objects
{
"type": "object",
"properties": {
"address": {
"type": "object",
"title": "Address",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" }
}
}
}
}Nested objects render as collapsible fieldsets.
Schema Composition
oneOf / anyOf
{
"title": "Payment Method",
"oneOf": [
{
"title": "Credit Card",
"type": "object",
"properties": {
"cardNumber": { "type": "string" },
"expiry": { "type": "string" }
}
},
{
"title": "PayPal",
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" }
}
}
]
}Renders a dropdown selector to switch between variants, with the active variant's form below.
Polymorphic Arrays
For arrays with oneOf/anyOf items, an "Add" dropdown lets users choose which variant to add:
{
"type": "array",
"items": {
"oneOf": [
{ "title": "Text Block", "properties": { "content": { "type": "string" } } },
{ "title": "Image Block", "properties": { "url": { "type": "string" } } }
]
}
}Conditional (if/then/else)
{
"type": "object",
"properties": {
"employmentType": { "type": "string", "enum": ["employed", "self-employed"] },
"companyName": { "type": "string" },
"businessName": { "type": "string" }
},
"if": { "properties": { "employmentType": { "const": "employed" } } },
"then": { "required": ["companyName"] },
"else": { "required": ["businessName"] }
}Nullable Fields
{
"type": ["string", "null"],
"title": "Middle Name"
}Renders with a "Set to null" toggle.
Validation
Supported Constraints
- String:
minLength,maxLength,pattern,format - Number:
minimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOf - Array:
minItems,maxItems,uniqueItems - Object:
required,minProperties,maxProperties - General:
type,enum,const
Validation Timing
new SchemaForm({
schema,
container,
showErrors: 'blur' // 'blur' | 'change' | 'submit' | 'never'
});| Mode | Behavior |
|------|----------|
| blur | Validate when field loses focus (default) |
| change | Validate on every change |
| submit | Only on explicit validate() call |
| never | No automatic error display |
Custom Error Messages
Per-Field
{
"type": "string",
"minLength": 5,
"errorMessage": {
"minLength": "Username must be at least 5 characters"
}
}Global
new SchemaForm({
schema,
container,
messages: {
required: 'This field is required',
minLength: 'Too short (minimum {minLength} characters)'
}
});API Reference
SchemaForm Constructor
interface SchemaFormOptions {
schema: JSONSchema; // Required: JSON Schema definition
container: HTMLElement; // Required: DOM element to render into
value?: unknown; // Initial form value
onChange?: (value: unknown) => void;
onValidation?: (errors: ValidationError[]) => void;
showErrors?: 'blur' | 'change' | 'submit' | 'never';
disabled?: boolean;
readOnly?: boolean;
refs?: Record<string, JSONSchema>; // Pre-resolved $ref schemas
messages?: Record<string, string>; // Custom error messages
}
const form = new SchemaForm(options);Instance Methods
| Method | Description |
|--------|-------------|
| getValue() | Get current form value |
| setValue(value) | Set form value |
| validate() | Validate and return errors array |
| isValid() | Check if form is valid |
| reset() | Reset to initial value |
| clear() | Clear all values |
| enable() | Enable form editing |
| disable() | Disable form editing |
| focus(path?) | Focus field at path (or first field) |
| getEditor(path) | Get editor instance at path |
| destroy() | Cleanup and remove form |
Events
// Subscribe
const unsubscribe = form.on('change', (value) => { ... });
form.on('validate', (errors) => { ... });
form.on('ready', () => { ... });
// Unsubscribe
unsubscribe();React Props
interface SchemaFormProps {
schema: JSONSchema;
value?: unknown;
onChange?: (value: unknown) => void;
onValidation?: (errors: ValidationError[]) => void;
onReady?: () => void;
showErrors?: 'blur' | 'change' | 'submit' | 'never';
disabled?: boolean;
readOnly?: boolean;
refs?: Record<string, JSONSchema>;
messages?: Record<string, string>;
className?: string;
style?: React.CSSProperties;
}React Hooks
// Full imperative control
const { formRef, getValue, setValue, validate, isValid, reset, clear, focus } = useSchemaForm();
// With local state management
const { formRef, value, onChange, ...methods } = useSchemaFormValue(initialValue);Customization
Custom Extensions
| Keyword | Purpose |
|---------|---------|
| propertyOrder | Control property display order |
| headerTemplate | Template for array item headers ({{property}}) |
| options.collapsed | Start collapsed |
| options.hidden | Hide field from UI |
| options.enum_titles | Display labels for enum values |
| options.disable_array_add | Hide Add button |
| options.disable_array_delete | Hide Delete buttons |
| options.disable_array_reorder | Hide Move/reorder controls |
Theming with CSS Variables
:root {
/* Colors */
--sf-color-primary: #0066cc;
--sf-color-error: #dc3545;
--sf-color-border: #dee2e6;
--sf-color-background: #ffffff;
--sf-color-text: #212529;
/* Typography */
--sf-font-family: system-ui, -apple-system, sans-serif;
--sf-font-size-base: 0.9375rem;
/* Spacing */
--sf-spacing-sm: 0.5rem;
--sf-spacing-md: 1rem;
/* Borders */
--sf-border-radius: 0.375rem;
}Dark Mode
[data-theme="dark"] {
--sf-color-background: #1a1a1a;
--sf-color-text: #f0f0f0;
--sf-color-border: #404040;
}$ref Support
{
"$defs": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" }
}
}
},
"type": "object",
"properties": {
"homeAddress": { "$ref": "#/$defs/address", "title": "Home" },
"workAddress": { "$ref": "#/$defs/address", "title": "Work" }
}
}Supports local references (#/$defs/..., #/definitions/...) and nested refs.
Browser Support
- Chrome/Edge 88+
- Firefox 78+
- Safari 14+
Requires ES2020 support.
License
MIT © FoxFlow
