////////////////////////////////////// // // // JS domBuilder Library // // // // Tim Caswell // // // ////////////////////////////////////// // Modern version using TypeScript and ES6 features export type Refs = Record 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 REF_MATCH = /\$[^.#$]+/ const TAG_MATCH = /^[^.#$]+/ export function renderText(text: string): Text { return document.createTextNode(text) } export function renderGroup(json: Group, refs?: Refs): DocumentFragment { const frag = document.createDocumentFragment() for (const child of json) { frag.appendChild(renderAny(child, refs)) } return frag } export function renderComponent( json: ElementComponent, refs?: Refs, ): HTMLElement | Text | DocumentFragment { const [createComponent, ...state] = json return renderAny(createComponent(...state), refs) } export function renderElement(json: ElementNoProps | ElementWithProps, refs?: Refs): HTMLElement { // 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)) } const ref = first.match(REF_MATCH) if (refs && ref) { refs[ref[0].substring(1)] = el } // 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(renderAny(child, refs)) } return el } // This is a simple dom builder that takes a json structure and returns a dom tree. export function renderAny(json: Value, refs?: Refs): HTMLElement | Text | DocumentFragment { if (!Array.isArray(json)) { // Pass through html elements, text nodes, and fragments as-is if (isNode(json)) { return json } return renderText(String(json)) } // Handle Groups if (isGroup(json)) { return renderGroup(json, refs) } // Handle Components if (isComponent(json)) { return renderComponent(json, refs) } return renderElement(json, refs) } 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' }