@gluonjs/json-forms
v1.14.0
Published
Schema-driven, form-associated Custom Elements for Gluon.
Readme
@gluonjs/json-forms
@gluonjs/json-forms turns a deliberately documented subset of JSON Schema and
JSON Forms UI schema into one accessible, form-associated Web Component. It is
an optional package: schema rendering and AJV validation do not enter
@gluonjs/core.
@gluonjs/json-forms at a glance
Runtime: browser · Release: 1.14.0
Documentation guide · npm · Source
Public API: @gluonjs/json-forms
Install
npm install @gluonjs/json-formsQuick start
import { registerJsonForms } from '@gluonjs/json-forms';
registerJsonForms();Choose this package when
- Accessible form rendering from a documented JSON Schema subset.
- AJV-backed validation and form-associated custom elements.
- Optional schema-driven UI inside browser applications.
Choose another boundary when:
- Does not enter the core package runtime.
- Schema rendering is intentionally limited to the documented subset.
Related documentation
Stability notes
The package ships as part of the current 1.14.0 release line. Its documented
schema and UI schema subset, host-owned lifecycle, and renderer-registry
boundary are stable; broader JSON Schema features remain unsupported unless a
later contract adds them.
import {
registerJsonForms,
resolveJsonSchema,
type JsonFormsElement,
type JsonSchema,
} from '@gluonjs/json-forms';
const schema = {
type: 'object',
title: 'Delivery preference',
properties: {
email: { $ref: '#/$defs/email' },
time: { type: 'string', enum: ['morning', 'afternoon'], enumNames: ['Morning', 'Afternoon'] },
giftWrap: { type: 'boolean', title: 'Add gift wrap', default: false },
},
required: ['email', 'time'],
$defs: {
email: { type: 'string', format: 'email', title: 'Email address' },
},
} satisfies JsonSchema;
registerJsonForms();
const form = document.querySelector<JsonFormsElement>('gluon-json-form')!;
form.schema = resolveJsonSchema(schema, { maxDepth: 8, maxNodes: 64 });
form.data = { email: '[email protected]' };
form.addEventListener('change', (event) => {
console.log(event.detail.data, event.detail.errors);
});<form>
<gluon-json-form name="delivery"></gluon-json-form>
<button>Save delivery preference</button>
</form>The element serializes its current JSON object to the outer form under name,
uses ElementInternals for native validity, supports reset and state restore,
and dispatches change with frozen { data, errors }. validation-change
dispatches only when the validation result changes. The application owns the
authoritative data: update .data after a change event when the surrounding
state store accepts or transforms the edit.
Message provider
JSON Forms infrastructure copy is owned by a synchronous message provider. The
package exports createJsonFormsMessageProvider() and typed provider
interfaces so applications can swap locale-aware strings without importing
@gluonjs/i18n or making network requests. The provider covers the root form
label, array item numbering, add/remove controls, selection placeholders,
validation diagnostics, and configuration diagnostics.
import {
createJsonFormsMessageProvider,
registerJsonForms,
type JsonFormsMessageProvider,
} from '@gluonjs/json-forms';
const messages: JsonFormsMessageProvider = createJsonFormsMessageProvider({
locale: 'de-DE',
messages: {
selectPlaceholder: (required, locale) =>
required ? `Bitte auswählen (${locale})` : `Keine Auswahl (${locale})`,
},
});
const form = document.querySelector('gluon-json-form')!;
form.messages = messages;createJsonFormsMessageProvider() always falls back to the built-in English
defaults when a caller omits a specific override. Formatter overrides may
return undefined or null to leave a key unresolved; the provider then uses
the English fallback for that key. Validation formatters receive immutable AJV
keyword params, including numeric limits and missing-property names. Locale-
aware number formatting comes from Intl.NumberFormat, so item numbering and
validation thresholds can follow the active locale without depending on the
i18n package. The provider does not translate application-authored titles or
descriptions. Object and array validation messages are associated with their
own fieldset through aria-describedby, aria-errormessage, and
aria-invalid, just as primitive fields associate their controls.
The maintained docs-site/examples/json-forms.html application installs the
German provider in a real delivery-preferences form. Run
npm run build:docs-examples && npm run check:json-forms-example-browser to
verify localized validation and configuration diagnostics, long array-control
labels, the custom lead-time renderer, host-owned form state, 390px overflow,
and 44px controls in Chromium.
Renderer registry
createJsonFormsRendererRegistry() lets an application replace a supported
field control without forking JsonFormsElement. Registrations are synchronous
and request-free. A declarative selector may match field kind, JSON Schema
schemaType or format, and an exact data path. Higher integer priorities
win. Duplicate IDs, unsupported properties, invalid selectors, and overlapping
selectors at the same priority throw an actionable TypeError when the
registry is created, so selection never depends on registration timing.
import { html } from '@gluonjs/core';
import {
createJsonFormsRendererRegistry,
type JsonFormsElement,
} from '@gluonjs/json-forms';
const renderers = createJsonFormsRendererRegistry([{
id: 'quantity-stepper',
selector: { kind: 'number', path: ['quantity'] },
priority: 10,
render: (context) => {
const value = typeof context.value === 'number' ? context.value : 1;
return html`
<button
type="button"
aria-labelledby=${context.control.labelId}
?disabled=${context.disabled || context.readOnly}
@click=${() => context.control.commit(value + 1)}
>Increase</button>
<output
id=${context.control.id}
aria-labelledby=${context.control.labelId}
aria-describedby=${context.control.describedBy}
>${value}</output>
`;
},
}]);
document.querySelector<JsonFormsElement>('gluon-json-form')!.rendererRegistry = renderers;The frozen renderer context exposes the public field, resolved field schema,
field and root UI-schema context, exact path, current data/value, validation
errors, message provider, effective disabled/read-only state, and stable label,
description, and error IDs. context.control.commit() is the only registry
mutation boundary: the host still owns immutable data updates, AJV validation,
native form value/validity, reset, restore, and exactly one change and
validation-change lifecycle. Invalid non-JSON commits fail closed. Renderers
must connect their native control to the provided label and description IDs;
they must not dispatch replacement form events.
If no selector matches, the documented built-in renderer remains the fallback
for text, number, boolean, select, object, and bounded-array fields. Custom
controls are wrapped by stable part="field", part="control custom-control",
data-gluon-json-renderer, validation, and ARIA state hooks. Applications may
return a separately styled Gluon component or custom element when the generic
native control styles are insufficient; the registry does not inject product
themes, requests, authorization, persistence, or remote plugins.
Registry functions are deliberately not serialized. Deterministic SSR and hydration require the application to provide the same immutable registry while rendering and hydrating the element. Field selection depends only on the declarative selector, priority, and schema context, so the server and browser choose the same renderer.
Supported schema boundary
The renderer accepts a root schema with type: "object", primitive fields of
type string, number, integer, or boolean, string/number enum, nested
objects through properties, and arrays with one supported items schema.
Nested arrays are rejected so item editing remains bounded and addressable. It
supports title, description, default, required, minLength,
maxLength, minimum, maximum, minItems, maxItems, format: "email",
additionalProperties, field-level readOnly, and enumNames.
An optional JSON Forms VerticalLayout may order root or nested Control
elements and supply their labels or options.enumNames. Object fields render
as fieldsets. Array fields expose 44px Add/Remove controls and preserve the
same immutable change event, native form value, validation, reset, and state
restore contracts as direct fields. Native labels, keyboard controls, focus
indicators, error association, disabled state, and reduced motion behavior are
built into the element.
Local $ref accepts only fragment JSON Pointers whose first RFC 6901-decoded
token is $defs; the legacy definitions token is also accepted explicitly
for compatibility. Deeper own-property paths below either root container are
supported, including escaped / (~1) and ~ (~0) tokens. URI/remote,
anchor, inherited-property, and pointers outside those two containers are not
resolved. Definition containers are lookup-only and are omitted from the
normalized output.
References are resolved synchronously with a default maximum reference-chain
depth of 16 and a maximum of 256 visited schema objects. Call
resolveJsonSchema(schema, { maxDepth, maxNodes }) to use lower application
limits. The function never mutates or retains caller-owned arrays or objects;
its result is a null-prototype, deeply frozen clone. Keywords beside $ref
form a shallow overlay and take precedence when the target defines the same
keyword. Remote/URI references, malformed or missing
pointers, cycles, and limit overflow produce stable configuration diagnostics
(ref-remote, ref-pointer, ref-target, ref-cycle, ref-depth, or
ref-budget). A
resolved target still has exactly the supported field and validation boundary
above; $ref does not enable composition or arbitrary JSON Schema.
Conditional/composition keywords, JSON Forms rules, async schemas, file widgets,
nested arrays, and UI layouts other than VerticalLayout remain unsupported. An unsupported schema
or UI schema renders an explicit configuration error rather than silently
dropping fields, and that configuration copy also flows through the message
provider.
Historical delivery decision
The direct-property component was the first usable package slice identified in issue #256 and delivered in #257. Nested object and bounded array support is delivered in #377; the remaining unsupported capabilities above still require separate contracts and browser evidence.
