/** * Tiny DOM builder. ALL text and attribute values are set via `textContent` / `setAttribute` — this * module NEVER assigns `innerHTML`, so untrusted server/host data (host subdomains, etc.) can never * inject markup. Children passed as strings become text nodes (also safe). */ export type Child = Node | string export interface ElProps { class?: string text?: string type?: string name?: string placeholder?: string value?: string disabled?: boolean autocomplete?: string title?: string src?: string alt?: string role?: string ariaLabel?: string onClick?: (e: MouseEvent) => void onSubmit?: (e: SubmitEvent) => void } export function el( tag: K, props: ElProps = {}, children: Child[] = [], ): HTMLElementTagNameMap[K] { const node = document.createElement(tag) if (props.class !== undefined) node.className = props.class if (props.text !== undefined) node.textContent = props.text if (props.type !== undefined) node.setAttribute('type', props.type) if (props.name !== undefined) node.setAttribute('name', props.name) if (props.placeholder !== undefined) node.setAttribute('placeholder', props.placeholder) if (props.value !== undefined) (node as HTMLInputElement).value = props.value if (props.disabled) node.setAttribute('disabled', 'true') if (props.autocomplete !== undefined) node.setAttribute('autocomplete', props.autocomplete) if (props.title !== undefined) node.setAttribute('title', props.title) if (props.src !== undefined) node.setAttribute('src', props.src) if (props.alt !== undefined) node.setAttribute('alt', props.alt) if (props.role !== undefined) node.setAttribute('role', props.role) if (props.ariaLabel !== undefined) node.setAttribute('aria-label', props.ariaLabel) if (props.onClick !== undefined) node.addEventListener('click', props.onClick as EventListener) if (props.onSubmit !== undefined) node.addEventListener('submit', props.onSubmit as EventListener) for (const child of children) node.append(child) return node } /** Remove all children of a node. */ export function clear(node: Element): void { while (node.firstChild) node.removeChild(node.firstChild) } /** Replace a node's content with a single new child. */ export function mount(root: Element, child: Node): void { clear(root) root.append(child) }