-
-
Save webstrand/fb91dc3c0ad1008bdd9161a191ce019c to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| /** | |
| * Creates (or augments) a DOM element, then populates it with children and properties. | |
| * | |
| * `h` is polymorphic in both arguments: the first selects/creates the element, | |
| * and every remaining argument is a "child" interpreted by its runtime type. | |
| * | |
| * @param {string | Node | [namespaceURI: string, qualifiedName: string]} tag | |
| * Selects the element to operate on: | |
| * - **string** ― a tag name passed to `document.createElement` (e.g. `"div"`). | |
| * - **Node** ― an existing element (anything with a numeric `nodeType`). It is | |
| * used as-is; children and props are applied to it rather than to a new element. | |
| * - **array** ― spread into `document.createElementNS`, i.e. | |
| * `[namespaceURI, qualifiedName]`, for namespaced elements such as SVG. | |
| * | |
| * @param {...(string | number | bigint | boolean | symbol | null | undefined | Node | Function | Iterable<*> | Object)} children | |
| * Zero or more children, each interpreted by its runtime type: | |
| * - **string** ― appended as a text node. | |
| * - **number | bigint** ― stringified, then appended as a text node. | |
| * - **boolean | symbol** ― ignored, so `cond && child` appends nothing when `cond` is false. | |
| * - **null | undefined** ― ignored. | |
| * - **function** ― called with the element as its only argument; its return value | |
| * is processed as further children. | |
| * - **Node** ― appended directly. | |
| * - **iterable** (array, Set, generator, ...) ― flattened, each item processed | |
| * recursively. Strings are handled above, not here. | |
| * - **plain object** ― a bag of properties/attributes. Own, enumerable keys | |
| * (a value of `undefined` is skipped) are applied by key: | |
| * - `class` ― assigned to `el.className` on HTML; routed to `setAttribute` | |
| * on SVG, where `className` is a read-only `SVGAnimatedString`. | |
| * - `for` ― assigned to `el.htmlFor`. | |
| * - `classList` ― spread into `el.classList.add(...value)`. | |
| * - `style` / `dataset` ― an object value is merged with `Object.assign`; | |
| * a string value falls back to `setAttribute`. | |
| * - `attributes` ― a nested object; every entry is set as an attribute | |
| * (see "attribute values" below), whatever its key. | |
| * - a key containing a colon ― always set as an attribute, never a property. | |
| * - any other key ― assigned as a property when the element has a *writable* | |
| * slot for it; otherwise set as an attribute. (SVG's reflected attributes | |
| * such as `cx`/`width` surface as read-only `SVGAnimated*` objects, so they | |
| * fall through to `setAttribute` rather than throwing.) | |
| * | |
| * Attribute values (in the `attributes` block, on colon keys, and on | |
| * non-property keys) all go through one code path and are interpreted by type: | |
| * `undefined` is skipped, `boolean` toggles via `toggleAttribute`, `null` | |
| * removes via `removeAttribute`, a `[namespaceURI, value]` array *on a name | |
| * containing a colon* is set with `setAttributeNS` (an array on a colon-less | |
| * name is just stringified), and anything else is stringified and set with | |
| * `setAttribute`. Within a namespaced array, a boolean `value` toggles too: | |
| * `true` sets the empty string, `false` removes it via `removeAttributeNS` | |
| * (which takes the local name only). | |
| * | |
| * @returns {Node} The created (or passed-in) element, after children and props are applied. | |
| * | |
| * @example | |
| * // Nested elements, text, props, and an event handler | |
| * h("button", | |
| * { class: "primary", onclick: () => alert("hi") }, | |
| * "Click ", h("strong", "me")); | |
| * | |
| * @example | |
| * // Namespaced (SVG) element and a namespaced attribute | |
| * const SVG = "http://www.w3.org/2000/svg"; | |
| * const XLINK = "http://www.w3.org/1999/xlink"; | |
| * h([SVG, "svg"], { viewBox: "0 0 10 10" }, | |
| * h([SVG, "use"], { "xlink:href": [XLINK, "#icon"] })); | |
| * | |
| * @example | |
| * // Iterables are flattened; null/false children are skipped | |
| * h("ul", items.map(t => h("li", t)), showFoot && h("li", "footer")); | |
| */ | |
| function h(tag, ...children) { | |
| const el = typeof tag.nodeType === "number" | |
| ? tag | |
| : Array.isArray(tag) | |
| ? document.createElementNS(...tag) | |
| : document.createElement(tag); | |
| annotate(el, ...children); | |
| return el; | |
| } | |
| /** Appends children to `el`, interpreting each by runtime type (internal; see `h`). */ | |
| function annotate(el, ...children) { for (let c of children) switch (typeof c) { | |
| case "boolean": | |
| case "symbol": | |
| break; | |
| default: | |
| c = String(c); | |
| case "string": | |
| el.append(c); | |
| break; | |
| case "function": | |
| annotate(el, c(el)); | |
| break; | |
| case "undefined": | |
| case "object": | |
| if (c == null) break; | |
| else if (typeof c.nodeType === "number") { | |
| el.append(c); | |
| } else if (typeof c[Symbol.iterator] === "function") { | |
| for (const item of c) annotate(el, item); | |
| } else { | |
| for (const k in c) { | |
| if (!Object.prototype.hasOwnProperty.call(c, k)) continue; | |
| const v = c[k]; | |
| if (v === undefined) continue; | |
| switch (k) { | |
| case "class": | |
| if (assignable(el, "className")) el.className = v; | |
| else el.setAttribute("class", v); | |
| break; | |
| case "for": el.htmlFor = v; break; | |
| case "classList": | |
| el.classList.add(...v); | |
| break; | |
| case "style": | |
| case "dataset": | |
| if (typeof v === "object") Object.assign(el[k], v); | |
| else el.setAttribute(k, v); // harmless dataset=string | |
| break; | |
| case "attributes": | |
| for (const n in v) { | |
| if (!Object.prototype.hasOwnProperty.call(v, n)) continue; | |
| annotateAttr(el, n, v[n]); | |
| } | |
| break; | |
| default: { | |
| const colon = k.indexOf(":"); | |
| if (colon === -1 && assignable(el, k)) el[k] = v; | |
| else annotateAttr(el, k, v, colon); | |
| } | |
| } | |
| } | |
| } | |
| break; | |
| } } | |
| /** | |
| * Sets one attribute on `el`, dispatched by the value's runtime type (internal; see `h`). | |
| * `colon`, when provided, is `name.indexOf(":")`; otherwise it is derived lazily and | |
| * only when needed (i.e. when the value is a `[namespaceURI, value]` array). | |
| */ | |
| function annotateAttr(el, name, v, colon) { | |
| switch (typeof v) { | |
| case "undefined": break; | |
| case "boolean": | |
| el.toggleAttribute(name, v); | |
| break; | |
| default: | |
| if (v === null) { | |
| el.removeAttribute(name); | |
| break; | |
| } | |
| if (Array.isArray(v) && (colon ??= name.indexOf(":")) !== -1) { | |
| const [ns, val] = v; | |
| if (typeof val === "boolean") { | |
| if (val) el.setAttributeNS(ns, name, ""); | |
| else el.removeAttributeNS(ns, name.slice(colon + 1)); | |
| } else { | |
| el.setAttributeNS(ns, name, String(val)); | |
| } | |
| break; | |
| } | |
| v = String(v); | |
| case "string": | |
| el.setAttribute(name, v); | |
| break; | |
| } | |
| } | |
| /** | |
| * True when `el[name] = value` is a usable write ― the slot exists and is not a | |
| * read-only DOM wrapper (the `SVGAnimated*` objects, `SVGPointList`, etc., which | |
| * surface as non-null objects). `null` counts as writable, so event-handler slots | |
| * like `onclick` (default `null`) still bind as properties rather than stringifying. | |
| * | |
| * Caveat: a custom element with a writable property defaulting to a non-null object, | |
| * or re-annotating an element whose object-valued property is already set, will be | |
| * routed to `setAttribute` instead. | |
| */ | |
| function assignable(el, name) { | |
| const cur = el[name]; | |
| return name in el && cur !== undefined && (cur === null || typeof cur !== "object"); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment