Packages
lustre
4.0.0-rc.2
5.7.1
5.7.0
5.6.0
5.5.2
5.5.1
5.5.0
5.4.0
5.3.5
5.3.4
5.3.3
5.3.2
5.3.1
5.3.0
5.2.1
5.2.0
5.1.1
5.1.0
5.0.3
5.0.2
5.0.1
5.0.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.1
4.5.0
4.4.4
4.4.3
4.4.1
4.4.0
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.8
4.1.7
4.1.6
4.1.5
4.1.4
4.1.3
4.1.2
4.1.1
4.1.0
4.0.0
4.0.0-rc1
4.0.0-rc.2
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.2
3.0.0-rc.1
2.0.1
2.0.0
1.3.0
1.2.0
1.1.0
1.0.0
Create HTML templates, single page applications, Web Components, and real-time server components in Gleam!
Current section
Files
Jump to
Current section
Files
src/server-component.mjs
// Note that this path is relative to the built Gleam project, not the source files
// in `src/`. This particular module is not used by the Lustre package itself, but
// is instead bundled and made available to package users in the `priv/` directory.
//
// It makes obvious sense to co-locate the source with the rest of the package
// source code, but if we use relative imports here the bundle will fail because
// `vdom.ffi.mjs` is importing things from the Gleam standard library and expects
// to be placed in the `build/dev/javascript/lustre/` directory.
//
import * as Constants from "../build/dev/javascript/lustre/lustre/internals/constants.mjs";
import { patch, morph } from "../build/dev/javascript/lustre/vdom.ffi.mjs";
export class LustreServerComponent extends HTMLElement {
static get observedAttributes() {
return ["route"];
}
#observer = null;
#root = null;
#socket = null;
constructor() {
super();
this.#observer = new MutationObserver((mutations) => {
const changed = [];
for (const mutation of mutations) {
if (mutation.type === "attributes") {
const { attributeName: name, oldValue: prev } = mutation;
const next = this.getAttribute(name);
if (prev !== next) {
try {
changed.push([name, JSON.parse(next)]);
} catch {
changed.push([name, next]);
}
}
}
}
if (changed.length) {
this.#socket?.send(JSON.stringify([Constants.attrs, changed]));
}
});
}
connectedCallback() {
this.#root = document.createElement("div");
this.appendChild(this.#root);
}
attributeChangedCallback(name, prev, next) {
switch (name) {
case "route": {
if (!next) {
this.#socket?.close();
this.#socket = null;
} else if (prev !== next) {
const id = this.getAttribute("id");
const route = next + (id ? `?id=${id}` : "");
this.#socket?.close();
this.#socket = new WebSocket(`ws://${window.location.host}${route}`);
this.#socket.addEventListener("message", (message) =>
this.messageReceivedCallback(message),
);
}
}
}
}
messageReceivedCallback({ data }) {
const [kind, ...payload] = JSON.parse(data);
switch (kind) {
case Constants.diff:
return this.diff(payload);
case Constants.emit:
return this.emit(payload);
case Constants.init:
return this.init(payload);
}
}
init([attrs, vdom]) {
const initial = [];
for (const attr of attrs) {
if (attr in this) {
initial.push([attr, this[attr]]);
} else if (this.hasAttribute(attr)) {
initial.push([attr, this.getAttribute(attr)]);
}
Object.defineProperty(this, attr, {
get() {
return this[`_${attr}`] ?? this.getAttribute(attr);
},
set(value) {
const prev = this[attr];
if (typeof value === "string") {
this.setAttribute(attr, value);
} else {
this[`_${attr}`] = value;
}
if (prev !== value) {
this.#socket?.send(
JSON.stringify([Constants.attrs, [[attr, value]]]),
);
}
},
});
}
this.#observer.observe(this, {
attributeFilter: attrs,
attributeOldValue: true,
attributes: true,
characterData: false,
characterDataOldValue: false,
childList: false,
subtree: false,
});
this.morph(vdom);
if (initial.length) {
this.#socket?.send(JSON.stringify([Constants.attrs, initial]));
}
}
morph(vdom) {
this.#root = morph(this.#root, vdom, (msg) => {
this.#socket?.send(JSON.stringify([Constants.event, msg.tag, msg.data]));
});
}
diff([diff]) {
this.#root = patch(this.#root, diff, (msg) => {
this.#socket?.send(JSON.stringify([Constants.event, msg.tag, msg.data]));
});
}
emit([event, data]) {
this.dispatchEvent(new CustomEvent(event, { detail: data }));
}
disconnectedCallback() {
this.#socket?.close();
}
}
window.customElements.define("lustre-server-component", LustreServerComponent);