a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 9.7 kB view raw
1/** 2 * DOM creation utilities for building demo sections programmatically 3 */ 4 5import { isNil } from "$core/shared"; 6import type { None, Nullable } from "$types/helpers"; 7 8type Attributes = Record<string, string | boolean | None>; 9 10type Attrs = Nullable<Attributes | string>; 11 12type CreateFn<K extends keyof HTMLElementTagNameMap> = ( 13 attrs?: Attrs, 14 ...children: (Node | string)[] 15) => HTMLElementTagNameMap[K]; 16 17type ElementFactory = <K extends keyof HTMLElementTagNameMap>( 18 tag: K, 19 attrs?: Attrs, 20 ...children: (Node | string)[] 21) => HTMLElementTagNameMap[K]; 22 23type ListFactory = <K extends keyof HTMLElementTagNameMap>( 24 createFn: CreateFn<K>, 25 items: string[], 26 attrs?: Attrs, 27) => HTMLElementTagNameMap[K][]; 28 29export const el: ElementFactory = (tag, attrs?, ...children) => { 30 const element = document.createElement(tag); 31 32 if (typeof attrs === "string") { 33 element.className = attrs; 34 } else if (attrs) { 35 for (const [key, value] of Object.entries(attrs)) { 36 if (isNil(value) || value === false) continue; 37 if (value === true) { 38 element.setAttribute(key, ""); 39 } else { 40 element.setAttribute(key, String(value)); 41 } 42 } 43 } 44 45 for (const child of children) { 46 if (typeof child === "string") { 47 element.append(document.createTextNode(child)); 48 } else { 49 element.append(child); 50 } 51 } 52 53 return element; 54}; 55 56export function text(content: string): Text { 57 return document.createTextNode(content); 58} 59 60export function fragment(...children: (Node | string)[]): DocumentFragment { 61 const frag = document.createDocumentFragment(); 62 for (const child of children) { 63 if (typeof child === "string") { 64 frag.append(document.createTextNode(child)); 65 } else { 66 frag.append(child); 67 } 68 } 69 return frag; 70} 71 72export const repeat: ListFactory = (createFn, items, attrs) => { 73 return items.map((item) => createFn(attrs, item)); 74}; 75 76/** 77 * Create key-value pairs for description lists (dt/dd) 78 * 79 * @example 80 * dl(null, ...kv([ 81 * ["Term", "Definition"], 82 * ["Signal", "A reactive primitive"] 83 * ])) 84 */ 85export function kv(pairs: Array<[string, string]>, dtAttrs?: Attrs, ddAttrs?: Attrs): HTMLElement[] { 86 const elements = []; 87 for (const [term, definition] of pairs) { 88 elements.push(dt(dtAttrs, term), dd(ddAttrs, definition)); 89 } 90 return elements; 91} 92 93/** 94 * Create option elements for select dropdowns 95 * 96 * @example 97 * select({ id: "country" }, ...options([ 98 * ["us", "United States"], 99 * ["uk", "United Kingdom"] 100 * ])) 101 */ 102export function options(items: Array<[string, string]>, attrs?: Attrs): HTMLOptionElement[] { 103 return items.map(([value, label]) => { 104 const optionAttrs = typeof attrs === "string" ? { class: attrs, value } : { ...attrs, value }; 105 return option(optionAttrs, label); 106 }); 107} 108 109/** 110 * Create a label and input as adjacent siblings 111 * The label's `for` attribute will match the input's `id` 112 * 113 * @example 114 * ...labelFor("Name", { id: "name", type: "text", required: true }) 115 */ 116export function labelFor( 117 labelText: string, 118 inputAttrs: Attrs & { id: string }, 119 labelAttrs?: Attrs, 120): [HTMLLabelElement, HTMLInputElement] { 121 const labelElement = label( 122 typeof labelAttrs === "string" ? labelAttrs : { ...labelAttrs, for: inputAttrs.id }, 123 labelText, 124 ); 125 const inputElement = input(inputAttrs); 126 return [labelElement, inputElement]; 127} 128 129/** 130 * Create a label wrapping an input element 131 * No `for` or `id` needed since the input is wrapped 132 * 133 * @example 134 * labelWith("Subscribe to newsletter", { type: "checkbox", "data-volt-model": "newsletter" }) 135 */ 136export function labelWith( 137 labelText: string | (Node | string)[], 138 inputAttrs: Attrs, 139 labelAttrs?: Attrs, 140): HTMLLabelElement { 141 const inputElement = input(inputAttrs); 142 if (typeof labelText === "string") { 143 return label(labelAttrs, inputElement, " ", labelText); 144 } 145 return label(labelAttrs, inputElement, " ", ...labelText); 146} 147 148/** 149 * Create multiple buttons with different click handlers 150 * 151 * @example 152 * ...buttons([ 153 * ["Increment", "increment"], 154 * ["Decrement", "decrement"], 155 * { label: "Reset", onClick: "reset", type: "reset" } 156 * ]) 157 */ 158export function buttons( 159 items: Array<[string, string] | { label: string; onClick: string } & Attributes>, 160 sharedAttrs?: Attrs, 161): HTMLButtonElement[] { 162 return items.map((item) => { 163 if (Array.isArray(item)) { 164 const [label, onClick] = item; 165 const baseAttrs = typeof sharedAttrs === "object" && sharedAttrs !== null ? sharedAttrs : {}; 166 const attrs = { ...baseAttrs, "data-volt-on-click": onClick }; 167 return button(attrs, label); 168 } 169 const { label: buttonLabel, onClick, ...restAttrs } = item; 170 const baseAttrs = typeof sharedAttrs === "object" && sharedAttrs !== null ? sharedAttrs : {}; 171 const attrs = { ...baseAttrs, ...restAttrs, "data-volt-on-click": onClick }; 172 return button(attrs, buttonLabel); 173 }); 174} 175 176export const h1: CreateFn<"h1"> = (attrs?, ...children) => el("h1", attrs, ...children); 177export const h2: CreateFn<"h2"> = (attrs?, ...children) => el("h2", attrs, ...children); 178export const h3: CreateFn<"h3"> = (attrs?, ...children) => el("h3", attrs, ...children); 179export const h4: CreateFn<"h4"> = (attrs?, ...children) => el("h4", attrs, ...children); 180export const h5: CreateFn<"h5"> = (attrs?, ...children) => el("h5", attrs, ...children); 181export const h6: CreateFn<"h6"> = (attrs?, ...children) => el("h6", attrs, ...children); 182export const p: CreateFn<"p"> = (attrs?, ...children) => el("p", attrs, ...children); 183export const div: CreateFn<"div"> = (attrs?, ...children) => el("div", attrs, ...children); 184export const span: CreateFn<"span"> = (attrs?, ...children) => el("span", attrs, ...children); 185export const small: CreateFn<"small"> = (attrs?, ...children) => el("small", attrs, ...children); 186export const article: CreateFn<"article"> = (attrs?, ...children) => el("article", attrs, ...children); 187export const aside: CreateFn<"aside"> = (attrs?, ...children) => el("aside", attrs, ...children); 188export const section: CreateFn<"section"> = (attrs?, ...children) => el("section", attrs, ...children); 189export const header: CreateFn<"header"> = (attrs?, ...children) => el("header", attrs, ...children); 190export const footer: CreateFn<"footer"> = (attrs?, ...children) => el("footer", attrs, ...children); 191export const nav: CreateFn<"nav"> = (attrs?, ...children) => el("nav", attrs, ...children); 192export const ul: CreateFn<"ul"> = (attrs?, ...children) => el("ul", attrs, ...children); 193export const ol: CreateFn<"ol"> = (attrs?, ...children) => el("ol", attrs, ...children); 194export const li: CreateFn<"li"> = (attrs?, ...children) => el("li", attrs, ...children); 195export const dl: CreateFn<"dl"> = (attrs?, ...children) => el("dl", attrs, ...children); 196export const dt: CreateFn<"dt"> = (attrs?, ...children) => el("dt", attrs, ...children); 197export const dd: CreateFn<"dd"> = (attrs?, ...children) => el("dd", attrs, ...children); 198export const a: CreateFn<"a"> = (attrs?, ...children) => el("a", attrs, ...children); 199export const button: CreateFn<"button"> = (attrs?, ...children) => el("button", attrs, ...children); 200export const input: CreateFn<"input"> = (attrs?: Attributes | string | null) => el("input", attrs); 201export const textarea: CreateFn<"textarea"> = (attrs?, ...children) => el("textarea", attrs, ...children); 202export const select: CreateFn<"select"> = (attrs?, ...children) => el("select", attrs, ...children); 203export const option: CreateFn<"option"> = (attrs?, ...children) => el("option", attrs, ...children); 204export const label: CreateFn<"label"> = (attrs?, ...children) => el("label", attrs, ...children); 205export const form: CreateFn<"form"> = (attrs?, ...children) => el("form", attrs, ...children); 206export const fieldset: CreateFn<"fieldset"> = (attrs?, ...children) => el("fieldset", attrs, ...children); 207export const legend: CreateFn<"legend"> = (attrs?, ...children) => el("legend", attrs, ...children); 208export const table: CreateFn<"table"> = (attrs?, ...children) => el("table", attrs, ...children); 209export const thead: CreateFn<"thead"> = (attrs?, ...children) => el("thead", attrs, ...children); 210export const tbody: CreateFn<"tbody"> = (attrs?, ...children) => el("tbody", attrs, ...children); 211export const tr: CreateFn<"tr"> = (attrs?, ...children) => el("tr", attrs, ...children); 212export const th: CreateFn<"th"> = (attrs?, ...children) => el("th", attrs, ...children); 213export const td: CreateFn<"td"> = (attrs?, ...children) => el("td", attrs, ...children); 214export const blockquote: CreateFn<"blockquote"> = (attrs?, ...children) => el("blockquote", attrs, ...children); 215export const cite: CreateFn<"cite"> = (attrs?, ...children) => el("cite", attrs, ...children); 216export const code: CreateFn<"code"> = (attrs?, ...children) => el("code", attrs, ...children); 217export const pre: CreateFn<"pre"> = (attrs?, ...children) => el("pre", attrs, ...children); 218export const dialog: CreateFn<"dialog"> = (attrs?, ...children) => el("dialog", attrs, ...children); 219export const details: CreateFn<"details"> = (attrs?, ...children) => el("details", attrs, ...children); 220export const summary: CreateFn<"summary"> = (attrs?, ...children) => el("summary", attrs, ...children); 221export const strong: CreateFn<"strong"> = (attrs?, ...children) => el("strong", attrs, ...children); 222export const em: CreateFn<"em"> = (attrs?, ...children) => el("em", attrs, ...children); 223export const del: CreateFn<"del"> = (attrs?, ...children) => el("del", attrs, ...children); 224export const abbr: CreateFn<"abbr"> = (attrs?, ...children) => el("abbr", attrs, ...children); 225export const hr: CreateFn<"hr"> = (attrs?) => el("hr", attrs);