aboutsummaryrefslogtreecommitdiff
path: root/src/runtime.ffi.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'src/runtime.ffi.mjs')
-rw-r--r--src/runtime.ffi.mjs239
1 files changed, 239 insertions, 0 deletions
diff --git a/src/runtime.ffi.mjs b/src/runtime.ffi.mjs
new file mode 100644
index 0000000..dd02f8b
--- /dev/null
+++ b/src/runtime.ffi.mjs
@@ -0,0 +1,239 @@
+import { Empty } from "./gleam.mjs";
+import { map as result_map } from "../gleam_stdlib/gleam/result.mjs";
+
+export function morph(prev, curr, dispatch, parent) {
+ if (curr[3]) {
+ return prev?.nodeType === 1 &&
+ prev.nodeName === curr[0].toUpperCase() &&
+ prev.namespaceURI === curr[3]
+ ? morphElement(prev, curr, curr[3], dispatch, parent)
+ : createElement(prev, curr, curr[3], dispatch, parent);
+ }
+
+ if (curr[2]) {
+ return prev?.nodeType === 1 && prev.nodeName === curr[0].toUpperCase()
+ ? morphElement(prev, curr, null, dispatch, parent)
+ : createElement(prev, curr, null, dispatch, parent);
+ }
+
+ if (curr[0] && typeof curr[0] === "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(" ")
+ );
+}
+
+// ELEMENTS --------------------------------------------------------------------
+
+function createElement(prev, curr, ns, dispatch, parent = null) {
+ const el = ns
+ ? document.createElementNS(ns, curr[0])
+ : document.createElement(curr[0]);
+
+ el.$lustre = {};
+
+ let attr = curr[1];
+ while (attr.head) {
+ morphAttr(
+ el,
+ attr.head[0],
+ attr.head[0] === "class" && el.className
+ ? `${el.className} ${attr.head[1]}`
+ : attr.head[1],
+ dispatch
+ );
+
+ attr = attr.tail;
+ }
+
+ if (customElements.get(curr[0])) {
+ el._slot = curr[2];
+ } else if (curr[0] === "slot") {
+ let child = new Empty();
+ let parentWithSlot = parent;
+
+ while (parentWithSlot) {
+ if (parentWithSlot._slot) {
+ child = parentWithSlot._slot;
+ break;
+ } else {
+ parentWithSlot = parentWithSlot.parentNode;
+ }
+ }
+
+ while (child.head) {
+ el.appendChild(morph(null, child.head, dispatch, el));
+ child = child.tail;
+ }
+ } else {
+ let child = curr[2];
+ while (child.head) {
+ el.appendChild(morph(null, child.head, dispatch, el));
+ child = child.tail;
+ }
+
+ if (prev) prev.replaceWith(el);
+ }
+
+ return el;
+}
+
+function morphElement(prev, curr, ns, dispatch, parent) {
+ const prevAttrs = prev.attributes;
+ const currAttrs = new Map();
+
+ let currAttr = curr[1];
+ while (currAttr.head) {
+ currAttrs.set(
+ currAttr.head[0],
+ currAttr.head[0] === "class" && currAttrs.has("class")
+ ? `${currAttrs.get("class")} ${currAttr.head[1]}`
+ : currAttr.head[1]
+ );
+
+ currAttr = currAttr.tail;
+ }
+
+ for (const { name, value: prevValue } of prevAttrs) {
+ if (!currAttrs.has(name)) {
+ prev.removeAttribute(name);
+ } else {
+ const value = currAttrs.get(name);
+
+ if (value !== prevValue) {
+ morphAttr(prev, name, value, dispatch);
+ currAttrs.delete(name);
+ }
+ }
+ }
+
+ for (const [name, value] of currAttrs) {
+ morphAttr(prev, name, value, dispatch);
+ }
+
+ if (customElements.get(curr[0])) {
+ prev._slot = curr[2];
+ } else if (curr[0] === "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 (currChild.head) {
+ morph(prevChild, currChild.head, dispatch, prev);
+ currChild = currChild.tail;
+ }
+
+ prevChild = prevChild.nextSibling;
+ }
+
+ while (currChild.head) {
+ prev.appendChild(morph(null, currChild.head, dispatch, prev));
+ currChild = currChild.tail;
+ }
+ } else {
+ let prevChild = prev.firstChild;
+ let currChild = curr[2];
+
+ while (prevChild) {
+ 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;
+ }
+ }
+
+ while (currChild.head) {
+ prev.appendChild(morph(null, currChild.head, dispatch, prev));
+ currChild = currChild.tail;
+ }
+ }
+
+ return prev;
+}
+
+// ATTRIBUTES ------------------------------------------------------------------
+
+function morphAttr(el, name, value, dispatch) {
+ switch (typeof value) {
+ case "string":
+ if (el.getAttribute(name) !== value) el.setAttribute(name, value);
+ if (value === "") el.removeAttribute(name);
+ if (name === "value" && el.value !== value) el.value = value;
+ break;
+
+ // Event listeners need to be handled slightly differently because we need
+ // to be able to support custom events. We
+ case name.startsWith("on") && "function": {
+ if (el.$lustre[name] === value) break;
+
+ const event = name.slice(2).toLowerCase();
+ const handler = (e) => result_map(value(e), dispatch);
+
+ if (el.$lustre[`${name}Handler`]) {
+ el.removeEventListener(event, el.$lustre[`${name}Handler`]);
+ }
+
+ el.addEventListener(event, handler);
+
+ el.$lustre[name] = value;
+ el.$lustre[`${name}Handler`] = handler;
+
+ break;
+ }
+
+ default:
+ el[name] = value;
+ }
+}
+
+// TEXT ------------------------------------------------------------------------
+
+function createText(prev, curr) {
+ if (!curr[0]) {
+ prev?.remove();
+ return null;
+ }
+
+ const el = document.createTextNode(curr[0]);
+
+ if (prev) prev.replaceWith(el);
+ return el;
+}
+
+function morphText(prev, curr) {
+ const prevValue = prev.nodeValue;
+ const currValue = curr[0];
+
+ if (!currValue) {
+ prev?.remove();
+ return null;
+ }
+
+ if (prevValue !== currValue) prev.nodeValue = currValue;
+
+ return prev;
+}