An implementation of JsonForms built with lit and Web Awesome.
A Json Form allows you to create forms using only JSON. Every form needs two schemas to function:
All examples on this page will console-log their value every time they change.
export const simple = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value, ev)}
.schema=${{
type: 'object',
properties: {
name: { type: 'string', description: 'Please enter your name' },
age: { type: 'integer', description: 'Please enter your age' },
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/name' },
{ type: 'Control', scope: '#/properties/age' },
],
}}
></json-form>`;
Add options.autofill to a text control to keep free-text entry while offering common values.
Use value for a single field or fill to update several form paths atomically. Combining
multi: true with autofill renders a textarea.
export const autofill = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
iban: { type: 'string', title: 'IBAN' },
bic: { type: 'string', title: 'BIC' },
accountHolder: { type: 'string', title: 'Account holder' },
internalNote: { type: 'string', title: 'Internal note' },
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/iban',
options: {
autofill: [
{
label: 'Main account · Example Bank',
fill: {
'#/properties/iban': 'AT12 3456 7890',
'#/properties/bic': 'EXAMPLEAT',
'#/properties/accountHolder': 'Jane Doe',
},
},
],
},
},
{ type: 'Control', scope: '#/properties/bic' },
{ type: 'Control', scope: '#/properties/accountHolder' },
{
type: 'Control',
scope: '#/properties/internalNote',
options: {
multi: true,
autofill: [
{
label: 'Follow-up required',
value: 'Please follow up with the customer.',
},
],
},
},
],
}}
.value=${{}}
></json-form>`;
These are the default Json-Form controls:
export const defaultControls = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
string: {
type: 'string',
},
boolean: {
type: 'boolean',
description: 'Boolean description as a tooltip',
},
number: {
type: 'number',
},
integer: {
type: 'integer',
},
date: {
type: 'string',
format: 'date',
},
time: {
type: 'string',
format: 'time',
},
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/string',
},
{
type: 'Control',
scope: '#/properties/boolean',
},
{
type: 'Control',
scope: '#/properties/number',
},
{
type: 'Control',
scope: '#/properties/integer',
},
{
type: 'Control',
scope: '#/properties/date',
},
{
type: 'Control',
scope: '#/properties/time',
},
],
}}
.value=${{
string: 'This is a string',
boolean: true,
number: 50.5,
integer: 50,
date: '2020-06-25',
}}
></json-form>`;
Supported schema types:
Supported schema formats (type string):
The ArrayLayout renders one card per entry of an array property, with buttons to add and remove entries.
It takes a scope pointing to the array property and a single layout in elements that is repeated for every entry.
Scopes inside that layout are written as if they pointed at the array itself — the entry index is inserted automatically.
Supported options:
hidePlus: {true} hides the add buttonhideTrash: {true} hides the remove buttonsexport const array = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
string: { type: 'string' },
array: {
type: 'array',
minItems: 3,
items: {
type: 'object',
properties: { string: { type: 'string' }, number: { type: 'number' } },
},
},
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/string' },
{
type: 'ArrayLayout',
label: 'Array',
scope: '#/properties/array',
elements: {
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/array/properties/string' },
{ type: 'Control', scope: '#/properties/array/properties/number' },
{ type: 'Control', scope: '#/properties/string' },
],
},
},
],
}}
.value=${{
string: 'foobar',
array: [{ string: 'foo' }, { string: 'bar', number: 5 }],
}}
>
</json-form>`;
There are two types that will produce multiple-choice inputs, enum and oneOf.
const, and the visible text as titleexport const enumOneOf = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
enum: {
type: 'string',
enum: ['One', 'Two', 'Three'],
},
typeOneOf: {
type: 'integer',
title: 'oneOf',
oneOf: [
{ const: 1, title: 'One' },
{ const: 2, title: 'Two' },
{ const: 3, title: 'Three' },
],
},
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/enum',
},
{
type: 'Control',
scope: '#/properties/typeOneOf',
},
],
}}
></json-form>`;
If you want to be able to select multiple options at once, make the property an array and set the items to enum or oneOf.
export const multiselect = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
enum: {
type: 'array',
items: {
type: 'string',
enum: ['One', 'Two', 'Three'],
},
},
typeOneOf: {
type: 'array',
title: 'oneOf',
items: {
type: 'integer',
oneOf: [
{ const: 1, title: 'One' },
{ const: 2, title: 'Two' },
{ const: 3, title: 'Three' },
],
},
},
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/enum',
},
{
type: 'Control',
scope: '#/properties/typeOneOf',
},
],
}}
></json-form>`;
export const options = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
toggle: {
type: 'boolean',
},
checkboxTag: {
type: 'boolean',
},
tagVariant: {
type: 'boolean',
},
size: {
type: 'boolean',
},
slider: {
type: 'number',
minimum: 1,
maximum: 50,
},
rating: {
type: 'number',
minimum: 0,
maximum: 5,
},
multi: {
type: 'string',
},
radio: {
type: 'string',
enum: ['One', 'Two', 'Three'],
},
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/toggle',
options: {
toggle: true,
},
},
{
type: 'Control',
scope: '#/properties/checkboxTag',
options: {
checkboxTag: true,
},
},
{
type: 'Control',
scope: '#/properties/tagVariant',
options: {
checkboxTag: true,
tagVariant: 'success',
},
},
{
type: 'Control',
scope: '#/properties/size',
label: 'Large Checkbox',
options: {
size: 'large',
},
},
{
type: 'Control',
scope: '#/properties/slider',
options: {
slider: true,
},
},
{
type: 'Control',
scope: '#/properties/rating',
options: {
rating: true,
},
},
{
type: 'Control',
scope: '#/properties/multi',
options: {
multi: true,
},
},
{
type: 'Control',
scope: '#/properties/radio',
options: {
format: 'radio',
},
},
],
}}
></json-form>`;
Supported uiSchema options:
{true} for booleans: renders checkbox as a toggle{true} for booleans: renders the checkbox in a tag{"brand" | "success" | "warning" | "danger" | "neutral"} for booleans: the color for the checkbox tag{"small" | "medium" | "large"} for booleans: renders the checkbox in a different size{true} for numbers: renders a slider. You can set the boundaries with the minimum and maximum properties in the normal schema.{true} for numbers: renders a star-rating. You can set the boundaries with the minimum and maximum properties in the normal schema.{true} for strings: renders a textarea instead of an input{"radio"} for single-select enums: renders a radio groupTo structure your forms, you can put your controls into different layouts. A layout is an entry in the uiSchema with type set to ...Layout and a list of elements.
export const vertical = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
name: { type: 'string', description: 'Please enter your name' },
age: { type: 'integer', description: 'Please enter your age' },
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/name' },
{ type: 'Control', scope: '#/properties/age' },
],
}}
></json-form>`;
export const horizontal = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
name: { type: 'string', description: 'Please enter your name' },
age: { type: 'integer', description: 'Please enter your age' },
},
}}
.uiSchema=${{
type: 'HorizontalLayout',
elements: [
{ type: 'Control', scope: '#/properties/name' },
{ type: 'Control', scope: '#/properties/age' },
],
}}
></json-form>`;
A group layout is like a vertical layout, but it has a visual separator and an additional label property.
export const group = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
name: { type: 'string', description: 'Please enter your name' },
age: { type: 'integer', description: 'Please enter your age' },
},
}}
.uiSchema=${{
type: 'GroupLayout',
label: 'Group Layout',
elements: [
{ type: 'Control', scope: '#/properties/name' },
{ type: 'Control', scope: '#/properties/age' },
],
}}
></json-form>`;
Elements in Layouts can also be other layouts
export const nested = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
name: { type: 'string', description: 'Please enter your name' },
age: { type: 'integer', description: 'Please enter your age' },
},
}}
.uiSchema=${{
type: 'GroupLayout',
label: 'Nested Layout',
elements: [
{
type: 'HorizontalLayout',
elements: [
{
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/name' },
{ type: 'Control', scope: '#/properties/age' },
],
},
{
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/name' },
{ type: 'Control', scope: '#/properties/age' },
],
},
],
},
],
}}
></json-form>`;
A horizontal layout meant for checkboxes with tags that adds a 'select-all' checkbox
export const checkboxCombo = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
size: {
type: 'object',
properties: {
small: { type: 'boolean' },
medium: { type: 'boolean' },
large: { type: 'boolean' },
},
},
},
}}
.uiSchema=${{
type: 'CheckboxComboLayout',
elements: [
{
type: 'Control',
scope: '#/properties/size/properties/small',
options: {
checkboxTag: true,
tagVariant: 'neutral',
},
},
{
type: 'Control',
scope: '#/properties/size/properties/medium',
options: {
checkboxTag: true,
tagVariant: 'brand',
},
},
{
type: 'Control',
scope: '#/properties/size/properties/large',
options: {
checkboxTag: true,
tagVariant: 'success',
},
},
],
}}
></json-form>`;
A layout for grouping forms into tabs.
export const tabs = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
name: { type: 'string', description: 'Please enter your name' },
age: { type: 'integer', description: 'Please enter your age' },
},
}}
.uiSchema=${{
type: 'GroupLayout',
label: 'Tab Layout',
elements: [
{
type: 'TabLayout',
options: {
tabNames: ['foo', 'bar'],
},
elements: [
{
type: 'VerticalLayout',
elements: [{ type: 'Control', scope: '#/properties/name' }],
},
{
type: 'VerticalLayout',
elements: [{ type: 'Control', scope: '#/properties/age' }],
},
],
},
],
}}
></json-form>`;
A layout for hiding forms.
export const details = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
street: { type: 'string' },
nr: { type: 'string' },
city: { type: 'string' },
postalCode: { type: 'string' },
},
}}
.uiSchema=${{
type: 'GroupLayout',
label: 'Details Layout',
elements: [
{
type: 'DetailsLayout',
label: 'My Address',
options: {
formatter: '{street} {nr}, {postalCode} {city}',
formatterScope: '#/properties',
},
subLayout: {
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/street' },
{ type: 'Control', scope: '#/properties/nr' },
{ type: 'Control', scope: '#/properties/postalCode' },
{ type: 'Control', scope: '#/properties/city' },
],
},
},
],
}}
.value=${{
street: 'Broadway',
nr: '2',
postalCode: '10000',
city: 'New York',
}}
></json-form>`;
The third type of uiSchema elements is labels. Labels can be used to divide the form into multiple parts, while still keeping it linear. Controls can also be given labels. If a control has no label, it will try to generate it from the property name.
export const labels = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value, ev)}
.schema=${{
type: 'object',
properties: {
name: { type: 'string', description: 'Please enter your name' },
age: { type: 'integer', description: 'Please enter your age' },
fruit: {
type: 'string',
oneOf: [
{ const: 'apple', title: 'Apples' },
{ const: 'orange', title: 'Oranges' },
],
},
},
}}
.uiSchema=${{
type: 'GroupLayout',
label: 'Layout Label',
elements: [
{ type: 'Control', scope: '#/properties/name', label: 'Name: also this is a custom label' },
{ type: 'Control', scope: '#/properties/fruit', label: 'Your favorite fruit' },
{ type: 'Label', scope: '#/properties/fruit' },
{ type: 'Label', text: 'Are you old enough?' },
{ type: 'Control', scope: '#/properties/age' },
],
}}
></json-form>`;
You can add a separator by using type separator.
export const separator = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value, ev)}
.schema=${{
type: 'object',
properties: {
name: { type: 'string', description: 'Please enter your name' },
age: { type: 'integer', description: 'Please enter your age' },
},
}}
.uiSchema=${{
type: 'GroupLayout',
label: 'Layout Label',
elements: [
{ type: 'Control', scope: '#/properties/name', label: 'Name: also this is a custom label' },
{ type: 'Separator', options: { type: 'horizontal', label: 'I am a Separator' } },
{ type: 'Control', scope: '#/properties/age' },
],
}}
></json-form>`;
Controls and layouts can be shown, hidden, enabled and disabled dynamically with
rules. A rule has an effect (SHOW, HIDE, ENABLE or DISABLE)
and a condition with a scope pointing at a property of the form value and a schema that is validated
against that property. The effect is applied while the condition matches.
export const rules = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
employed: { type: 'boolean' },
employer: { type: 'string' },
salary: { type: 'number' },
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/employed', options: { toggle: true } },
{
type: 'Control',
scope: '#/properties/employer',
rule: {
effect: 'SHOW',
condition: { scope: '#/properties/employed', schema: { const: true } },
},
},
{
type: 'Control',
scope: '#/properties/salary',
rule: {
effect: 'ENABLE',
condition: { scope: '#/properties/employed', schema: { const: true } },
},
},
],
}}
></json-form>`;
The form value is validated against the schema on every change. Errors are shown on a control
once the user has interacted with it — set forceErrors to show all errors immediately
(e.g. when the user tries to submit an untouched form).
export const validation = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.validatorState)}
forceErrors
.schema=${{
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string', minLength: 5 },
},
required: ['name'],
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/name' },
{ type: 'Control', scope: '#/properties/email' },
],
}}
.value=${{ email: 'a@b' }}
></json-form>`;
The form element also exposes:
validatorState: {ValidationResult} the current validation result ({valid, errors}). Managed by the form — read only.validate() re-runs the validation and updates validatorState.getFirstInvalid(): {Element | undefined} returns the first form element with an invalid control, e.g. to scroll it into view on submit.Note: empty strings, null and empty objects are stripped from the value before validation, so an
empty required text field reports a "required" error instead of a type error.
Set the readonly attribute to disable all controls of a form. ArrayLayout add/remove buttons are hidden as well.
export const readonly = () =>
html`<json-form
readonly
.schema=${{
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' },
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/name' },
{ type: 'Control', scope: '#/properties/age' },
],
}}
.value=${{ name: 'James Bond', age: 42 }}
></json-form>`;
Every time the user changes the form, a formDataChange event is fired on the json-form element.
The current change is accessible via the value and path properties, but you probably want the full form value,
which is in the value property of the form element (which is the event target).
export const events = () =>
html`<json-form
@formDataChange=${ev =>
console.log(
`Changed property: ${ev.path}, New value: ${ev.value}, New form value:`,
ev.target.value,
)}
.schema=${{
type: 'object',
properties: {
name: { type: 'string' },
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [{ type: 'Control', scope: '#/properties/name' }],
}}
></json-form>`;
You can switch to clickEditable renderers midway through a uiSchema.
export const switchRenderers = () =>
html`<json-form
@formDataChange=${ev =>
console.log(
`Changed property: ${ev.path}, New value: ${ev.value}, New form value:`,
ev.target.value,
)}
.schema=${{
type: 'object',
properties: {
name: { type: 'string' },
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{ type: 'VerticalLayout', elements: [{ type: 'Control', scope: '#/properties/name' }] },
{
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/name' },
{ type: 'VerticalLayout', elements: [{ type: 'Control', scope: '#/properties/name' }] },
],
options: { renderers: 'clickEditable' },
},
],
}}
></json-form>`;
Next to schema and uiSchema, Json-Form also supports these properties:
value: {any}, allows you to set the value of the entire form.forceErrors: {boolean}, makes the form emit errors for incorrect fields, even if the user has not touched them yet.readonly: {boolean}, disables all controls, see Readonly.mode: {'form' | 'schema'}, schema renders the field types instead of inputs, see Displaying schemas.renderers: {Record<string, Renderer>}, allows you to overwrite renderers for certain types, see the next chapter.The rootForm and validatorState properties are managed internally by the form — don't set them.
Changing the renderers is still experimental and not very well supported. The default render types are:
checkboxTagdatetimedatetimestringbooleannumberintegerenummultiEnumIf you set one of these, it will overwrite the existing renderer. Otherwise, a new one will be added. You can then
use your new renderer by setting the name of your render type to true in the options of the uiSchema.
import { inputListener, resolveDataSchema, processLabel } from '@open-wc/components/JsonForm.js';
export const customize = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
name: { type: 'string' },
acceptTerms: { type: 'boolean', description: 'Accept Terms?' },
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/name' },
{ type: 'Control', scope: '#/properties/acceptTerms', options: { dontClickAway: true } },
],
}}
.renderers=${{
// don't use these renderers, they are just examples
dontClickAway: (state, ruleOptions, value) =>
html`<input
@input=${inputListener(state.uiSchema, 'checked')}
?checked=${resolveDataSchema(value, state.uiSchema.scope)}
type="radio"
/><label>${processLabel(state)}</label>`,
string: (state, ruleOptions, value) =>
html`<div contenteditable="true" @input=${inputListener(state.uiSchema, 'innerText')}>
${resolveDataSchema(value, state.uiSchema.scope)}
</div>`,
}}
.value=${{ name: 'James Bond', acceptTerms: false }}
></json-form>`;
Set mode to 'schema' to render the shape of the data — the field types — instead of interactive
inputs. Useful for read-only previews or documenting a schema.
export const schemaMode = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.mode=${'schema'}
.schema=${{
type: 'object',
properties: {
string: {
type: 'string',
},
boolean: {
type: 'boolean',
description: 'Boolean description as a tooltip',
},
number: {
type: 'number',
},
integer: {
type: 'integer',
},
date: {
type: 'string',
format: 'date',
},
time: {
type: 'string',
format: 'time',
},
},
}}
.uiSchema=${{
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/string',
},
{
type: 'Control',
scope: '#/properties/boolean',
},
{
type: 'Control',
scope: '#/properties/number',
},
{
type: 'Control',
scope: '#/properties/integer',
},
{
type: 'Control',
scope: '#/properties/date',
},
{
type: 'Control',
scope: '#/properties/time',
},
],
}}
.value=${{
string: 'This is a string',
boolean: true,
number: 50.5,
integer: 50,
date: '2020-06-25',
}}
></json-form>`;
A larger combined example: an ArrayLayout nested inside a TabLayout, showing how layouts,
controls, and array entries compose together in a single form.
export const arrayTabsKitchenSink = () =>
html`<json-form
@formDataChange=${ev => console.log(ev.target.value)}
.schema=${{
type: 'object',
properties: {
string: { type: 'string' },
number: { type: 'number' },
array: {
type: 'array',
minItems: 3,
items: {
type: 'object',
properties: { string: { type: 'string' }, number: { type: 'number' } },
},
},
},
}}
.uiSchema=${{
type: 'TabLayout',
options: { tabNames: ['foo', 'array'] },
elements: [
{
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/string' },
{ type: 'Control', scope: '#/properties/number' },
],
},
{
type: 'VerticalLayout',
elements: [
{
type: 'ArrayLayout',
label: 'Array',
scope: '#/properties/array',
elements: {
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/array/properties/string' },
{ type: 'Control', scope: '#/properties/array/properties/number' },
{ type: 'Control', scope: '#/properties/string' },
],
},
},
],
},
],
}}
.value=${{
string: 'foobar',
array: [
{ string: 'foo', number: 4 },
{ string: 'bar', number: 5 },
],
}}
>
</json-form>`;
| Property | Type | Default | Description |
|---|---|---|---|
schema | JsonSchema7 | {} | JSON Schema that defines and validates the form data. |
uiSchema | UISchemaElement | {} | JsonForms UI schema that selects controls, layouts, labels, and options. |
value | object | {} | Current form data. Updated in place as controls emit formDataChange. |
validatorState | ValidationResult | { valid: true, errors: [] } | Latest validation result for value against schema. |
rootForm | boolean | true | Marks the form that owns value updates and validation. Nested forms set this internally. |
forceErrors | boolean | false | Forces validation errors to render even before a field has been touched. |
renderers | RendererRecord | {} | Custom renderers merged with the default renderer set. |
readonly | boolean | false | Renders controls read-only by passing readonly into control options. |
mode | 'form' | 'schema' | 'form' | Selects the form rendering mode. |
| Event | Description |
|---|---|
formDataChange | Fired by controls and layouts when form data changes; read the current form data from target.value. |
| Method | Description |
|---|---|
validate() | Revalidates value against schema and updates validatorState. |
getFirstInvalid() | Returns the first invalid nested json-form element, or undefined. |