Using https://dev.to/yne/a-one-liner-for-createelement-with-attributes-and-children-55nh
The following snippet convert any JSON Schema to DOM elements.
const el = (tag, props = {}, ch = [], attrs = {}) => ch.reduce((e, c) => (e.appendChild(c), e), Object.entries(attrs).reduce((e, [k, v]) => (e.setAttribute(k, v), e), Object.assign(document.createElement(tag), Object.fromEntries(Object.entries(props).filter(([key, value]) => value !== undefined)))));
function schema2dom(item, name = "", parent = {}) {
let ul, total; // this total <input> track if we stay in minItems/maxItems
if (item.type == "array") return [ul = el("ul", {}, [el("li", {}, [el("button", {
type: "button", innerText: "Add", onclick: () => ul.insertAdjacentElement("beforeend", el("li", { ariaRowIndex: total.ariaRowIndex = ++total.value }, [
el("button", { type: "button", innerText: "Remove", onclick() { total.ariaRowIndex = --total.value; this.parentElement.remove() } }),
...schema2dom(item.items, name, parent.required?.includes(name))
]))
}), total = el("input", { type: "number", ariaRowIndex: 0, value: 0, min: item.minItems, max: item.maxItems, oninput() { this.value = this.ariaRowIndex } })])])];
if (item.type == "object") return [el("dl", { }, Object.entries(item.properties).map(([name, v]) => [
el("dt", { innerText: v.title || name, title: v.description || '' }),
el("dd", {}, schema2dom(v, name, item)),// "additionalProperties", propertyNames+pattern, minProperties maxProperties
]).flat())];
const common = { name, title:item.description||parent.description, required: parent.required?.includes(name), minLength: item.minLength, maxLength: item.maxLength, pattern: item.pattern, min: item.minimum, max: item.maximum };
if (item.enum) return [el("select", common, item.enum.map((v,i) => el("option", { innerText: v === Object(v) ? JSON.stringify(v) : v, value: JSON.stringify(v), title:(item.markdownEnumDescriptions||item.enumDescriptions||[])[i]})))];
const datalist = [el("datalist", { id: `list_${Math.random()}` }, item.examples?.map(ex => el("option", { innerText: ex, title: JSON.stringify(ex) })))];
if (item.type == "boolean") return [el("input", { type: "checkbox", checked: item.default, ...common }), ...datalist];
if (item.type == "integer") return [el("input", { type: "number", value: item.default, step: item.multipleOf || 1, ...common }), ...datalist];
if (item.type == "number") return [el("input", { type: "number", value: item.default, step: item.multipleOf, ...common }), ...datalist];
if (item.type == "string") return [el("input", { type: "text", value: item.default, ...common }, [], { list: datalist?.[0].id }), ...datalist];
return []; // item.type == "null"
}
For example:
schema2dom({type:"object",properties:{n:{type:"number"},s:{type:"string"}}})
Will return 2 fields: a <input type=number>
plus a <input type=text>
Limitation
-
examples
can't be array or object they will be toString() and put as value of - Object are non-extensible (see
additionalProperties
and associatedpropertyNames
,pattern
,minProperties
,maxProperties
.
Serialization
Simple Schema generated
can be serialized usingDataForm()
:const obj = Object.fromEntries((new FormData(myForm)).entries())
However, nested list/objects require a specific serialization:
function serialise(el) {
if (el.tagName == 'DL') return Object.fromEntries([...el.children].reduce((acc, _, i, ch) => i % 2 ? acc : [...acc, [ch[i].innerText, serialise(ch[i + 1].children[0])]], []))
if (el.tagName == 'UL') return [...el.children].slice(1).map(li => serialise(li.children[1]))
if (el.tagName == 'SELECT') return JSON.parse(el.value);
if (el.type == 'checkbox') return el.checked;
if (el.type == 'number') return el.valueAsNumber;
if (el.type == 'text') return el.value;
return null;
}
Style
No style are required, but a grid display could help.
dl,ul>li {
display: grid;
grid-template-columns: 1fr 5fr;
}
dd { display: contents; }
dl, ul {
padding: 0;
margin: 0;
}
Top comments (0)