Last active
December 6, 2024 01:30
-
-
Save creationix/596fbf41f57ec90b24b98a9b638841f1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
////////////////////////////////////// | |
// // | |
// 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