Skip to content

Instantly share code, notes, and snippets.

@creationix
Last active December 6, 2024 01:30
Show Gist options
  • Save creationix/596fbf41f57ec90b24b98a9b638841f1 to your computer and use it in GitHub Desktop.
Save creationix/596fbf41f57ec90b24b98a9b638841f1 to your computer and use it in GitHub Desktop.
//////////////////////////////////////
// //
// JS domBuilder Library //
// //
// Tim Caswell <[email protected]> //
// //
//////////////////////////////////////
// Modern version using TypeScript and ES6 features
export type Refs = Record<string, HTMLElement>
export type Component<T extends unknown[]> = (...args: T) => Value
export type Value = string | Element | Group | HTMLElement | Text | DocumentFragment
export type Group = (Element | Group)[]
export type Element = ElementNoProps | ElementWithProps | ElementComponent<unknown[]>
export type ElementNoProps = [string, ...Value[]]
export type ElementWithProps = [string, Properties, ...Value[]]
export type ElementComponent<T extends unknown[]> = [Component<T>, ...T]
interface Properties {
[key: string]: string | boolean | ((node: HTMLElement) => void) | Record<string, string>
$?: (node: HTMLElement) => void
style?: Record<string, string> | 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<T extends unknown[]>(
json: ElementComponent<T>,
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<unknown[]> {
return typeof value[0] === 'function'
}
function isElementWithProps(value: unknown[]): value is ElementWithProps {
return value.length > 1 && typeof value[1] === 'object'
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment