Native Lit / web-component binding for JSON Forms.
JSON Forms ships official bindings for React, Angular, and Vue. This repository adds a binding for Lit so that JSON Forms can be embedded in any web-component based application with no React/Vue/Angular dependency.
The binding reuses @jsonforms/core unchanged: state management, AJV
validation, ranked-tester dispatch, and the state-to-props mappers all come
from the upstream core package. Only the framework-specific glue (store
hosting, context distribution, dispatch element, reactive controllers) lives
here.
| Package | Description |
|---|---|
@jsonforms-lit/core |
Framework infrastructure: <json-forms> element, <jf-dispatch-renderer>, JsonFormsStore, @lit/context definitions, reactive controllers. |
@jsonforms-lit/vanilla-renderers |
Unstyled renderer set: text / number / boolean / date / enum controls, vertical / horizontal / group layouts, array & object renderers. |
apps/demo |
Vite-powered runnable demo that wires the two packages to a handful of example schemas. |
pnpm install
pnpm -r build
pnpm dev # starts the demo app on http://localhost:5173Both
import '@jsonforms-lit/core'andimport '@jsonforms-lit/vanilla-renderers'are side-effect imports — each module's top-level code callscustomElements.define(...)to register its tags. Include them both at least once in your app entry. Without them,document.createElement('json-forms')returns a genericHTMLElementand nothing renders.
import '@jsonforms-lit/core';
import '@jsonforms-lit/vanilla-renderers';
import { vanillaRenderers } from '@jsonforms-lit/vanilla-renderers';
const el = document.createElement('json-forms');
el.schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer', minimum: 0 },
},
required: ['name'],
};
el.data = { name: 'Alice', age: 30 };
el.renderers = vanillaRenderers;
el.addEventListener('change', (e) => console.log(e.detail.data));
document.body.appendChild(el);JsonFormsStorewraps the corecoreReducerwith a tiny pub/sub. It is the single source of truth for data, schema, uischema, and validation errors.<json-forms>provides the store and renderer registry via@lit/contextso every descendant (even across shadow DOM boundaries) can pull state without prop drilling.<jf-dispatch-renderer>evaluates every registeredRankedTesterand imperatively instantiates the highest-ranked renderer by tag name, caching the resulting element so data updates don't rebuild the DOM subtree.- Reactive controllers (
ControlController,LayoutController,ArrayController) subscribe to the store on behalf of renderer elements and expose ready-to-useControlProps/LayoutPropsby callingmapStateToControlProps/mapStateToLayoutPropsfrom@jsonforms/core.
Three authoring styles are supported. All three use the same dispatch mechanism — pick whichever fits your scope.
- Reactive controller — the most flexible path. A single element can
combine
ControlControllerwith, say, anI18nControlleror aValidationControllerbecause reactive controllers compose without the multiple-inheritance headache of class-based bases. Seepackages/vanilla-renderers/src/base/VanillaControlBase.tsfor a real-world example. ControlMixin/LayoutMixin— a drop-in ergonomic mixin that instantiates the controller for you and copies the mapped props onto the host as reactive@statefields. Templates stay terse:<input .value=${this.controlData}>. Trade-off: you lose the compositional flexibility of multiple controllers per element.- Manual store access — if you need to bypass the helpers, consume
jsonFormsStoreContextdirectly via@lit/contextand callmapStateToControlPropsyourself. This is the same code path the controllers use under the hood.
See packages/core/README.md for copy-paste
examples of each style.
Once your element class is defined, register it via:
import { registerRenderer } from '@jsonforms-lit/core';
import { rankWith, isStringControl } from '@jsonforms/core';
export const myEntry = registerRenderer(
'my-text-control',
rankWith(1, isStringControl),
MyTextControl,
);
// Then in your app:
form.renderers = [myEntry, ...vanillaRenderers];The ranked-tester dispatch prefers the highest score, so a rank of 10
will always win against the vanilla pack's rank 1–3.
pnpm install
pnpm typecheck
pnpm -r build
pnpm test
pnpm devLibrary packages (core, vanilla-renderers) emit plain ESM via tsc.
The demo app is built with Vite. All tests run on node:test via tsx,
with a happy-dom shim installed before any Lit module is loaded — so
real custom-element upgrades and @lit/context event propagation work
without a browser harness.
Test coverage highlights:
JsonFormsStore: init, dispatch, subscribe, unsubscribe, validation flow, readonly / config helpers (9 tests).- Controllers:
ControlController(5),LayoutController(3),ArrayController(3) — including addItem / removeItems coverage. <jf-dispatch-renderer>: tester ranking, unknown-renderer fallback, element caching across data updates, tag swap on uischema change (4 tests).<json-forms>integration: context propagation across shadow DOM boundaries, user-input round-trip through the store, AJV error surfacing, externaldatareset, internal-vs-external update distinction (5 tests).- Mixins:
ControlMixinpopulates label/data/required/errors, forwards edits viahandleChange;LayoutMixinexposeslayoutElements/layoutDirectionand renders child dispatchers (5 tests). registerRenderer: idempotent custom-element definition, registry entry returned (2 tests).- Vanilla renderers: text, boolean, number, integer, enum, vertical layout, array add/remove/render, object renderer, group layout, tabbed categorization, multi-line, date (17 tests).
Run the full suite from the workspace root:
pnpm -r test # 36 core + 17 vanilla-renderers = 53 testsMIT