aboutsummaryrefslogtreecommitdiff
path: root/lib/src/runtime.ffi.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/src/runtime.ffi.mjs')
-rw-r--r--lib/src/runtime.ffi.mjs230
1 files changed, 230 insertions, 0 deletions
diff --git a/lib/src/runtime.ffi.mjs b/lib/src/runtime.ffi.mjs
new file mode 100644
index 0000000..7325583
--- /dev/null
+++ b/lib/src/runtime.ffi.mjs
@@ -0,0 +1,230 @@
+import { element, namespaced, text } from "./lustre/element.mjs";
+import { List, Empty } from "./gleam.mjs";
+import { Some, None } from "../gleam_stdlib/gleam/option.mjs";
+
+const Element = element("").constructor;
+const ElementNs = namespaced("", "").constructor;
+const Text = text("").constructor;
+
+export function morph(prev, curr, parent) {
+ if (curr instanceof ElementNs)
+ return prev?.nodeType === 1 &&
+ prev.nodeName === curr[0].toUpperCase() &&
+ prev.namespaceURI === curr[3]
+ ? morphElement(prev, curr, curr[3], parent)
+ : createElement(prev, curr, curr[3], parent);
+
+ if (curr instanceof Element) {
+ return prev?.nodeType === 1 && prev.nodeName === curr[0].toUpperCase()
+ ? morphElement(prev, curr, null, parent)
+ : createElement(prev, curr, null, parent);
+ }
+
+ if (curr instanceof Text) {
+ 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.come/hayleigh-dot-dev/lustre/issues/new.",
+ ].join(" ")
+ );
+}
+
+// ELEMENTS --------------------------------------------------------------------
+
+function createElement(prev, curr, ns, parent = null) {
+ const el = ns
+ ? document.createElementNS(ns, curr[0])
+ : document.createElement(curr[0]);
+
+ let attr = curr[1];
+ while (attr.head) {
+ morphAttr(el, attr.head[0], attr.head[1]);
+ 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, el));
+ child = child.tail;
+ }
+ } else {
+ let child = curr[2];
+ while (child.head) {
+ el.appendChild(morph(null, child.head, el));
+ child = child.tail;
+ }
+
+ if (prev) prev.replaceWith(el);
+ }
+ return el;
+}
+
+function morphElement(prev, curr, ns, parent) {
+ const prevAttrs = prev.attributes;
+ const currAttrs = new Map();
+
+ let currAttr = curr[1];
+ while (currAttr.head) {
+ currAttrs.set(currAttr.head[0], 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);
+ currAttrs.delete(name);
+ }
+ }
+ }
+
+ for (const [name, value] of currAttrs) {
+ morphAttr(prev, name, value);
+ }
+
+ 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, prev);
+ currChild = currChild.tail;
+ }
+
+ prevChild = prevChild.nextSibling;
+ }
+
+ while (currChild.head) {
+ prev.appendChild(morph(null, currChild.head, 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, prev);
+ currChild = currChild.tail;
+ prevChild = next;
+ } else {
+ const next = prevChild.nextSibling;
+ prevChild.remove();
+ prevChild = next;
+ }
+ }
+
+ while (currChild.head) {
+ prev.appendChild(morph(null, currChild.head, prev));
+ currChild = currChild.tail;
+ }
+ }
+
+ return prev;
+}
+
+// ATTRIBUTES ------------------------------------------------------------------
+
+function morphAttr(el, name, value) {
+ switch (typeof value) {
+ case "string":
+ el.setAttribute(name, value);
+ break;
+
+ // Boolean attributes work a bit differently in HTML. Their presence always
+ // implies true: to set an attribute to false you need to remove it entirely.
+ case "boolean":
+ value ? el.setAttribute(name, name) : el.removeAttribute(name);
+ 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": {
+ const event = name.slice(2).toLowerCase();
+
+ if (el[`_${name}`] === value) break;
+
+ el.removeEventListener(event, el[`_${name}`]);
+ el.addEventListener(event, value);
+ el[`_${name}`] = value;
+ break;
+ }
+
+ default: {
+ el[name] = toJsValue(value);
+ }
+ }
+}
+
+function toJsValue(value) {
+ if (value instanceof List) {
+ return value.toArray().map(toJsValue);
+ }
+
+ if (value instanceof Some) {
+ return toJsValue(value[0]);
+ }
+
+ if (value instanceof None) {
+ return null;
+ }
+
+ return value;
+}
+
+// TEXT ------------------------------------------------------------------------
+
+function createText(prev, curr) {
+ 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 (prevValue !== currValue) prev.nodeValue = currValue;
+
+ return prev;
+}