Caleb Porzio 2 years ago
parent
commit
8e7e1dd89d

+ 55 - 48
packages/morph/src/dom.js

@@ -1,50 +1,71 @@
-class DomManager {
-    el = undefined
 
-    constructor(el) {
-        this.el = el
-    }
+export function createElement(html) {
+    const template = document.createElement('template')
+    template.innerHTML = html
+    return template.content.firstElementChild
+}
 
-    traversals = {
-        'first': 'firstElementChild',
-        'next': 'nextElementSibling',
-        'parent': 'parentElement',
-    }
+export function textOrComment(el) {
+    return el.nodeType === 3
+        || el.nodeType === 8
+}
 
-    nodes() {
-        this.traversals = {
-            'first': 'firstChild',
-            'next': 'nextSibling',
-            'parent': 'parentNode',
-        }; return this
-    }
+export let dom = {
+    replace(children, old, replacement) {
+        let index = children.indexOf(old)
 
-    first() {
-        return this.teleportTo(this.el[this.traversals['first']])
-    }
+        if (index === -1) throw 'Cant find element in children'
 
-    next() {
-        return this.teleportTo(this.teleportBack(this.el[this.traversals['next']]))
-    }
+        old.replaceWith(replacement)
 
-    before(insertee) {
-        this.el[this.traversals['parent']].insertBefore(insertee, this.el); return insertee
-    }
+        children[index] = replacement
 
-    replace(replacement) {
-        this.el[this.traversals['parent']].replaceChild(replacement, this.el); return replacement
-    }
+        return children
+    },
+    before(children, reference, subject) {
+        let index = children.indexOf(reference)
 
-    append(appendee) {
-        this.el.appendChild(appendee); return appendee
-    }
+        if (index === -1) throw 'Cant find element in children'
+
+        reference.before(subject)
+
+        children.splice(index, 0, subject)
+
+        return children
+    },
+    append(children, subject, appendFn) {
+        let last = children[children.length - 1]
+
+        appendFn(subject)
 
+        children.push(subject)
+
+        return children
+    },
+    remove(children, subject) {
+        let index = children.indexOf(subject)
+
+        if (index === -1) throw 'Cant find element in children'
+
+        subject.remove()
+
+        return children.filter(i => i !== subject)
+    },
+    first(children) {
+        return this.teleportTo(children[0])
+    },
+    next(children, reference) {
+        let index = children.indexOf(reference)
+
+        if (index === -1) return
+
+        return this.teleportTo(this.teleportBack(children[index + 1]))
+    },
     teleportTo(el) {
         if (! el) return el
         if (el._x_teleport) return el._x_teleport
         return el
-    }
-
+    },
     teleportBack(el) {
         if (! el) return el
         if (el._x_teleportBack) return el._x_teleportBack
@@ -52,17 +73,3 @@ class DomManager {
     }
 }
 
-export function dom(el) {
-    return new DomManager(el)
-}
-
-export function createElement(html) {
-    const template = document.createElement('template')
-    template.innerHTML = html
-    return template.content.firstElementChild
-}
-
-export function textOrComment(el) {
-    return el.nodeType === 3
-        || el.nodeType === 8
-}

+ 128 - 148
packages/morph/src/morph.js

@@ -4,7 +4,7 @@ let resolveStep = () => {}
 
 let logger = () => {}
 
-export async function morph(from, toHtml, options) {
+export function morph(from, toHtml, options) {
     // We're defining these globals and methods inside this function (instead of outside)
     // because it's an async function and if run twice, they would overwrite
     // each other.
@@ -19,16 +19,6 @@ export async function morph(from, toHtml, options) {
         ,removed
         ,adding
         ,added
-        ,debug
-
-
-    function breakpoint(message) {
-        if (! debug) return
-
-        logger((message || '').replace('\n', '\\n'), fromEl, toEl)
-
-        return new Promise(resolve => resolveStep = () => resolve())
-    }
 
     function assignOptions(options = {}) {
         let defaultGetKey = el => el.getAttribute('key')
@@ -42,10 +32,9 @@ export async function morph(from, toHtml, options) {
         added = options.added || noop
         key = options.key || defaultGetKey
         lookahead = options.lookahead || false
-        debug = options.debug || false
     }
 
-    async function patch(from, to) {
+    function patch(from, to) {
         // This is a time saver, however, it won't catch differences in nested <template> tags.
         // I'm leaving this here as I believe it's an important speed improvement, I just
         // don't see a way to enable it currently:
@@ -53,11 +42,8 @@ export async function morph(from, toHtml, options) {
         // if (from.isEqualNode(to)) return
 
         if (differentElementNamesTypesOrKeys(from, to)) {
-            let result = patchElement(from, to)
-
-            await breakpoint('Swap elements')
-
-            return result
+            // Swap elements...
+            return patchElement(from, to)
         }
 
         let updateChildrenOnly = false
@@ -67,19 +53,21 @@ export async function morph(from, toHtml, options) {
         window.Alpine && initializeAlpineOnTo(from, to, () => updateChildrenOnly = true)
 
         if (textOrComment(to)) {
-            await patchNodeValue(from, to)
+            patchNodeValue(from, to)
             updated(from, to)
 
             return
         }
 
         if (! updateChildrenOnly) {
-            await patchAttributes(from, to)
+            patchAttributes(from, to)
         }
 
         updated(from, to)
 
-        await patchChildren(from, to)
+        patchChildren(Array.from(from.childNodes), Array.from(to.childNodes), (toAppend) => {
+            from.appendChild(toAppend)
+        })
     }
 
     function differentElementNamesTypesOrKeys(from, to) {
@@ -95,23 +83,22 @@ export async function morph(from, toHtml, options) {
 
         if (shouldSkip(adding, toCloned)) return
 
-        dom(from).replace(toCloned)
+        dom.replace([from], from, toCloned)
 
         removed(from)
         added(toCloned)
     }
 
-    async function patchNodeValue(from, to) {
+    function patchNodeValue(from, to) {
         let value = to.nodeValue
 
         if (from.nodeValue !== value) {
+            // Change text node...
             from.nodeValue = value
-
-            await breakpoint('Change text node to: ' + value)
         }
     }
 
-    async function patchAttributes(from, to) {
+    function patchAttributes(from, to) {
         if (from._x_isShown && ! to._x_isShown) {
             return
         }
@@ -126,9 +113,8 @@ export async function morph(from, toHtml, options) {
             let name = domAttributes[i].name;
 
             if (! to.hasAttribute(name)) {
+                // Remove attribute...
                 from.removeAttribute(name)
-
-                await breakpoint('Remove attribute')
             }
         }
 
@@ -138,139 +124,151 @@ export async function morph(from, toHtml, options) {
 
             if (from.getAttribute(name) !== value) {
                 from.setAttribute(name, value)
-
-                await breakpoint(`Set [${name}] attribute to: "${value}"`)
             }
         }
     }
 
-    async function patchChildren(from, to) {
-        let domChildren = from.childNodes
-        let toChildren = to.childNodes
-
-        let toKeyToNodeMap = keyToMap(toChildren)
-        let domKeyDomNodeMap = keyToMap(domChildren)
-
-        let currentTo = dom(to).nodes().first()
-        let currentFrom = dom(from).nodes().first()
-
-        let domKeyHoldovers = {}
+    function patchChildren(fromChildren, toChildren, appendFn) {
+        // I think I can get rid of this for now:
+        let fromKeyDomNodeMap = {} // keyToMap(fromChildren)
+        let fromKeyHoldovers = {}
 
-        let isInsideWall = false
+        let currentTo = dom.first(toChildren)
+        let currentFrom = dom.first(fromChildren)
 
         while (currentTo) {
-            // If "<!-- end -->"
-            if (
-                currentTo.nodeType === 8
-                && currentTo.textContent === ' end '
-            ) {
-                isInsideWall = false
-                currentTo = dom(currentTo).nodes().next()
-                currentFrom = dom(currentFrom).nodes().next()
-                continue
-            }
-
-            if (insideWall) {
-
-            }
-
-            if (isInsideWall) {
-                console.log(currentFrom, currentTo)
-            }
-
             let toKey = getKey(currentTo)
-            let domKey = getKey(currentFrom)
+            let fromKey = getKey(currentFrom)
 
             // Add new elements
             if (! currentFrom) {
-                if (toKey && domKeyHoldovers[toKey]) {
-                    let holdover = domKeyHoldovers[toKey]
+                if (toKey && fromKeyHoldovers[toKey]) {
+                    // Add element (from key)...
+                    let holdover = fromKeyHoldovers[toKey]
 
-                    dom(from).append(holdover)
+                    fromChildren = dom.append(fromChildren, holdover, appendFn)
                     currentFrom = holdover
-
-                    await breakpoint('Add element (from key)')
                 } else {
-                    let added = addNodeTo(currentTo, from) || {}
+                    if(! shouldSkip(adding, currentTo)) {
+                        // Add element...
+                        let clone = currentTo.cloneNode(true)
 
-                    await breakpoint('Add element: ' + (added.outerHTML || added.nodeValue))
+                        fromChildren = dom.append(fromChildren, clone, appendFn)
+
+                        added(clone)
+                    }
 
-                    currentTo = dom(currentTo).nodes().next()
+                    currentTo = dom.next(toChildren, currentTo)
 
                     continue
                 }
             }
 
-            // If "<!-- if -->"
-            if (
-                currentTo.nodeType === 8
-                && currentTo.textContent === ' if '
-                && currentFrom.nodeType === 8
-                && currentFrom.textContent === ' if '
-            ) {
-                isInsideWall = true
-                currentTo = dom(currentTo).nodes().next()
-                currentFrom = dom(currentFrom).nodes().next()
-                continue
-            }
-
-            if (lookahead) {
-                let nextToElementSibling = dom(currentTo).next()
-
-                let found = false
+            // Handle conditional markers (presumably added by backends like Livewire)...
+            let isIf = node => node.nodeType === 8 && node.textContent === ' __IF__ '
+            let isEnd = node => node.nodeType === 8 && node.textContent === ' __ENDIF__ '
+
+            if (isIf(currentTo) && isIf(currentFrom)) {
+                let newFromChildren = []
+                let appendPoint
+                let nestedIfCount = 0
+                while (currentFrom) {
+                    let next = dom.next(fromChildren, currentFrom)
+
+                    if (isIf(next)) {
+                        nestedIfCount++
+                    } else if (isEnd(next) && nestedIfCount > 0) {
+                        nestedIfCount--
+                    } else if (isEnd(next) && nestedIfCount === 0) {
+                        currentFrom = dom.next(fromChildren, next)
+                        appendPoint = next
+
+                        break;
+                    }
 
-                while (!found && nextToElementSibling) {
-                    if (currentFrom.isEqualNode(nextToElementSibling)) {
-                        found = true
+                    newFromChildren.push(next)
+                    currentFrom = next
+                }
 
-                        currentFrom = addNodeBefore(currentTo, currentFrom)
+                let newToChildren = []
+                nestedIfCount = 0
+                while (currentTo) {
+                    let next = dom.next(toChildren, currentTo)
 
-                        domKey = getKey(currentFrom)
+                    if (isIf(next)) {
+                        nestedIfCount++
+                    } else if (isEnd(next) && nestedIfCount > 0) {
+                        nestedIfCount--
+                    } else if (isEnd(next) && nestedIfCount === 0) {
+                        currentTo = dom.next(toChildren, next)
 
-                        await breakpoint('Move element (lookahead)')
+                        break;
                     }
 
-                    nextToElementSibling = dom(nextToElementSibling).next()
+                    newToChildren.push(next)
+                    currentTo = next
                 }
+
+                patchChildren(newFromChildren, newToChildren, node => appendPoint.before(node))
+
+                continue
             }
 
-            if (toKey !== domKey) {
-                if (! toKey && domKey) {
-                    domKeyHoldovers[domKey] = currentFrom
-                    currentFrom = addNodeBefore(currentTo, currentFrom)
-                    domKeyHoldovers[domKey].remove()
-                    currentFrom = dom(currentFrom).nodes().next()
-                    currentTo = dom(currentTo).nodes().next()
+            // if (lookahead) {
+            //     let nextToElementSibling = dom.next(toChildren, currentTo, elementOnly)
+
+            //     let found = false
 
-                    await breakpoint('No "to" key')
+            //     while (!found && nextToElementSibling) {
+            //         if (currentFrom.isEqualNode(nextToElementSibling)) {
+            //             found = true; // This ";" needs to be here...
+
+            //             [fromChildren, currentFrom] = addNodeBefore(fromChildren, currentTo, currentFrom)
+
+            //             fromKey = getKey(currentFrom)
+
+                        // breakpoint('Move element (lookahead)')
+            //         }
+
+            //         nextToElementSibling = dom.next(toChildren, nextToElementSibling)
+            //     }
+            // }
+
+            if (toKey !== fromKey) {
+                if (! toKey && fromKey) {
+                    // No "to" key...
+                    fromKeyHoldovers[fromKey] = currentFrom; // This ";" needs to be here...
+                    [fromChildren, currentFrom] = addNodeBefore(fromChildren, currentTo, currentFrom)
+                    fromChildren = dom.remove(fromChildren, fromKeyHoldovers[fromKey])
+                    currentFrom = dom.next(fromChildren, currentFrom)
+                    currentTo = dom.next(toChildren, currentTo)
 
                     continue
                 }
 
-                if (toKey && ! domKey) {
-                    if (domKeyDomNodeMap[toKey]) {
-                        currentFrom = dom(currentFrom).replace(domKeyDomNodeMap[toKey])
-
-                        await breakpoint('No "from" key')
+                if (toKey && ! fromKey) {
+                    if (fromKeyDomNodeMap[toKey]) {
+                        // No "from" key...
+                        fromChildren = dom.replace(fromChildren, currentFrom, fromKeyDomNodeMap[toKey])
+                        currentFrom = fromKeyDomNodeMap[toKey]
                     }
                 }
 
-                if (toKey && domKey) {
-                    domKeyHoldovers[domKey] = currentFrom
-                    let domKeyNode = domKeyDomNodeMap[toKey]
-
-                    if (domKeyNode) {
-                        currentFrom = dom(currentFrom).replace(domKeyNode)
+                if (toKey && fromKey) {
+                    let fromKeyNode = fromKeyDomNodeMap[toKey]
 
-                        await breakpoint('Move "from" key')
+                    if (fromKeyNode) {
+                        // Move "from" key...
+                        fromKeyHoldovers[fromKey] = currentFrom
+                        fromChildren = dom.replace(fromChildren, currentFrom, fromKeyNode)
+                        currentFrom = fromKeyNode
                     } else {
-                        domKeyHoldovers[domKey] = currentFrom
-                        currentFrom = addNodeBefore(currentTo, currentFrom)
-                        domKeyHoldovers[domKey].remove()
-                        currentFrom = dom(currentFrom).next()
-                        currentTo = dom(currentTo).next()
-
-                        await breakpoint('Swap elements with keys')
+                        // Swap elements with keys...
+                        fromKeyHoldovers[fromKey] = currentFrom; // This ";" needs to be here...
+                        [fromChildren, currentFrom] = addNodeBefore(fromChildren, currentTo, currentFrom)
+                        fromChildren = dom.remove(fromChildren, fromKeyHoldovers[fromKey])
+                        currentFrom = dom.next(fromChildren, currentFrom)
+                        currentTo = dom.next(toChildren, currentTo)
 
                         continue
                     }
@@ -278,12 +276,12 @@ export async function morph(from, toHtml, options) {
             }
 
             // Get next from sibling before patching in case the node is replaced
-            let currentFromNext = currentFrom && dom(currentFrom).nodes().next()
+            let currentFromNext = currentFrom && dom.next(fromChildren, currentFrom)
 
             // Patch elements
-            await patch(currentFrom, currentTo)
+            patch(currentFrom, currentTo)
 
-            currentTo = currentTo && dom(currentTo).nodes().next()
+            currentTo = currentTo && dom.next(toChildren, currentTo)
             currentFrom = currentFromNext
         }
 
@@ -295,7 +293,7 @@ export async function morph(from, toHtml, options) {
         while (currentFrom) {
             if(! shouldSkip(removing, currentFrom)) removals.push(currentFrom)
 
-            currentFrom = dom(currentFrom).nodes().next()
+            currentFrom = dom.next(fromChildren, currentFrom)
         }
 
         // Now we can do the actual removals.
@@ -304,8 +302,6 @@ export async function morph(from, toHtml, options) {
 
             domForRemoval.remove()
 
-            await breakpoint('remove el')
-
             removed(domForRemoval)
         }
     }
@@ -328,32 +324,18 @@ export async function morph(from, toHtml, options) {
         return map
     }
 
-    function addNodeTo(node, parent) {
+    function addNodeBefore(children, node, beforeMe) {
         if(! shouldSkip(adding, node)) {
             let clone = node.cloneNode(true)
 
-            dom(parent).append(clone)
+            children = dom.before(children, beforeMe, clone)
 
             added(clone)
 
-            return clone
+            return [children, clone]
         }
 
-        return null;
-    }
-
-    function addNodeBefore(node, beforeMe) {
-        if(! shouldSkip(adding, node)) {
-            let clone = node.cloneNode(true)
-
-            dom(beforeMe).before(clone)
-
-            added(clone)
-
-            return clone
-        }
-
-        return beforeMe
+        return [children, node]
     }
 
     // Finally we morph the element
@@ -371,9 +353,7 @@ export async function morph(from, toHtml, options) {
         toEl._x_dataStack && window.Alpine.clone(from, toEl)
     }
 
-    await breakpoint()
-
-    await patch(from, toEl)
+    patch(from, toEl)
 
     // Release these for the garbage collector.
     fromEl = undefined

+ 405 - 0
packages/morph/src/old_morph.js

@@ -0,0 +1,405 @@
+import { dom, createElement, textOrComment} from './dom.js'
+
+let resolveStep = () => {}
+
+let logger = () => {}
+
+export async function morph(from, toHtml, options) {
+    // We're defining these globals and methods inside this function (instead of outside)
+    // because it's an async function and if run twice, they would overwrite
+    // each other.
+
+    let fromEl
+    let toEl
+    let key
+        ,lookahead
+        ,updating
+        ,updated
+        ,removing
+        ,removed
+        ,adding
+        ,added
+        ,debug
+
+
+    function breakpoint(message) {
+        if (! debug) return
+
+        logger((message || '').replace('\n', '\\n'), fromEl, toEl)
+
+        return new Promise(resolve => resolveStep = () => resolve())
+    }
+
+    function assignOptions(options = {}) {
+        let defaultGetKey = el => el.getAttribute('key')
+        let noop = () => {}
+
+        updating = options.updating || noop
+        updated = options.updated || noop
+        removing = options.removing || noop
+        removed = options.removed || noop
+        adding = options.adding || noop
+        added = options.added || noop
+        key = options.key || defaultGetKey
+        lookahead = options.lookahead || false
+        debug = options.debug || false
+    }
+
+    async function patch(from, to) {
+        // This is a time saver, however, it won't catch differences in nested <template> tags.
+        // I'm leaving this here as I believe it's an important speed improvement, I just
+        // don't see a way to enable it currently:
+        //
+        // if (from.isEqualNode(to)) return
+
+        if (differentElementNamesTypesOrKeys(from, to)) {
+            let result = patchElement(from, to)
+
+            await breakpoint('Swap elements')
+
+            return result
+        }
+
+        let updateChildrenOnly = false
+
+        if (shouldSkip(updating, from, to, () => updateChildrenOnly = true)) return
+
+        window.Alpine && initializeAlpineOnTo(from, to, () => updateChildrenOnly = true)
+
+        if (textOrComment(to)) {
+            await patchNodeValue(from, to)
+            updated(from, to)
+
+            return
+        }
+
+        if (! updateChildrenOnly) {
+            await patchAttributes(from, to)
+        }
+
+        updated(from, to)
+
+        await patchChildren(from, to)
+    }
+
+    function differentElementNamesTypesOrKeys(from, to) {
+        return from.nodeType != to.nodeType
+            || from.nodeName != to.nodeName
+            || getKey(from) != getKey(to)
+    }
+
+    function patchElement(from, to) {
+        if (shouldSkip(removing, from)) return
+
+        let toCloned = to.cloneNode(true)
+
+        if (shouldSkip(adding, toCloned)) return
+
+        dom(from).replace(toCloned)
+
+        removed(from)
+        added(toCloned)
+    }
+
+    async function patchNodeValue(from, to) {
+        let value = to.nodeValue
+
+        if (from.nodeValue !== value) {
+            from.nodeValue = value
+
+            await breakpoint('Change text node to: ' + value)
+        }
+    }
+
+    async function patchAttributes(from, to) {
+        if (from._x_isShown && ! to._x_isShown) {
+            return
+        }
+        if (! from._x_isShown && to._x_isShown) {
+            return
+        }
+
+        let domAttributes = Array.from(from.attributes)
+        let toAttributes = Array.from(to.attributes)
+
+        for (let i = domAttributes.length - 1; i >= 0; i--) {
+            let name = domAttributes[i].name;
+
+            if (! to.hasAttribute(name)) {
+                from.removeAttribute(name)
+
+                await breakpoint('Remove attribute')
+            }
+        }
+
+        for (let i = toAttributes.length - 1; i >= 0; i--) {
+            let name = toAttributes[i].name
+            let value = toAttributes[i].value
+
+            if (from.getAttribute(name) !== value) {
+                from.setAttribute(name, value)
+
+                await breakpoint(`Set [${name}] attribute to: "${value}"`)
+            }
+        }
+    }
+
+    async function patchChildren(from, to) {
+        let domChildren = from.childNodes
+        let toChildren = to.childNodes
+
+        let toKeyToNodeMap = keyToMap(toChildren)
+        let domKeyDomNodeMap = keyToMap(domChildren)
+
+        let currentTo = dom(to).nodes().first()
+        let currentFrom = dom(from).nodes().first()
+
+        let domKeyHoldovers = {}
+
+        let isInsideWall = false
+
+        while (currentTo) {
+            // If "<!-- end -->"
+            if (
+                currentTo.nodeType === 8
+                && currentTo.textContent === ' end '
+            ) {
+                isInsideWall = false
+                currentTo = dom(currentTo).nodes().next()
+                currentFrom = dom(currentFrom).nodes().next()
+                continue
+            }
+
+            if (insideWall)
+
+            if (isInsideWall) {
+                console.log(currentFrom, currentTo)
+            }
+
+            let toKey = getKey(currentTo)
+            let domKey = getKey(currentFrom)
+
+            // Add new elements
+            if (! currentFrom) {
+                if (toKey && domKeyHoldovers[toKey]) {
+                    let holdover = domKeyHoldovers[toKey]
+
+                    dom(from).append(holdover)
+                    currentFrom = holdover
+
+                    await breakpoint('Add element (from key)')
+                } else {
+                    let added = addNodeTo(currentTo, from) || {}
+
+                    await breakpoint('Add element: ' + (added.outerHTML || added.nodeValue))
+
+                    currentTo = dom(currentTo).nodes().next()
+
+                    continue
+                }
+            }
+
+            // If "<!-- if -->"
+            if (
+                currentTo.nodeType === 8
+                && currentTo.textContent === ' if '
+                && currentFrom.nodeType === 8
+                && currentFrom.textContent === ' if '
+            ) {
+                isInsideWall = true
+                currentTo = dom(currentTo).nodes().next()
+                currentFrom = dom(currentFrom).nodes().next()
+                continue
+            }
+
+            if (lookahead) {
+                let nextToElementSibling = dom(currentTo).next()
+
+                let found = false
+
+                while (!found && nextToElementSibling) {
+                    if (currentFrom.isEqualNode(nextToElementSibling)) {
+                        found = true
+
+                        currentFrom = addNodeBefore(currentTo, currentFrom)
+
+                        domKey = getKey(currentFrom)
+
+                        await breakpoint('Move element (lookahead)')
+                    }
+
+                    nextToElementSibling = dom(nextToElementSibling).next()
+                }
+            }
+
+            if (toKey !== domKey) {
+                if (! toKey && domKey) {
+                    domKeyHoldovers[domKey] = currentFrom
+                    currentFrom = addNodeBefore(currentTo, currentFrom)
+                    domKeyHoldovers[domKey].remove()
+                    currentFrom = dom(currentFrom).nodes().next()
+                    currentTo = dom(currentTo).nodes().next()
+
+                    await breakpoint('No "to" key')
+
+                    continue
+                }
+
+                if (toKey && ! domKey) {
+                    if (domKeyDomNodeMap[toKey]) {
+                        currentFrom = dom(currentFrom).replace(domKeyDomNodeMap[toKey])
+
+                        await breakpoint('No "from" key')
+                    }
+                }
+
+                if (toKey && domKey) {
+                    domKeyHoldovers[domKey] = currentFrom
+                    let domKeyNode = domKeyDomNodeMap[toKey]
+
+                    if (domKeyNode) {
+                        currentFrom = dom(currentFrom).replace(domKeyNode)
+
+                        await breakpoint('Move "from" key')
+                    } else {
+                        domKeyHoldovers[domKey] = currentFrom
+                        currentFrom = addNodeBefore(currentTo, currentFrom)
+                        domKeyHoldovers[domKey].remove()
+                        currentFrom = dom(currentFrom).next()
+                        currentTo = dom(currentTo).next()
+
+                        await breakpoint('Swap elements with keys')
+
+                        continue
+                    }
+                }
+            }
+
+            // Get next from sibling before patching in case the node is replaced
+            let currentFromNext = currentFrom && dom(currentFrom).nodes().next()
+
+            // Patch elements
+            await patch(currentFrom, currentTo)
+
+            currentTo = currentTo && dom(currentTo).nodes().next()
+            currentFrom = currentFromNext
+        }
+
+        // Cleanup extra froms.
+        let removals = []
+
+        // We need to collect the "removals" first before actually
+        // removing them so we don't mess with the order of things.
+        while (currentFrom) {
+            if(! shouldSkip(removing, currentFrom)) removals.push(currentFrom)
+
+            currentFrom = dom(currentFrom).nodes().next()
+        }
+
+        // Now we can do the actual removals.
+        while (removals.length) {
+            let domForRemoval = removals.shift()
+
+            domForRemoval.remove()
+
+            await breakpoint('remove el')
+
+            removed(domForRemoval)
+        }
+    }
+
+    function getKey(el) {
+        return el && el.nodeType === 1 && key(el)
+    }
+
+    function keyToMap(els) {
+        let map = {}
+
+        els.forEach(el => {
+            let theKey = getKey(el)
+
+            if (theKey) {
+                map[theKey] = el
+            }
+        })
+
+        return map
+    }
+
+    function addNodeTo(node, parent) {
+        if(! shouldSkip(adding, node)) {
+            let clone = node.cloneNode(true)
+
+            dom(parent).append(clone)
+
+            added(clone)
+
+            return clone
+        }
+
+        return null;
+    }
+
+    function addNodeBefore(node, beforeMe) {
+        if(! shouldSkip(adding, node)) {
+            let clone = node.cloneNode(true)
+
+            dom(beforeMe).before(clone)
+
+            added(clone)
+
+            return clone
+        }
+
+        return beforeMe
+    }
+
+    // Finally we morph the element
+
+    assignOptions(options)
+
+    fromEl = from
+    toEl = typeof toHtml === 'string' ? createElement(toHtml) : toHtml
+
+    // If there is no x-data on the element we're morphing,
+    // let's seed it with the outer Alpine scope on the page.
+    if (window.Alpine && window.Alpine.closestDataStack && ! from._x_dataStack) {
+        toEl._x_dataStack = window.Alpine.closestDataStack(from)
+
+        toEl._x_dataStack && window.Alpine.clone(from, toEl)
+    }
+
+    await breakpoint()
+
+    await patch(from, toEl)
+
+    // Release these for the garbage collector.
+    fromEl = undefined
+    toEl = undefined
+
+    return from
+}
+
+morph.step = () => resolveStep()
+morph.log = (theLogger) => {
+    logger = theLogger
+}
+
+function shouldSkip(hook, ...args) {
+    let skip = false
+
+    hook(...args, () => skip = true)
+
+    return skip
+}
+
+function initializeAlpineOnTo(from, to, childrenOnly) {
+    if (from.nodeType !== 1) return
+
+    // If the element we are updating is an Alpine component...
+    if (from._x_dataStack) {
+        // Then temporarily clone it (with it's data) to the "to" element.
+        // This should simulate backend Livewire being aware of Alpine changes.
+        window.Alpine.clone(from, to)
+    }
+}

+ 66 - 0
tests/cypress/integration/plugins/morph.spec.js

@@ -355,3 +355,69 @@ test('can morph table tr',
         get('td').should(haveText('2'))
     },
 )
+
+test.only('can morph with conditional markers',
+    [html`
+        <main>
+            <!-- __IF__ -->
+            <div>foo<input></div>
+            <!-- __ENDIF__ -->
+            <div>bar<input></div>
+        </main>
+    `],
+    ({ get }, reload, window, document) => {
+        let toHtml = html`
+        <main>
+            <!-- __IF__ -->
+            <div>foo<input></div>
+            <div>baz<input></div>
+            <!-- __ENDIF__ -->
+            <div>bar<input></div>
+        </main>
+        `
+
+        get('div:nth-of-type(1) input').type('foo')
+        get('div:nth-of-type(2) input').type('bar')
+
+        get('main').then(([el]) => window.Alpine.morph(el, toHtml))
+
+        get('div:nth-of-type(1) input').should(haveValue('foo'))
+        get('div:nth-of-type(2) input').should(haveValue(''))
+        get('div:nth-of-type(3) input').should(haveValue('bar'))
+    },
+)
+
+test.only('can morph with flat-nested conditional markers',
+    [html`
+        <main>
+            <!-- __IF__ -->
+            <div>foo<input></div>
+            <!-- __IF__ -->
+            <!-- __ENDIF__ -->
+            <!-- __ENDIF__ -->
+            <div>bar<input></div>
+        </main>
+    `],
+    ({ get }, reload, window, document) => {
+        let toHtml = html`
+        <main>
+            <!-- __IF__ -->
+            <div>foo<input></div>
+            <!-- __IF__ -->
+            <!-- __ENDIF__ -->
+            <div>baz<input></div>
+            <!-- __ENDIF__ -->
+            <div>bar<input></div>
+        </main>
+        `
+
+        get('div:nth-of-type(1) input').type('foo')
+        get('div:nth-of-type(2) input').type('bar')
+
+        get('main').then(([el]) => window.Alpine.morph(el, toHtml))
+
+        get('div:nth-of-type(1) input').should(haveValue('foo'))
+        get('div:nth-of-type(2) input').should(haveValue(''))
+        get('div:nth-of-type(3) input').should(haveValue('bar'))
+    },
+)