aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2023-07-19 10:48:10 +0100
committerHayleigh Thompson <me@hayleigh.dev>2023-07-19 10:48:10 +0100
commit42067167e278fef921a07addf974f2b278944222 (patch)
treee6b1213995af938eaf2ca0736112acc7d994144a
parent7b007a072f39cd69ff4ba16c5f766aa26fab47c9 (diff)
downloadlustre-42067167e278fef921a07addf974f2b278944222.tar.gz
lustre-42067167e278fef921a07addf974f2b278944222.zip
:alembic: Maybe we can just roll our own DOM patching?
-rw-r--r--src/lustre.ffi.mjs8
-rw-r--r--src/runtime.ffi.mjs988
2 files changed, 79 insertions, 917 deletions
diff --git a/src/lustre.ffi.mjs b/src/lustre.ffi.mjs
index 3a6556e..b57026f 100644
--- a/src/lustre.ffi.mjs
+++ b/src/lustre.ffi.mjs
@@ -1,4 +1,4 @@
-import { morphdom } from "./runtime.ffi.mjs";
+import { morph } from "./runtime.ffi.mjs";
import { Ok, Error } from "./gleam.mjs";
import { map } from "./lustre/element.mjs";
@@ -14,6 +14,10 @@ export class App {
#willUpdate = false;
#didUpdate = false;
+ #init = null;
+ #update = null;
+ #view = null;
+
constructor(init, update, render) {
this.#init = init;
this.#update = update;
@@ -51,7 +55,7 @@ export class App {
const node = this.#view(this.#state);
const vdom = map(node, (msg) => this.dispatch(msg));
- morphdom(this.#root.firstChild, vdom);
+ morph(this.#root, vdom);
}
#tick() {
diff --git a/src/runtime.ffi.mjs b/src/runtime.ffi.mjs
index c349a70..355c923 100644
--- a/src/runtime.ffi.mjs
+++ b/src/runtime.ffi.mjs
@@ -1,948 +1,106 @@
import { h, t } from "./lustre/element.mjs";
+import { fold, each } from "../gleam_stdlib/gleam/list.mjs";
-// This file is vendored from https://github.com/patrick-steele-idem/morphdom/
-// and is licensed under the MIT license. For a copy of the original license
-// head on over to:
-//
-// https://github.com/patrick-steele-idem/morphdom/blob/master/LICENSE
-//
+const Element = h("").constructor;
+const Text = t("").constructor;
-var DOCUMENT_FRAGMENT_NODE = 11;
+export function morph(prev, curr) {
+ if (curr instanceof Element) {
+ if (prev?.nodeType === 1 && prev.nodeName !== curr[0].toUpperCase()) {
+ return morphElement(prev, curr);
+ } else {
+ const el = document.createElement(curr[0]);
-function morphAttrs(fromNode, toNode) {
- var toNodeAttrs = toNode.attributes;
- var attr;
- var attrName;
- var attrNamespaceURI;
- var attrValue;
- var fromValue;
+ each(curr[1], (attr) => {
+ const name = attr[0];
+ const value = attr[1];
- // document-fragments dont have attributes so lets not do anything
- if (
- toNode.nodeType === DOCUMENT_FRAGMENT_NODE ||
- fromNode.nodeType === DOCUMENT_FRAGMENT_NODE
- ) {
- return;
- }
+ morphAttr(el, name, value);
+ });
- // update attributes on original DOM element
- for (var i = toNodeAttrs.length - 1; i >= 0; i--) {
- attr = toNodeAttrs[i];
- attrName = attr.name;
- attrNamespaceURI = attr.namespaceURI;
- attrValue = attr.value;
+ each(curr[2], (child) => {
+ el.appendChild(morph(null, child));
+ });
- if (attrNamespaceURI) {
- attrName = attr.localName || attrName;
- fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);
+ if (prev) prev.replaceWith(el);
- if (fromValue !== attrValue) {
- if (attr.prefix === "xmlns") {
- attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix
- }
- fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);
- }
- } else {
- fromValue = fromNode.getAttribute(attrName);
-
- if (fromValue !== attrValue) {
- fromNode.setAttribute(attrName, attrValue);
- }
+ return el;
}
}
- // Remove any extra attributes found on the original DOM element that
- // weren't found on the target element.
- var fromNodeAttrs = fromNode.attributes;
-
- for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {
- attr = fromNodeAttrs[d];
- attrName = attr.name;
- attrNamespaceURI = attr.namespaceURI;
-
- if (attrNamespaceURI) {
- attrName = attr.localName || attrName;
-
- if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {
- fromNode.removeAttributeNS(attrNamespaceURI, attrName);
- }
+ if (curr instanceof Text) {
+ if (prev?.nodeType === 3) {
+ return morphText(prev, curr);
} else {
- if (!toNode.hasAttribute(attrName)) {
- fromNode.removeAttribute(attrName);
- }
+ const el = document.createTextNode(curr[0]);
+ if (prev) prev.replaceWith(el);
+ return el;
}
}
-}
-
-var range; // Create a range object for efficently rendering strings to elements.
-var NS_XHTML = "http://www.w3.org/1999/xhtml";
-
-var doc = typeof document === "undefined" ? undefined : document;
-var HAS_TEMPLATE_SUPPORT = !!doc && "content" in doc.createElement("template");
-var HAS_RANGE_SUPPORT =
- !!doc && doc.createRange && "createContextualFragment" in doc.createRange();
-
-function createFragmentFromTemplate(str) {
- var template = doc.createElement("template");
- template.innerHTML = str;
- return template.content.childNodes[0];
-}
-
-function createFragmentFromRange(str) {
- if (!range) {
- range = doc.createRange();
- range.selectNode(doc.body);
- }
-
- var fragment = range.createContextualFragment(str);
- return fragment.childNodes[0];
-}
-
-function createFragmentFromWrap(str) {
- var fragment = doc.createElement("body");
- fragment.innerHTML = str;
- return fragment.childNodes[0];
-}
-
-/**
- * This is about the same
- * var html = new DOMParser().parseFromString(str, 'text/html');
- * return html.body.firstChild;
- *
- * @method toElement
- * @param {String} str
- */
-function toElement(str) {
- str = str.trim();
- if (HAS_TEMPLATE_SUPPORT) {
- // avoid restrictions on content for things like `<tr><th>Hi</th></tr>` which
- // createContextualFragment doesn't support
- // <template> support not available in IE
- return createFragmentFromTemplate(str);
- } else if (HAS_RANGE_SUPPORT) {
- return createFragmentFromRange(str);
- }
- return createFragmentFromWrap(str);
+ return null;
}
-/**
- * Returns true if two node's names are the same.
- *
- * NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same
- * nodeName and different namespace URIs.
- *
- * @param {Element} a
- * @param {Element} b The target element
- * @return {boolean}
- */
-function compareNodeNames(fromEl, toEl) {
- var fromNodeName = fromEl.nodeName;
- var toNodeName = toEl.nodeName;
- var fromCodeStart, toCodeStart;
+function morphElement(prev, curr) {
+ const prevAttrs = prev.attributes;
+ const currAttrs = fold(curr[1], new Map(), (acc, attr) => {
+ const name = attr[0];
+ const value = attr[1];
- if (fromNodeName === toNodeName) {
- return true;
- }
+ acc.set(name, value);
+ return acc;
+ });
+ const currChildren = curr[2].toArray();
- fromCodeStart = fromNodeName.charCodeAt(0);
- toCodeStart = toNodeName.charCodeAt(0);
+ for (const { name, value: prevValue } of prevAttrs) {
+ if (!currAttrs.has(name)) prev.removeAttribute(name);
+ const value = currAttrs.get(name);
- // If the target element is a virtual DOM node or SVG node then we may
- // need to normalize the tag name before comparing. Normal HTML elements that are
- // in the "http://www.w3.org/1999/xhtml"
- // are converted to upper case
- if (fromCodeStart <= 90 && toCodeStart >= 97) {
- // from is upper and to is lower
- return fromNodeName === toNodeName.toUpperCase();
- } else if (toCodeStart <= 90 && fromCodeStart >= 97) {
- // to is upper and from is lower
- return toNodeName === fromNodeName.toUpperCase();
- } else {
- return false;
+ if (value !== prevValue) {
+ morphAttr(prev, name, value);
+ currAttrs.delete(name);
+ }
}
-}
-
-/**
- * Create an element, optionally with a known namespace URI.
- *
- * @param {string} name the element name, e.g. 'div' or 'svg'
- * @param {string} [namespaceURI] the element's namespace URI, i.e. the value of
- * its `xmlns` attribute or its inferred namespace.
- *
- * @return {Element}
- */
-function createElementNS(name, namespaceURI) {
- return !namespaceURI || namespaceURI === NS_XHTML
- ? doc.createElement(name)
- : doc.createElementNS(namespaceURI, name);
-}
-/**
- * Copies the children of one DOM element to another DOM element
- */
-function moveChildren(fromEl, toEl) {
- var curChild = fromEl.firstChild;
- while (curChild) {
- var nextChild = curChild.nextSibling;
- toEl.appendChild(curChild);
- curChild = nextChild;
+ for (const [name, value] of currAttrs) {
+ morphAttr(prev, name, value);
}
- return toEl;
-}
-function syncBooleanAttrProp(fromEl, toEl, name) {
- if (fromEl[name] !== toEl[name]) {
- fromEl[name] = toEl[name];
- if (fromEl[name]) {
- fromEl.setAttribute(name, "");
+ for (let child = prev.firstChild; child; child = child.nextSibling) {
+ if (currChildren.length) {
+ morph(child, currChildren.shift());
} else {
- fromEl.removeAttribute(name);
+ prev.removeChild(child);
}
}
-}
-
-var specialElHandlers = {
- OPTION: function (fromEl, toEl) {
- var parentNode = fromEl.parentNode;
- if (parentNode) {
- var parentName = parentNode.nodeName.toUpperCase();
- if (parentName === "OPTGROUP") {
- parentNode = parentNode.parentNode;
- parentName = parentNode && parentNode.nodeName.toUpperCase();
- }
- if (parentName === "SELECT" && !parentNode.hasAttribute("multiple")) {
- if (fromEl.hasAttribute("selected") && !toEl.selected) {
- // Workaround for MS Edge bug where the 'selected' attribute can only be
- // removed if set to a non-empty value:
- // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12087679/
- fromEl.setAttribute("selected", "selected");
- fromEl.removeAttribute("selected");
- }
- // We have to reset select element's selectedIndex to -1, otherwise setting
- // fromEl.selected using the syncBooleanAttrProp below has no effect.
- // The correct selectedIndex will be set in the SELECT special handler below.
- parentNode.selectedIndex = -1;
- }
- }
- syncBooleanAttrProp(fromEl, toEl, "selected");
- },
- /**
- * The "value" attribute is special for the <input> element since it sets
- * the initial value. Changing the "value" attribute without changing the
- * "value" property will have no effect since it is only used to the set the
- * initial value. Similar for the "checked" attribute, and "disabled".
- */
- INPUT: function (fromEl, toEl) {
- syncBooleanAttrProp(fromEl, toEl, "checked");
- syncBooleanAttrProp(fromEl, toEl, "disabled");
-
- if (fromEl.value !== toEl.value) {
- fromEl.value = toEl.value;
- }
-
- if (!toEl.hasAttribute("value")) {
- fromEl.removeAttribute("value");
- }
- },
-
- TEXTAREA: function (fromEl, toEl) {
- var newValue = toEl.value;
- if (fromEl.value !== newValue) {
- fromEl.value = newValue;
- }
-
- var firstChild = fromEl.firstChild;
- if (firstChild) {
- // Needed for IE. Apparently IE sets the placeholder as the
- // node value and vise versa. This ignores an empty update.
- var oldValue = firstChild.nodeValue;
- if (
- oldValue == newValue ||
- (!newValue && oldValue == fromEl.placeholder)
- ) {
- return;
- }
-
- firstChild.nodeValue = newValue;
- }
- },
- SELECT: function (fromEl, toEl) {
- if (!toEl.hasAttribute("multiple")) {
- var selectedIndex = -1;
- var i = 0;
- // We have to loop through children of fromEl, not toEl since nodes can be moved
- // from toEl to fromEl directly when morphing.
- // At the time this special handler is invoked, all children have already been morphed
- // and appended to / removed from fromEl, so using fromEl here is safe and correct.
- var curChild = fromEl.firstChild;
- var optgroup;
- var nodeName;
- while (curChild) {
- nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();
- if (nodeName === "OPTGROUP") {
- optgroup = curChild;
- curChild = optgroup.firstChild;
- } else {
- if (nodeName === "OPTION") {
- if (curChild.hasAttribute("selected")) {
- selectedIndex = i;
- break;
- }
- i++;
- }
- curChild = curChild.nextSibling;
- if (!curChild && optgroup) {
- curChild = optgroup.nextSibling;
- optgroup = null;
- }
- }
- }
-
- fromEl.selectedIndex = selectedIndex;
- }
- },
-};
-
-export const ELEMENT_NODE = 1;
-export const DOCUMENT_FRAGMENT_NODE$1 = 11;
-export const TEXT_NODE = 3;
-export const COMMENT_NODE = 8;
-
-function noop() {}
-
-function defaultGetNodeKey(node) {
- if (node) {
- return (node.getAttribute && node.getAttribute("id")) || node.id;
+ for (const child of currChildren) {
+ prev.appendChild(morph(null, child));
}
-}
-
-function morphdomFactory(morphAttrs) {
- return function morphdom(fromNode, toNode, options) {
- if (!options) {
- options = {};
- }
-
- if (typeof toNode === "string") {
- if (
- fromNode.nodeName === "#document" ||
- fromNode.nodeName === "HTML" ||
- fromNode.nodeName === "BODY"
- ) {
- var toNodeHtml = toNode;
- toNode = doc.createElement("html");
- toNode.innerHTML = toNodeHtml;
- } else {
- toNode = toElement(toNode);
- }
- } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
- toNode = toNode.firstElementChild;
- }
-
- var getNodeKey = options.getNodeKey || defaultGetNodeKey;
- var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
- var onNodeAdded = options.onNodeAdded || noop;
- var onBeforeElUpdated = options.onBeforeElUpdated || noop;
- var onElUpdated = options.onElUpdated || noop;
- var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
- var onNodeDiscarded = options.onNodeDiscarded || noop;
- var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
- var skipFromChildren = options.skipFromChildren || noop;
- var addChild =
- options.addChild ||
- function (parent, child) {
- return parent.appendChild(child);
- };
- var childrenOnly = options.childrenOnly === true;
-
- // This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
- var fromNodesLookup = Object.create(null);
- var keyedRemovalList = [];
-
- function addKeyedRemoval(key) {
- keyedRemovalList.push(key);
- }
-
- function walkDiscardedChildNodes(node, skipKeyedNodes) {
- if (node.nodeType === ELEMENT_NODE) {
- var curChild = node.firstChild;
- while (curChild) {
- var key = undefined;
-
- if (skipKeyedNodes && (key = getNodeKey(curChild))) {
- // If we are skipping keyed nodes then we add the key
- // to a list so that it can be handled at the very end.
- addKeyedRemoval(key);
- } else {
- // Only report the node as discarded if it is not keyed. We do this because
- // at the end we loop through all keyed elements that were unmatched
- // and then discard them in one final pass.
- onNodeDiscarded(curChild);
- if (curChild.firstChild) {
- walkDiscardedChildNodes(curChild, skipKeyedNodes);
- }
- }
-
- curChild = curChild.nextSibling;
- }
- }
- }
-
- /**
- * Removes a DOM node out of the original DOM
- *
- * @param {Node} node The node to remove
- * @param {Node} parentNode The nodes parent
- * @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
- * @return {undefined}
- */
- function removeNode(node, parentNode, skipKeyedNodes) {
- if (onBeforeNodeDiscarded(node) === false) {
- return;
- }
-
- if (parentNode) {
- parentNode.removeChild(node);
- }
-
- onNodeDiscarded(node);
- walkDiscardedChildNodes(node, skipKeyedNodes);
- }
-
- function indexTree(node) {
- if (
- node.nodeType === ELEMENT_NODE ||
- node.nodeType === DOCUMENT_FRAGMENT_NODE$1
- ) {
- var curChild = node.firstChild;
- while (curChild) {
- var key = getNodeKey(curChild);
- if (key) {
- fromNodesLookup[key] = curChild;
- }
-
- // Walk recursively
- indexTree(curChild);
-
- curChild = curChild.nextSibling;
- }
- }
- }
-
- indexTree(fromNode);
-
- function handleNodeAdded(el) {
- onNodeAdded(el);
-
- var curChild = el.firstChild;
- while (curChild) {
- var nextSibling = curChild.nextSibling;
-
- var key = getNodeKey(curChild);
- if (key) {
- var unmatchedFromEl = fromNodesLookup[key];
- // if we find a duplicate #id node in cache, replace `el` with cache value
- // and morph it to the child node.
- if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
- curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
- morphEl(unmatchedFromEl, curChild);
- } else {
- handleNodeAdded(curChild);
- }
- } else {
- // recursively call for curChild and it's children to see if we find something in
- // fromNodesLookup
- handleNodeAdded(curChild);
- }
-
- curChild = nextSibling;
- }
- }
-
- function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
- // We have processed all of the "to nodes". If curFromNodeChild is
- // non-null then we still have some from nodes left over that need
- // to be removed
- while (curFromNodeChild) {
- var fromNextSibling = curFromNodeChild.nextSibling;
- if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
- // Since the node is keyed it might be matched up later so we defer
- // the actual removal to later
- addKeyedRemoval(curFromNodeKey);
- } else {
- // NOTE: we skip nested keyed nodes from being removed since there is
- // still a chance they will be matched up later
- removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
- }
- curFromNodeChild = fromNextSibling;
- }
- }
-
- function morphEl(fromEl, toEl, childrenOnly) {
- var toElKey = getNodeKey(toEl);
- if (toElKey) {
- // If an element with an ID is being morphed then it will be in the final
- // DOM so clear it out of the saved elements collection
- delete fromNodesLookup[toElKey];
- }
-
- if (!childrenOnly) {
- // optional
- if (onBeforeElUpdated(fromEl, toEl) === false) {
- return;
- }
-
- // update attributes on original DOM element first
- morphAttrs(fromEl, toEl);
- // optional
- onElUpdated(fromEl);
-
- if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
- return;
- }
- }
-
- if (fromEl.nodeName !== "TEXTAREA") {
- morphChildren(fromEl, toEl);
- } else {
- specialElHandlers.TEXTAREA(fromEl, toEl);
- }
- }
-
- function morphChildren(fromEl, toEl) {
- var skipFrom = skipFromChildren(fromEl);
- var curToNodeChild = toEl.firstChild;
- var curFromNodeChild = fromEl.firstChild;
- var curToNodeKey;
- var curFromNodeKey;
-
- var fromNextSibling;
- var toNextSibling;
- var matchingFromEl;
-
- // walk the children
- outer: while (curToNodeChild) {
- toNextSibling = curToNodeChild.nextSibling;
- curToNodeKey = getNodeKey(curToNodeChild);
-
- // walk the fromNode children all the way through
- while (!skipFrom && curFromNodeChild) {
- fromNextSibling = curFromNodeChild.nextSibling;
-
- if (
- curToNodeChild.isSameNode &&
- curToNodeChild.isSameNode(curFromNodeChild)
- ) {
- curToNodeChild = toNextSibling;
- curFromNodeChild = fromNextSibling;
- continue outer;
- }
-
- curFromNodeKey = getNodeKey(curFromNodeChild);
-
- var curFromNodeType = curFromNodeChild.nodeType;
-
- // this means if the curFromNodeChild doesnt have a match with the curToNodeChild
- var isCompatible = undefined;
-
- if (curFromNodeType === curToNodeChild.nodeType) {
- if (curFromNodeType === ELEMENT_NODE) {
- // Both nodes being compared are Element nodes
-
- if (curToNodeKey) {
- // The target node has a key so we want to match it up with the correct element
- // in the original DOM tree
- if (curToNodeKey !== curFromNodeKey) {
- // The current element in the original DOM tree does not have a matching key so
- // let's check our lookup to see if there is a matching element in the original
- // DOM tree
- if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
- if (fromNextSibling === matchingFromEl) {
- // Special case for single element removals. To avoid removing the original
- // DOM node out of the tree (since that can break CSS transitions, etc.),
- // we will instead discard the current node and wait until the next
- // iteration to properly match up the keyed target element with its matching
- // element in the original tree
- isCompatible = false;
- } else {
- // We found a matching keyed element somewhere in the original DOM tree.
- // Let's move the original DOM node into the current position and morph
- // it.
-
- // NOTE: We use insertBefore instead of replaceChild because we want to go through
- // the `removeNode()` function for the node that is being discarded so that
- // all lifecycle hooks are correctly invoked
- fromEl.insertBefore(matchingFromEl, curFromNodeChild);
-
- // fromNextSibling = curFromNodeChild.nextSibling;
-
- if (curFromNodeKey) {
- // Since the node is keyed it might be matched up later so we defer
- // the actual removal to later
- addKeyedRemoval(curFromNodeKey);
- } else {
- // NOTE: we skip nested keyed nodes from being removed since there is
- // still a chance they will be matched up later
- removeNode(
- curFromNodeChild,
- fromEl,
- true /* skip keyed nodes */
- );
- }
-
- curFromNodeChild = matchingFromEl;
- }
- } else {
- // The nodes are not compatible since the "to" node has a key and there
- // is no matching keyed node in the source tree
- isCompatible = false;
- }
- }
- } else if (curFromNodeKey) {
- // The original has a key
- isCompatible = false;
- }
-
- isCompatible =
- isCompatible !== false &&
- compareNodeNames(curFromNodeChild, curToNodeChild);
- if (isCompatible) {
- // We found compatible DOM elements so transform
- // the current "from" node to match the current
- // target DOM node.
- // MORPH
- morphEl(curFromNodeChild, curToNodeChild);
- }
- } else if (
- curFromNodeType === TEXT_NODE ||
- curFromNodeType == COMMENT_NODE
- ) {
- // Both nodes being compared are Text or Comment nodes
- isCompatible = true;
- // Simply update nodeValue on the original node to
- // change the text value
- if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
- curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
- }
- }
- }
-
- if (isCompatible) {
- // Advance both the "to" child and the "from" child since we found a match
- // Nothing else to do as we already recursively called morphChildren above
- curToNodeChild = toNextSibling;
- curFromNodeChild = fromNextSibling;
- continue outer;
- }
-
- // No compatible match so remove the old node from the DOM and continue trying to find a
- // match in the original DOM. However, we only do this if the from node is not keyed
- // since it is possible that a keyed node might match up with a node somewhere else in the
- // target tree and we don't want to discard it just yet since it still might find a
- // home in the final DOM tree. After everything is done we will remove any keyed nodes
- // that didn't find a home
- if (curFromNodeKey) {
- // Since the node is keyed it might be matched up later so we defer
- // the actual removal to later
- addKeyedRemoval(curFromNodeKey);
- } else {
- // NOTE: we skip nested keyed nodes from being removed since there is
- // still a chance they will be matched up later
- removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
- }
-
- curFromNodeChild = fromNextSibling;
- } // END: while(curFromNodeChild) {}
-
- // If we got this far then we did not find a candidate match for
- // our "to node" and we exhausted all of the children "from"
- // nodes. Therefore, we will just append the current "to" node
- // to the end
- if (
- curToNodeKey &&
- (matchingFromEl = fromNodesLookup[curToNodeKey]) &&
- compareNodeNames(matchingFromEl, curToNodeChild)
- ) {
- // MORPH
- if (!skipFrom) {
- addChild(fromEl, matchingFromEl);
- }
- morphEl(matchingFromEl, curToNodeChild);
- } else {
- var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
- if (onBeforeNodeAddedResult !== false) {
- if (onBeforeNodeAddedResult) {
- curToNodeChild = onBeforeNodeAddedResult;
- }
-
- if (curToNodeChild.actualize) {
- curToNodeChild = curToNodeChild.actualize(
- fromEl.ownerDocument || doc
- );
- }
- addChild(fromEl, curToNodeChild);
- handleNodeAdded(curToNodeChild);
- }
- }
-
- curToNodeChild = toNextSibling;
- curFromNodeChild = fromNextSibling;
- }
-
- cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);
-
- var specialElHandler = specialElHandlers[fromEl.nodeName];
- if (specialElHandler) {
- specialElHandler(fromEl, toEl);
- }
- } // END: morphChildren(...)
-
- var morphedNode = fromNode;
- var morphedNodeType = morphedNode.nodeType;
- var toNodeType = toNode.nodeType;
-
- if (!childrenOnly) {
- // Handle the case where we are given two DOM nodes that are not
- // compatible (e.g. <div> --> <span> or <div> --> TEXT)
- if (morphedNodeType === ELEMENT_NODE) {
- if (toNodeType === ELEMENT_NODE) {
- if (!compareNodeNames(fromNode, toNode)) {
- onNodeDiscarded(fromNode);
- morphedNode = moveChildren(
- fromNode,
- createElementNS(toNode.nodeName, toNode.namespaceURI)
- );
- }
- } else {
- // Going from an element node to a text node
- morphedNode = toNode;
- }
- } else if (
- morphedNodeType === TEXT_NODE ||
- morphedNodeType === COMMENT_NODE
- ) {
- // Text or comment node
- if (toNodeType === morphedNodeType) {
- if (morphedNode.nodeValue !== toNode.nodeValue) {
- morphedNode.nodeValue = toNode.nodeValue;
- }
-
- return morphedNode;
- } else {
- // Text node to something else
- morphedNode = toNode;
- }
- }
- }
-
- if (morphedNode === toNode) {
- // The "to node" was not compatible with the "from node" so we had to
- // toss out the "from node" and use the "to node"
- onNodeDiscarded(fromNode);
- } else {
- if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {
- return;
- }
-
- morphEl(morphedNode, toNode, childrenOnly);
-
- // We now need to loop over any keyed nodes that might need to be
- // removed. We only do the removal if we know that the keyed node
- // never found a match. When a keyed node is matched up we remove
- // it out of fromNodesLookup and we use fromNodesLookup to determine
- // if a keyed node has been matched up or not
- if (keyedRemovalList) {
- for (var i = 0, len = keyedRemovalList.length; i < len; i++) {
- var elToRemove = fromNodesLookup[keyedRemovalList[i]];
- if (elToRemove) {
- removeNode(elToRemove, elToRemove.parentNode, false);
- }
- }
- }
- }
-
- if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
- if (morphedNode.actualize) {
- morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
- }
- // If we had to swap out the from node with a new node because the old
- // node was not compatible with the target node then we need to
- // replace the old DOM node in the original DOM tree. This is only
- // possible if the original DOM node was part of a DOM tree which
- // we know is the case if it has a parent node.
- fromNode.parentNode.replaceChild(morphedNode, fromNode);
- }
-
- return morphedNode;
- };
+ return prev;
}
-export const morphdom = morphdomFactory(morphAttrs);
-
-export default morphdom;
-
-// VDOM HACKS ------------------------------------------------------------------
-
-// Whew this is some Naughty Stuff™. Our `Element` Gleam type is opaque so the
-// class constructors are not exposed and we can't import them. We need to
-// massage these classes into a shape that morphdom knows how to deal with, and
-// because we can't get at the constructors directly we're going to be sneaky and
-// get in by constructing some dummy elements and stealing their prototypes.
-//
-// Morphdom expects the following properties/methods to exist on a valid VDOM
-// node:
-//
-// - firstChild;
-// - nextSibling;
-// - nodeType;
-// - nodeName;
-// - namespaceURI;
-// - nodeValue;
-// - attributes;
-// - value;
-// - selected;
-// - disabled;
-// - hasAttributeNS(namespaceURI, name);
-// - actualize(document);
-//
-// See more here: https://github.com/patrick-steele-idem/morphdom/blob/master/docs/virtual-dom.md
-//
-
-Object.defineProperties(h("").constructor.prototype, {
- firstChild: {
- get() {
- // If this is the first time `firstChild` is being accessed, we need to
- // create the children array first.
- if (!this.children) this.children = this[2].toArray();
-
- const child = this.children[0];
-
- if (child) {
- child.parentElement = this;
- child.index = 0;
- }
-
- return child;
- },
- },
-
- nextSibling: {
- get() {
- const sibling = this.parentElement?.children[this.index + 1];
-
- if (sibling) {
- sibling.parentElement = this.parentElement;
- sibling.index = this.index + 1;
- }
-
- return sibling;
- },
- },
-
- nodeType: {
- value: ELEMENT_NODE,
- },
-
- nodeName: {
- get() {
- return this[0].toUpperCase();
- },
- },
-
- namespaceURI: {
- value: undefined,
- },
-
- nodeValue: {
- value: null,
- },
-
- attributes: {
- get() {
- if (!this._attributes) {
- this._attributes = Object.fromEntries(this[1].toArray());
- }
-
- return this._attributes;
- },
- },
-
- value: {
- get() {
- return this.attributes.value;
- },
- },
-
- selected: {
- get() {
- return !!this.attributes.selected;
- },
- },
-
- disabled: {
- get() {
- return !!this.attributes.disabled;
- },
- },
-
- hasAttributeNS: {
- value: function (_, name) {
- return name in this.attributes;
- },
- },
-
- actualize: {
- value: function (document) {
- const el = document.createElement(this[0]);
-
- for (const key in this.attributes) {
- el[key] = this.attributes[key];
- }
-
- for (let child = this.firstChild; !!child; child = child.nextSibling) {
- el.appendChild(child.actualize(document));
- }
-
- return el;
- },
- },
-});
-
-Object.defineProperties(t("").constructor.prototype, {
- nextSibling: {
- get() {
- const sibling = this.parentElement?.children[this.index + 1];
-
- if (sibling) {
- sibling.parentElement = this.parentElement;
- sibling.index = this.index + 1;
- }
-
- return sibling;
- },
- },
-
- nodeType: {
- get() {
- return TEXT_NODE;
- },
- },
-
- nodeName: {
- value: "#text",
- },
-
- nodeValue: {
- get() {
- return this[0];
- },
- },
+function morphText(prev, curr) {
+ if (prev.nodeValue !== curr[0]) prev.nodeValue = curr[0];
+ return prev;
+}
- actualize: {
- value: function (document) {
- return document.createTextNode(this[0]);
- },
- },
-});
+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;
+
+ // For properties, we're leaning on reference equality to avoid unnecessary
+ // updates.
+ default:
+ if (el[name] !== value) el[name] = value;
+ }
+}