Skip to main content

max / makenotwork

1.7 KB · 47 lines History Blame Raw
1 /**
2 * Islands infrastructure.
3 *
4 * An island is a custom element that attaches behavior to server-rendered
5 * markup. It never re-renders what the server sent, so a page keeps working
6 * with the script blocked and the element only adds what needs a client.
7 *
8 * The payoff over the `DOMContentLoaded` + `htmx:after:swap` re-init dance that
9 * static/mt.js does by hand: the browser upgrades a custom element whenever it
10 * is connected to the document, including markup htmx swapped in, so an island
11 * needs no manual re-initialization.
12 *
13 * Ported from MNW/server/frontend/src/islands/base.ts, deliberately as a copy.
14 * The two apps share no workspace and no registry, and 30 lines duplicated is
15 * cheaper than the mechanism that would let them share it. See the Client
16 * section of the livechat design note.
17 *
18 * Pure logic goes in a sibling `*.logic.ts` with no DOM imports, so
19 * `node --test` exercises it without a DOM; the element file wires that logic
20 * over the light-DOM children.
21 */
22
23 /**
24 * Base class: runs `init()` exactly once, the first time the element is
25 * connected. Moving the element in the document will not re-run it.
26 */
27 export abstract class MtElement extends HTMLElement {
28 #initialized = false;
29
30 connectedCallback(): void {
31 if (this.#initialized) return;
32 this.#initialized = true;
33 this.init();
34 }
35
36 /** Wire behavior over the already-rendered light-DOM children. */
37 protected abstract init(): void;
38 }
39
40 /**
41 * Define a custom element once. Idempotent, so importing the module from more
42 * than one entry point does not throw on the second definition.
43 */
44 export function define(name: string, ctor: CustomElementConstructor): void {
45 if (!customElements.get(name)) customElements.define(name, ctor);
46 }
47