Skip to main content

max / makenotwork

2.2 KB · 50 lines History Blame Raw
1 // Islands infrastructure.
2 //
3 // An "island" is a custom element that attaches behavior to server-rendered
4 // markup. It never re-renders the server's HTML (no hydration). A key payoff
5 // over the old `querySelectorAll` + `DOMContentLoaded` + `htmx:after:swap`
6 // re-init dance: the browser upgrades a custom element automatically whenever
7 // it's connected to the document, INCLUDING content HTMX swaps in, so islands
8 // need no manual re-initialization after a swap.
9 //
10 // Pure, testable logic goes in a sibling `*.logic.ts` (no DOM imports) so
11 // `node --test` can exercise it without a DOM; the element file wires that
12 // logic over the light-DOM children.
13
14 /** Base class: runs `init()` exactly once, the first time the element is
15 * connected (a move/reconnect won't re-run it). */
16 export abstract class MnwElement extends HTMLElement {
17 #initialized = false;
18
19 connectedCallback(): void {
20 if (this.#initialized) return;
21 this.#initialized = true;
22 this.init();
23 // `data-island` means this element's behaviour ran. Nothing styles it; it
24 // exists to be asserted on.
25 //
26 // Set AFTER init() on purpose. If init throws, the attribute is absent and
27 // the element is visibly un-enhanced to a checker, which is the state that
28 // used to be undetectable.
29 //
30 // 2026-08-14 is why this is here. A stale module in the bundle's import
31 // graph made `core/index.js` fail to link, so no island on the site
32 // registered at all — and the page looked fine, because every island in
33 // this codebase enhances server-rendered markup that stands on its own
34 // without it. Progressive enhancement means a dead bundle and a healthy one
35 // differ only in behaviour, and behaviour is the thing nothing was
36 // checking. `scripts/page-smoke.mjs` checks it now, and this is what it
37 // reads.
38 this.setAttribute('data-island', '');
39 }
40
41 /** Wire behavior over the already-rendered light-DOM children. */
42 protected abstract init(): void;
43 }
44
45 /** Define a custom element once (idempotent, safe if the module is imported
46 * from more than one entry). */
47 export function define(name: string, ctor: CustomElementConstructor): void {
48 if (!customElements.get(name)) customElements.define(name, ctor);
49 }
50