a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
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);