Skip to content

Instantly share code, notes, and snippets.

@spiffytech
Created December 16, 2020 12:38
Show Gist options
  • Save spiffytech/4bdbbf1f87845e168fef4300742fbe07 to your computer and use it in GitHub Desktop.
Save spiffytech/4bdbbf1f87845e168fef4300742fbe07 to your computer and use it in GitHub Desktop.
Experimental HTML custom element library
import * as h from "hyperscript";
import morphdom from "morphdom";
import { nanoid } from "nanoid";
abstract class Coordinator<
Attrs extends Record<string, any> | null,
State extends Record<string, any>
> extends HTMLElement {
static readonly forwardedAttrs: Map<string, unknown> = new Map();
static attrsAttr = "data-attrs";
protected abstract readonly state: State;
private animationFrameNumber: number | null = null;
private lastHandledAttrsKey: string | null = null;
get attrs(): Attrs | null {
if (!this.hasAttribute(Coordinator.attrsAttr)) return null;
return Coordinator.forwardedAttrs.get(
this.getAttribute(Coordinator.attrsAttr)!
) as Attrs;
}
protected watchState(state: State): State {
const render = () => this._render();
return new Proxy(state, {
set(...args) {
Reflect.set(...args);
render();
return true;
},
});
}
private _render() {
this.animationFrameNumber ||= requestAnimationFrame(() => {
// Stuff the render output into a template element so morphdom can modify
// the children of this and the render output, instead of morphing this
// into the first child of the render output (essentially removing this
// from the DOM)
const el = h("template", this.render());
morphdom(this, el, { childrenOnly: true });
this.animationFrameNumber = null;
});
}
abstract render(): Element | Element[];
connectedCallback() {
const attrsKey = this.getAttribute(Coordinator.attrsAttr);
// connectedCallback and attributeChangedCallback both receive events for
// the element's initial attributes, but we don't want to double render.
if (attrsKey && attrsKey !== this.lastHandledAttrsKey) {
this.lastHandledAttrsKey = attrsKey;
this._render();
}
}
disconnectedCallback() {
setTimeout(() => {
if (this.isConnected) return; // We got reattached to something
if (this.hasAttribute(Coordinator.attrsAttr)) {
Coordinator.forwardedAttrs.delete(
this.getAttribute(Coordinator.attrsAttr)!
);
}
}, 1000);
}
attributeChangedCallback(
name: string,
oldValue: string | undefined,
value: string
) {
// Morphdom mucks with our attributes when shuffling nodes around. We don't
// want to double render while that's happening.
if (!this.isConnected) return;
if (name !== Coordinator.attrsAttr) return;
if (oldValue) Coordinator.forwardedAttrs.delete(oldValue);
// connectedCallback and attributeChangedCallback both receive events for
// the element's initial attributes, but we don't want to double render.
if (value !== this.lastHandledAttrsKey) {
this.lastHandledAttrsKey = value;
this._render();
}
}
protected forwardAttrs(args: Record<string, unknown>) {
const id = nanoid();
Coordinator.forwardedAttrs.set(id, args);
return { [Coordinator.attrsAttr]: id };
}
}
export class MyElementChild extends Coordinator<
{ time: string },
{ id: string }
> {
state = this.watchState({ id: nanoid() });
private renderCount = 0;
render() {
this.renderCount += 1;
function onclick(e: any) {
e.preventDefault();
alert("Oh no, sire! A click!");
}
return h(
"p",
"The time is: ",
this.attrs?.time,
h("button", { onclick }, "Click me!"),
h("p", "My ID is: ", this.state.id),
h("p", `I have been rendered ${this.renderCount} times`)
);
}
static get observedAttributes() {
return [Coordinator.attrsAttr];
}
}
class MyElementParent extends Coordinator<null, { counter: number }> {
state = this.watchState({ counter: 0 });
connectedCallback() {
super.connectedCallback();
setInterval(() => (this.state.counter += 1), 1000);
}
render() {
return [
...(new Date().getSeconds() % 2 === 0
? [
h("my-child", {
id: "child-1",
...this.forwardAttrs({ time: "the beginning" }),
}),
]
: []),
h("my-child", {
id: "child-2",
...this.forwardAttrs({ time: new Date().toISOString() }),
}),
];
}
}
customElements.define("my-child", MyElementChild);
customElements.define("my-parent", MyElementParent);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment