////////////////////////////////////// // // // JS domBuilder Library // // // // Tim Caswell // // // ////////////////////////////////////// // Modern version using TypeScript and ES6 features export type Component = (...args: T) => Value export type Value = string | Element | Group | HTMLElement | Text | DocumentFragment export type Group = (Element | Group)[] export type Element = ElementNoProps | ElementWithProps | ElementComponent export type ElementNoProps = [string, ...Value[]] export type ElementWithProps = [string, Properties, ...Value[]] export type ElementComponent = [Component, ...T] interface Properties { [key: string]: string | boolean | ((node: HTMLElement) => void) | Record $: (node: HTMLElement) => void style: Record | string } const CLASS_MATCH = /\.[^.#$]+/g const ID_MATCH = /#[^.#$]+/ const TAG_MATCH = /^[^.#$]+/ // This is a simple dom builder that takes a json structure and returns a dom tree. export function domBuilder(json: Value): HTMLElement | Text | DocumentFragment { if (!Array.isArray(json)) { // Pass through html elements, text nodes, and fragments as-is if (isNode(json)) { return json } // Render Text Nodes return document.createTextNode(String(json)) } // Handle Groups if (isGroup(json)) { const frag = document.createDocumentFragment() for (const child of json) { frag.appendChild(domBuilder(child)) } return frag } // Handle Components if (isComponent(json)) { const [createComponent, ...state] = json return domBuilder(createComponent(...state)) } // Create Elements const first = json[0] const match = first.match(TAG_MATCH) const tag = match ? match[0] : 'div' const el = document.createElement(tag) const classes = first.match(CLASS_MATCH) if (classes) { el.setAttribute('class', classes.map(stripFirst).join(' ')) } const id = first.match(ID_MATCH) if (id) { el.setAttribute('id', id[0].substr(1)) } // Optionally apply properties and append children let children: Value[] if (isElementWithProps(json)) { const [, props, ...rest] = json applyProps(el, props) children = rest } else { children = json.slice(1) } for (const child of children) { el.appendChild(domBuilder(child)) } return el } function applyProps(node: HTMLElement, attrs: Properties) { for (const [key, value] of Object.entries(attrs)) { if (typeof value === 'string') { node.setAttribute(key, value) } else if (value === true) { node.setAttribute(key, key) } else if (value === false) { node.removeAttribute(key) } else if (key === 'style') { for (const [k, v] of Object.entries(value)) { node.style[k] = v } } else if (key !== '$') { node[key] = value } } if ('$' in attrs) { attrs.$(node) } } function stripFirst(part: string): string { return part.substring(1) } function isNode(value: Value): value is HTMLElement | Text | DocumentFragment { return value instanceof HTMLElement || value instanceof window.Text || value instanceof window.DocumentFragment } function isGroup(value: unknown[]): value is Group { return value.length === 0 || Array.isArray(value[0]) } function isComponent(value: unknown[]): value is ElementComponent { return typeof value[0] === 'function' } function isElementWithProps(value: unknown[]): value is ElementWithProps { return value.length > 1 && typeof value[1] === 'object' }