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.

Revisions

  1. creationix renamed this gist Dec 6, 2024. 1 changed file with 52 additions and 26 deletions.
    78 changes: 52 additions & 26 deletions dombuilder.ts → dom-builder.ts
    Original file line number Diff line number Diff line change
    @@ -7,6 +7,8 @@
    //////////////////////////////////////

    // 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)[]
    @@ -16,40 +18,36 @@ 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
    $?: (node: HTMLElement) => void
    style?: Record<string, string> | string
    }

    const CLASS_MATCH = /\.[^.#$]+/g
    const ID_MATCH = /#[^.#$]+/
    const REF_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))
    }
    export function renderText(text: string): Text {
    return document.createTextNode(text)
    }

    // Handle Groups
    if (isGroup(json)) {
    const frag = document.createDocumentFragment()
    for (const child of json) {
    frag.appendChild(domBuilder(child))
    }
    return frag
    export function renderGroup(json: Group, refs?: Refs): DocumentFragment {
    const frag = document.createDocumentFragment()
    for (const child of json) {
    frag.appendChild(renderAny(child, refs))
    }
    return frag
    }

    // Handle Components
    if (isComponent(json)) {
    const [createComponent, ...state] = json
    return domBuilder(createComponent(...state))
    }
    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)
    @@ -63,6 +61,10 @@ export function domBuilder(json: Value): HTMLElement | Text | DocumentFragment {
    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[]
    @@ -74,11 +76,35 @@ export function domBuilder(json: Value): HTMLElement | Text | DocumentFragment {
    children = json.slice(1)
    }
    for (const child of children) {
    el.appendChild(domBuilder(child))
    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') {
  2. creationix revised this gist Dec 5, 2024. 1 changed file with 6 additions and 6 deletions.
    12 changes: 6 additions & 6 deletions dombuilder.ts
    Original file line number Diff line number Diff line change
    @@ -7,13 +7,13 @@
    //////////////////////////////////////

    // Modern version using TypeScript and ES6 features

    export type Value = string | Element | Component<unknown[]> | Group | HTMLElement | Text | DocumentFragment
    export type Group = (Element | Component<unknown[]> | Group)[]
    export type Element = ElementNoProps | ElementWithProps
    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 Component<T extends unknown[]> = [(...args: T) => Value, ...T]
    export type ElementComponent<T extends unknown[]> = [Component<T>, ...T]
    interface Properties {
    [key: string]: string | boolean | ((node: HTMLElement) => void) | Record<string, string>
    $: (node: HTMLElement) => void
    @@ -112,7 +112,7 @@ function isGroup(value: unknown[]): value is Group {
    return value.length === 0 || Array.isArray(value[0])
    }

    function isComponent(value: unknown[]): value is Component<unknown[]> {
    function isComponent(value: unknown[]): value is ElementComponent<unknown[]> {
    return typeof value[0] === 'function'
    }

  3. creationix created this gist Dec 5, 2024.
    121 changes: 121 additions & 0 deletions dombuilder.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,121 @@
    //////////////////////////////////////
    // //
    // JS domBuilder Library //
    // //
    // Tim Caswell <[email protected]> //
    // //
    //////////////////////////////////////

    // Modern version using TypeScript and ES6 features

    export type Value = string | Element | Component<unknown[]> | Group | HTMLElement | Text | DocumentFragment
    export type Group = (Element | Component<unknown[]> | Group)[]
    export type Element = ElementNoProps | ElementWithProps
    export type ElementNoProps = [string, ...Value[]]
    export type ElementWithProps = [string, Properties, ...Value[]]
    export type Component<T extends unknown[]> = [(...args: T) => Value, ...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 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 Component<unknown[]> {
    return typeof value[0] === 'function'
    }

    function isElementWithProps(value: unknown[]): value is ElementWithProps {
    return value.length > 1 && typeof value[1] === 'object'
    }