Packages
lustre
4.0.0
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
priv/static/lustre-server-component.mjs
// build/dev/javascript/lustre/lustre/internals/constants.mjs
var diff = 0;
var emit = 1;
var init = 2;
var event = 4;
var attrs = 5;
// build/dev/javascript/prelude.mjs
var CustomType = class {
withFields(fields) {
let properties = Object.keys(this).map(
(label) => label in fields ? fields[label] : this[label]
);
return new this.constructor(...properties);
}
};
var List = class {
static fromArray(array, tail) {
let t = tail || new Empty();
return array.reduceRight((xs, x) => new NonEmpty(x, xs), t);
}
[Symbol.iterator]() {
return new ListIterator(this);
}
toArray() {
return [...this];
}
atLeastLength(desired) {
for (let _ of this) {
if (desired <= 0)
return true;
desired--;
}
return desired <= 0;
}
hasLength(desired) {
for (let _ of this) {
if (desired <= 0)
return false;
desired--;
}
return desired === 0;
}
countLength() {
let length2 = 0;
for (let _ of this)
length2++;
return length2;
}
};
var ListIterator = class {
#current;
constructor(current) {
this.#current = current;
}
next() {
if (this.#current instanceof Empty) {
return { done: true };
} else {
let { head, tail } = this.#current;
this.#current = tail;
return { value: head, done: false };
}
}
};
var Empty = class extends List {
};
var NonEmpty = class extends List {
constructor(head, tail) {
super();
this.head = head;
this.tail = tail;
}
};
var Result = class _Result extends CustomType {
static isResult(data) {
return data instanceof _Result;
}
};
var Ok = class extends Result {
constructor(value) {
super();
this[0] = value;
}
isOk() {
return true;
}
};
var Error = class extends Result {
constructor(detail) {
super();
this[0] = detail;
}
isOk() {
return false;
}
};
// build/dev/javascript/gleam_stdlib/dict.mjs
var tempDataView = new DataView(new ArrayBuffer(8));
var SHIFT = 5;
var BUCKET_SIZE = Math.pow(2, SHIFT);
var MASK = BUCKET_SIZE - 1;
var MAX_INDEX_NODE = BUCKET_SIZE / 2;
var MIN_ARRAY_NODE = BUCKET_SIZE / 4;
// build/dev/javascript/gleam_stdlib/gleam/result.mjs
function map2(result, fun) {
if (result.isOk()) {
let x = result[0];
return new Ok(fun(x));
} else {
let e = result[0];
return new Error(e);
}
}
// build/dev/javascript/lustre/vdom.ffi.mjs
function morph(prev, curr, dispatch, parent) {
if (curr?.tag && prev?.nodeType === 1) {
const nodeName = curr.tag.toUpperCase();
const ns = curr.namespace || "http://www.w3.org/1999/xhtml";
if (prev.nodeName === nodeName && prev.namespaceURI == ns) {
return morphElement(prev, curr, dispatch, parent);
} else {
return createElement(prev, curr, dispatch, parent);
}
}
if (curr?.tag) {
return createElement(prev, curr, dispatch, parent);
}
if (typeof curr?.content === "string") {
return prev?.nodeType === 3 ? morphText(prev, curr) : createText(prev, curr);
}
return document.createComment(
[
"[internal lustre error] I couldn't work out how to render this element. This",
"function should only be called internally by lustre's runtime: if you think",
"this is an error, please open an issue at",
"https://github.com/hayleigh-dot-dev/gleam-lustre/issues/new"
].join(" ")
);
}
function patch(root, diff2, dispatch) {
for (const created of diff2[0]) {
const key = created[0];
if (key === "0") {
morph(root, created[1], dispatch, root.parentNode);
} else {
const segments = Array.from(key);
const parentKey = segments.slice(0, -1).join("");
const indexKey = segments.slice(-1)[0];
const prev = root.querySelector(`[data-lustre-key="${key}"]`) ?? root.querySelector(`[data-lustre-key="${parentKey}"]`).childNodes[indexKey];
morph(prev, created[1], dispatch, prev.parentNode);
}
}
for (const removed of diff2[1]) {
const key = removed[0];
const segments = Array.from(key);
const parentKey = segments.slice(0, -1).join("");
const indexKey = segments.slice(-1)[0];
const prev = root.querySelector(`[data-lustre-key="${key}"]`) ?? root.querySelector(`[data-lustre-key="${parentKey}"]`).childNodes[indexKey];
prev.remove();
}
for (const updated of diff2[2]) {
const key = updated[0];
const prev = key === "0" ? root : root.querySelector(`[data-lustre-key="${key}"]`);
prev.$lustre ??= { __registered_events: /* @__PURE__ */ new Set() };
for (const created of updated[0]) {
morphAttr(prev, created.name, created.value, dispatch);
}
for (const removed of updated[1]) {
if (prev.$lustre.__registered_events.has(removed)) {
const event2 = removed.slice(2).toLowerCase();
prev.removeEventListener(event2, prev.$lustre[`${removed}Handler`]);
prev.$lustre.__registered_events.delete(removed);
delete prev.$lustre[removed];
delete prev.$lustre[`${removed}Handler`];
} else {
prev.removeAttribute(removed);
}
}
}
return root;
}
function createElement(prev, curr, dispatch, parent = null) {
const el = curr.namespace ? document.createElementNS(curr.namespace, curr.tag) : document.createElement(curr.tag);
el.$lustre = {
__registered_events: /* @__PURE__ */ new Set()
};
let dangerousUnescapedHtml = "";
for (const attr of curr.attrs) {
if (attr[0] === "class") {
morphAttr(el, attr[0], `${el.className} ${attr[1]}`);
} else if (attr[0] === "style") {
morphAttr(el, attr[0], `${el.style.cssText} ${attr[1]}`);
} else if (attr[0] === "dangerous-unescaped-html") {
dangerousUnescapedHtml += attr[1];
} else if (attr[0] !== "") {
morphAttr(el, attr[0], attr[1], dispatch);
}
}
if (customElements.get(curr.tag)) {
el._slot = curr.children;
} else if (curr.tag === "slot") {
let children = new Empty();
let parentWithSlot = parent;
while (parentWithSlot) {
if (parentWithSlot._slot) {
children = parentWithSlot._slot;
break;
} else {
parentWithSlot = parentWithSlot.parentNode;
}
}
for (const child of children) {
el.appendChild(morph(null, child, dispatch, el));
}
} else if (dangerousUnescapedHtml) {
el.innerHTML = dangerousUnescapedHtml;
} else {
for (const child of curr.children) {
el.appendChild(morph(null, child, dispatch, el));
}
}
if (prev)
prev.replaceWith(el);
return el;
}
function morphElement(prev, curr, dispatch, parent) {
const prevAttrs = prev.attributes;
const currAttrs = /* @__PURE__ */ new Map();
prev.$lustre ??= { __registered_events: /* @__PURE__ */ new Set() };
for (const currAttr of curr.attrs) {
if (currAttr[0] === "class" && currAttrs.has("class")) {
currAttrs.set(currAttr[0], `${currAttrs.get("class")} ${currAttr[1]}`);
} else if (currAttr[0] === "style" && currAttrs.has("style")) {
currAttrs.set(currAttr[0], `${currAttrs.get("style")} ${currAttr[1]}`);
} else if (currAttr[0] === "dangerous-unescaped-html" && currAttrs.has("dangerous-unescaped-html")) {
currAttrs.set(
currAttr[0],
`${currAttrs.get("dangerous-unescaped-html")} ${currAttr[1]}`
);
} else if (currAttr[0] !== "") {
currAttrs.set(currAttr[0], currAttr[1]);
}
}
for (const { name } of prevAttrs) {
if (!currAttrs.has(name)) {
prev.removeAttribute(name);
} else {
const value = currAttrs.get(name);
morphAttr(prev, name, value, dispatch);
currAttrs.delete(name);
}
}
for (const name of prev.$lustre.__registered_events) {
if (!currAttrs.has(name)) {
const event2 = name.slice(2).toLowerCase();
prev.removeEventListener(event2, prev.$lustre[`${name}Handler`]);
prev.$lustre.__registered_events.delete(name);
delete prev.$lustre[name];
delete prev.$lustre[`${name}Handler`];
}
}
for (const [name, value] of currAttrs) {
morphAttr(prev, name, value, dispatch);
}
if (customElements.get(curr.tag)) {
prev._slot = curr.children;
} else if (curr.tag === "slot") {
let prevChild = prev.firstChild;
let currChild = new Empty();
let parentWithSlot = parent;
while (parentWithSlot) {
if (parentWithSlot._slot) {
currChild = parentWithSlot._slot;
break;
} else {
parentWithSlot = parentWithSlot.parentNode;
}
}
while (prevChild) {
if (Array.isArray(currChild) && currChild.length) {
morph(prevChild, currChild.shift(), dispatch, prev);
} else if (currChild.head) {
morph(prevChild, currChild.head, dispatch, prev);
currChild = currChild.tail;
}
prevChild = prevChild.nextSibling;
}
for (const child of currChild) {
prev.appendChild(morph(null, child, dispatch, prev));
}
} else if (currAttrs.has("dangerous-unescaped-html")) {
prev.innerHTML = currAttrs.get("dangerous-unescaped-html");
} else {
let prevChild = prev.firstChild;
let currChild = curr.children;
while (prevChild) {
if (Array.isArray(currChild) && currChild.length) {
const next = prevChild.nextSibling;
morph(prevChild, currChild.shift(), dispatch, prev);
prevChild = next;
} else if (currChild.head) {
const next = prevChild.nextSibling;
morph(prevChild, currChild.head, dispatch, prev);
currChild = currChild.tail;
prevChild = next;
} else {
const next = prevChild.nextSibling;
prevChild.remove();
prevChild = next;
}
}
for (const child of currChild) {
prev.appendChild(morph(null, child, dispatch, prev));
}
}
return prev;
}
function morphAttr(el, name, value, dispatch) {
switch (typeof value) {
case (name.startsWith("data-lustre-on-") && "string"): {
if (!value) {
el.removeAttribute(name);
el.removeEventListener(event2, el.$lustre[`${name}Handler`]);
break;
}
if (el.hasAttribute(name))
break;
const event2 = name.slice(15).toLowerCase();
const handler = (e) => dispatch(serverEventHandler(e));
if (el.$lustre[`${name}Handler`]) {
el.removeEventListener(event2, el.$lustre[`${name}Handler`]);
}
el.addEventListener(event2, handler);
el.$lustre[name] = value;
el.$lustre[`${name}Handler`] = handler;
el.$lustre.__registered_events.add(name);
el.setAttribute(name, value);
break;
}
case "string":
if (name === "value")
el.value = value;
if (value === "") {
el.removeAttribute(name);
} else {
el.setAttribute(name, value);
}
break;
case (name.startsWith("on") && "function"): {
if (el.$lustre[name] === value)
break;
const event2 = name.slice(2).toLowerCase();
const handler = (e) => map2(value(e), dispatch);
if (el.$lustre[`${name}Handler`]) {
el.removeEventListener(event2, el.$lustre[`${name}Handler`]);
}
el.addEventListener(event2, handler);
el.$lustre[name] = value;
el.$lustre[`${name}Handler`] = handler;
el.$lustre.__registered_events.add(name);
break;
}
default:
el[name] = value;
}
}
function createText(prev, curr) {
const el = document.createTextNode(curr.content);
if (prev)
prev.replaceWith(el);
return el;
}
function morphText(prev, curr) {
const prevValue = prev.nodeValue;
const currValue = curr.content;
if (!currValue) {
prev?.remove();
return null;
}
if (prevValue !== currValue)
prev.nodeValue = currValue;
return prev;
}
function serverEventHandler(event2) {
const el = event2.target;
const tag = el.getAttribute(`data-lustre-on-${event2.type}`);
const data = JSON.parse(el.getAttribute("data-lustre-data") || "{}");
const include = JSON.parse(el.getAttribute("data-lustre-include") || "[]");
switch (event2.type) {
case "input":
case "change":
include.push("target.value");
break;
}
return {
tag,
data: include.reduce((data2, property) => {
const path = property.split(".");
for (let i = 0, o = data2, e = event2; i < path.length; i++) {
if (i === path.length - 1) {
o[path[i]] = e[path[i]];
} else {
o[path[i]] ??= {};
e = e[path[i]];
o = o[path[i]];
}
}
return data2;
}, data)
};
}
// src/server-component.mjs
var LustreServerComponent = class 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([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 diff:
return this.diff(payload);
case emit:
return this.emit(payload);
case init:
return this.init(payload);
}
}
init([attrs2, vdom]) {
const initial = [];
for (const attr of attrs2) {
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([attrs, [[attr, value]]])
);
}
}
});
}
this.#observer.observe(this, {
attributeFilter: attrs2,
attributeOldValue: true,
attributes: true,
characterData: false,
characterDataOldValue: false,
childList: false,
subtree: false
});
this.morph(vdom);
if (initial.length) {
this.#socket?.send(JSON.stringify([attrs, initial]));
}
}
morph(vdom) {
this.#root = morph(this.#root, vdom, (msg) => {
this.#socket?.send(JSON.stringify([event, msg.tag, msg.data]));
});
}
diff([diff2]) {
this.#root = patch(this.#root, diff2, (msg) => {
this.#socket?.send(JSON.stringify([event, msg.tag, msg.data]));
});
}
emit([event2, data]) {
this.dispatchEvent(new CustomEvent(event2, { detail: data }));
}
disconnectedCallback() {
this.#socket?.close();
}
};
window.customElements.define("lustre-server-component", LustreServerComponent);
export {
LustreServerComponent
};