diff --git a/morph.html b/morph.html
index bf68136f7..e1738670b 100644
--- a/morph.html
+++ b/morph.html
@@ -5,44 +5,21 @@
@@ -69,7 +46,7 @@
Alpine.morph(
document.querySelector('#before').firstElementChild,
document.querySelector('#after').firstElementChild.outerHTML,
- { debug: true }
+ { debug: true, key(el) { return el.dataset.key } }
)
}
diff --git a/packages/alpinejs/src/alpine.js b/packages/alpinejs/src/alpine.js
index 39331ffc0..afd8e7c39 100644
--- a/packages/alpinejs/src/alpine.js
+++ b/packages/alpinejs/src/alpine.js
@@ -5,7 +5,7 @@ import { onElRemoved, onAttributeRemoved, onAttributesAdded, mutateDom, deferMut
import { mergeProxies, closestDataStack, addScopeToNode, scope as $data } from './scope'
import { setEvaluator, evaluate, evaluateLater, dontAutoEvaluateFunctions } from './evaluator'
import { transition } from './directives/x-transition'
-import { clone, skipDuringClone, onlyDuringClone } from './clone'
+import { clone, cloneNode, skipDuringClone, onlyDuringClone } from './clone'
import { interceptor } from './interceptor'
import { getBinding as bound, extractProp } from './utils/bind'
import { debounce } from './utils/debounce'
@@ -68,7 +68,8 @@ let Alpine = {
magic,
store,
start,
- clone,
+ clone, // INTERNAL
+ cloneNode, // INTERNAL
bound,
$data,
walk,
diff --git a/packages/alpinejs/src/clone.js b/packages/alpinejs/src/clone.js
index 3e870c8c3..0089bbd73 100644
--- a/packages/alpinejs/src/clone.js
+++ b/packages/alpinejs/src/clone.js
@@ -12,22 +12,53 @@ export function onlyDuringClone(callback) {
return (...args) => isCloning && callback(...args)
}
-export function interuptCrawl(callback) {
- return (...args) => isCloning || callback(...args)
+export function cloneNode(from, to)
+{
+ // Transfer over existing runtime Alpine state from
+ // the existing dom tree over to the new one...
+ if (from._x_dataStack) {
+ to._x_dataStack = from._x_dataStack
+
+ // Set a flag to signify the new tree is using
+ // pre-seeded state (used so x-data knows when
+ // and when not to initialize state)...
+ to.setAttribute('data-has-alpine-state', true)
+ }
+
+ isCloning = true
+
+ // We don't need reactive effects in the new tree.
+ // Cloning is just used to seed new server HTML with
+ // Alpine before "morphing" it onto live Alpine...
+ dontRegisterReactiveSideEffects(() => {
+ initTree(to, (el, callback) => {
+ // We're hijacking the "walker" so that we
+ // only initialize the element we're cloning...
+ callback(el, () => {})
+ })
+ })
+
+ isCloning = false
}
+let isCloningLegacy = false
+
+/** deprecated */
export function clone(oldEl, newEl) {
if (! newEl._x_dataStack) newEl._x_dataStack = oldEl._x_dataStack
isCloning = true
+ isCloningLegacy = true
dontRegisterReactiveSideEffects(() => {
cloneTree(newEl)
})
isCloning = false
+ isCloningLegacy = false
}
+/** deprecated */
export function cloneTree(el) {
let hasRunThroughFirstEl = false
@@ -59,3 +90,15 @@ function dontRegisterReactiveSideEffects(callback) {
overrideEffect(cache)
}
+
+// If we are cloning a tree, we only want to evaluate x-data if another
+// x-data context DOESN'T exist on the component.
+// The reason a data context WOULD exist is that we graft root x-data state over
+// from the live tree before hydrating the clone tree.
+export function shouldSkipRegisteringDataDuringClone(el) {
+ if (! isCloning) return false
+ if (isCloningLegacy) return true
+
+ return el.hasAttribute('data-has-alpine-state')
+}
+
diff --git a/packages/alpinejs/src/directives/x-data.js b/packages/alpinejs/src/directives/x-data.js
index 734b6b29e..c5723b3b4 100644
--- a/packages/alpinejs/src/directives/x-data.js
+++ b/packages/alpinejs/src/directives/x-data.js
@@ -2,7 +2,7 @@ import { directive, prefix } from '../directives'
import { initInterceptors } from '../interceptor'
import { injectDataProviders } from '../datas'
import { addRootSelector } from '../lifecycle'
-import { isCloning } from '../clone'
+import { shouldSkipRegisteringDataDuringClone } from '../clone'
import { addScopeToNode } from '../scope'
import { injectMagics, magic } from '../magics'
import { reactive } from '../reactivity'
@@ -11,11 +11,7 @@ import { evaluate } from '../evaluator'
addRootSelector(() => `[${prefix('data')}]`)
directive('data', ((el, { expression }, { cleanup }) => {
- // If we are cloning a tree, we only want to evaluate x-data if another
- // x-data context DOESN'T exist on the component.
- // The reason a data context WOULD exist is that we graft root x-data state over
- // from the live tree before hydrating the clone tree.
- if (isCloning && el._x_dataStack) return;
+ if (shouldSkipRegisteringDataDuringClone(el)) return
expression = expression === '' ? '{}' : expression
diff --git a/packages/morph/src/dom.js b/packages/morph/src/dom.js
deleted file mode 100644
index e6dc656d4..000000000
--- a/packages/morph/src/dom.js
+++ /dev/null
@@ -1,85 +0,0 @@
-
-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
-}
-
-export let dom = {
- replace(children, old, replacement) {
- // Here's what's happening here:
- // First, we're swapping the actual dom element with the new one
- // Then, we're replaceing the old one with the new one in the children array
- // Finally, because the old has been replaced by the new, we can remove the previous new element in it's old position...
- let index = children.indexOf(old)
-
- let replacementIndex = children.indexOf(replacement)
-
- if (index === -1) throw 'Cant find element in children'
-
- old.replaceWith(replacement)
-
- children[index] = replacement
-
- if (replacementIndex) {
- children.splice(replacementIndex, 1)
- }
-
- return children
- },
- before(children, reference, subject) {
- let index = children.indexOf(reference)
-
- 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
- return el
- }
-}
-
diff --git a/packages/morph/src/morph.js b/packages/morph/src/morph.js
index 5dfd7e6f2..0bf2908e8 100644
--- a/packages/morph/src/morph.js
+++ b/packages/morph/src/morph.js
@@ -1,4 +1,3 @@
-import { dom, createElement, textOrComment} from './dom.js'
let resolveStep = () => {}
@@ -13,19 +12,13 @@ export function morph(from, toHtml, options) {
let fromEl
let toEl
- let key
- ,lookahead
- ,updating
- ,updated
- ,removing
- ,removed
- ,adding
- ,added
+ let key, lookahead, updating, updated, removing, removed, adding, added
function assignOptions(options = {}) {
let defaultGetKey = el => el.getAttribute('key')
let noop = () => {}
+ console.log(options.key)
updating = options.updating || noop
updated = options.updated || noop
removing = options.removing || noop
@@ -37,25 +30,22 @@ export function morph(from, toHtml, options) {
}
function patch(from, to) {
- // This is a time saver, however, it won't catch differences in nested
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)) {
- // Swap elements...
- return patchElement(from, to)
+ return swapElements(from, to)
}
let updateChildrenOnly = false
if (shouldSkip(updating, from, to, () => updateChildrenOnly = true)) return
- window.Alpine && initializeAlpineOnTo(from, to, () => updateChildrenOnly = true)
+ // Initialize the server-side HTML element with Alpine...
+ if (from.nodeType === 1 && window.Alpine) {
+ window.Alpine.cloneNode(from, to)
+ }
if (textOrComment(to)) {
patchNodeValue(from, to)
+
updated(from, to)
return
@@ -67,9 +57,7 @@ export function morph(from, toHtml, options) {
updated(from, to)
- patchChildren(Array.from(from.childNodes), Array.from(to.childNodes), (toAppend) => {
- from.appendChild(toAppend)
- })
+ patchChildren(from, to)
}
function differentElementNamesTypesOrKeys(from, to) {
@@ -78,14 +66,14 @@ export function morph(from, toHtml, options) {
|| getKey(from) != getKey(to)
}
- function patchElement(from, to) {
+ function swapElements(from, to) {
if (shouldSkip(removing, from)) return
let toCloned = to.cloneNode(true)
if (shouldSkip(adding, toCloned)) return
- dom.replace([from], from, toCloned)
+ from.replaceWith(toCloned)
removed(from)
added(toCloned)
@@ -132,36 +120,37 @@ export function morph(from, toHtml, options) {
}
}
- function patchChildren(fromChildren, toChildren, appendFn) {
- let fromKeyDomNodeMap = keyToMap(fromChildren)
+ function patchChildren(from, to) {
+ let fromKeys = keyToMap(from.children)
let fromKeyHoldovers = {}
- let currentTo = dom.first(toChildren)
- let currentFrom = dom.first(fromChildren)
+ let currentTo = getFirstNode(to)
+ let currentFrom = getFirstNode(from)
while (currentTo) {
let toKey = getKey(currentTo)
let fromKey = getKey(currentFrom)
- // Add new elements
+ // Add new elements...
if (! currentFrom) {
if (toKey && fromKeyHoldovers[toKey]) {
// Add element (from key)...
let holdover = fromKeyHoldovers[toKey]
- fromChildren = dom.append(fromChildren, holdover, appendFn)
+ from.appendChild(holdover)
+
currentFrom = holdover
} else {
if(! shouldSkip(adding, currentTo)) {
// Add element...
let clone = currentTo.cloneNode(true)
- fromChildren = dom.append(fromChildren, clone, appendFn)
+ from.appendChild(clone)
added(clone)
}
- currentTo = dom.next(toChildren, currentTo)
+ currentTo = getNextSibling(to, currentTo)
continue
}
@@ -172,54 +161,61 @@ export function morph(from, toHtml, options) {
let isEnd = node => node && node.nodeType === 8 && node.textContent === ' __ENDBLOCK__ '
if (isIf(currentTo) && isIf(currentFrom)) {
- let newFromChildren = []
- let appendPoint
let nestedIfCount = 0
+
+ let fromBlockStart = currentFrom
+
while (currentFrom) {
- let next = dom.next(fromChildren, currentFrom)
+ let next = getNextSibling(from, 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
+ currentFrom = next
break;
}
- newFromChildren.push(next)
currentFrom = next
}
- let newToChildren = []
+ let fromBlockEnd = currentFrom
+
nestedIfCount = 0
+
+ let toBlockStart = currentTo
+
while (currentTo) {
- let next = dom.next(toChildren, currentTo)
+ let next = getNextSibling(to, currentTo)
if (isIf(next)) {
nestedIfCount++
} else if (isEnd(next) && nestedIfCount > 0) {
nestedIfCount--
} else if (isEnd(next) && nestedIfCount === 0) {
- currentTo = dom.next(toChildren, next)
+ currentTo = next
break;
}
- newToChildren.push(next)
currentTo = next
}
- patchChildren(newFromChildren, newToChildren, node => appendPoint.before(node))
+ let toBlockEnd = currentTo
+
+ let fromBlock = new Block(fromBlockStart, fromBlockEnd)
+ let toBlock = new Block(toBlockStart, toBlockEnd)
+
+ patchChildren(fromBlock, toBlock)
continue
}
// Lookaheads should only apply to non-text-or-comment elements...
if (currentFrom.nodeType === 1 && lookahead && ! currentFrom.isEqualNode(currentTo)) {
- let nextToElementSibling = dom.next(toChildren, currentTo)
+ let nextToElementSibling = getNextSibling(to, currentTo)
let found = false
@@ -227,12 +223,12 @@ export function morph(from, toHtml, options) {
if (nextToElementSibling.nodeType === 1 && currentFrom.isEqualNode(nextToElementSibling)) {
found = true; // This ";" needs to be here...
- [fromChildren, currentFrom] = addNodeBefore(fromChildren, currentTo, currentFrom)
+ currentFrom = addNodeBefore(from, currentTo, currentFrom)
fromKey = getKey(currentFrom)
}
- nextToElementSibling = dom.next(toChildren, nextToElementSibling)
+ nextToElementSibling = getNextSibling(to, nextToElementSibling)
}
}
@@ -240,37 +236,37 @@ export function morph(from, toHtml, options) {
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)
+ currentFrom = addNodeBefore(from, currentTo, currentFrom)
+ fromKeyHoldovers[fromKey].remove()
+ currentFrom = getNextSibling(from, currentFrom)
+ currentTo = getNextSibling(to, currentTo)
continue
}
if (toKey && ! fromKey) {
- if (fromKeyDomNodeMap[toKey]) {
+ if (fromKeys[toKey]) {
// No "from" key...
- fromChildren = dom.replace(fromChildren, currentFrom, fromKeyDomNodeMap[toKey])
- currentFrom = fromKeyDomNodeMap[toKey]
+ currentFrom.replaceWith(fromKeys[toKey])
+ currentFrom = fromKeys[toKey]
}
}
if (toKey && fromKey) {
- let fromKeyNode = fromKeyDomNodeMap[toKey]
+ let fromKeyNode = fromKeys[toKey]
if (fromKeyNode) {
// Move "from" key...
fromKeyHoldovers[fromKey] = currentFrom
- fromChildren = dom.replace(fromChildren, currentFrom, fromKeyNode)
+ currentFrom.replaceWith(fromKeyNode)
currentFrom = fromKeyNode
} else {
// 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)
+ currentFrom = addNodeBefore(from, currentTo, currentFrom)
+ fromKeyHoldovers[fromKey].remove()
+ currentFrom = getNextSibling(from, currentFrom)
+ currentTo = getNextSibling(to, currentTo)
continue
}
@@ -278,12 +274,13 @@ export function morph(from, toHtml, options) {
}
// Get next from sibling before patching in case the node is replaced
- let currentFromNext = currentFrom && dom.next(fromChildren, currentFrom)
+ let currentFromNext = currentFrom && getNextSibling(from, currentFrom) //dom.next(from, fromChildren, currentFrom))
// Patch elements
patch(currentFrom, currentTo)
- currentTo = currentTo && dom.next(toChildren, currentTo)
+ currentTo = currentTo && getNextSibling(to, currentTo) // dom.next(from, toChildren, currentTo))
+
currentFrom = currentFromNext
}
@@ -293,9 +290,10 @@ export function morph(from, toHtml, options) {
// 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)
+ if (! shouldSkip(removing, currentFrom)) removals.push(currentFrom)
- currentFrom = dom.next(fromChildren, currentFrom)
+ // currentFrom = dom.next(fromChildren, currentFrom)
+ currentFrom = getNextSibling(from, currentFrom)
}
// Now we can do the actual removals.
@@ -315,29 +313,29 @@ export function morph(from, toHtml, options) {
function keyToMap(els) {
let map = {}
- els.forEach(el => {
+ for (let el of els) {
let theKey = getKey(el)
if (theKey) {
map[theKey] = el
}
- })
+ }
return map
}
- function addNodeBefore(children, node, beforeMe) {
+ function addNodeBefore(parent, node, beforeMe) {
if(! shouldSkip(adding, node)) {
let clone = node.cloneNode(true)
- children = dom.before(children, beforeMe, clone)
+ parent.insertBefore(clone, beforeMe)
added(clone)
- return [children, clone]
+ return clone
}
- return [children, node]
+ return node
}
// Finally we morph the element
@@ -347,12 +345,14 @@ export function morph(from, toHtml, 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) {
+ // Just in case a part of this template uses Alpine scope from somewhere
+ // higher in the DOM tree, we'll find that state and replace it on the root
+ // element so everything is synced up accurately.
toEl._x_dataStack = window.Alpine.closestDataStack(from)
- toEl._x_dataStack && window.Alpine.clone(from, toEl)
+ // We will kick off a clone on the root element.
+ toEl._x_dataStack && window.Alpine.cloneNode(from, toEl)
}
patch(from, toEl)
@@ -364,10 +364,9 @@ export function morph(from, toHtml, options) {
return from
}
-morph.step = () => resolveStep()
-morph.log = (theLogger) => {
- logger = theLogger
-}
+// These are legacy holdovers that don't do anything anymore...
+morph.step = () => {}
+morph.log = () => {}
function shouldSkip(hook, ...args) {
let skip = false
@@ -377,18 +376,91 @@ function shouldSkip(hook, ...args) {
return skip
}
-function initializeAlpineOnTo(from, to, childrenOnly) {
- if (from.nodeType !== 1) return
+let patched = false
+
+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
+}
+
+// "Block"s are used when morphing with conditional markers.
+// They allow us to patch isolated portions of a list of
+// siblings in a DOM tree...
+class Block {
+ constructor(start, end) {
+ // We're assuming here that the start and end caps are comment blocks...
+ this.startComment = start
+ this.endComment = end
+ }
+
+ get children() {
+ let children = [];
+
+ let currentNode = this.startComment.nextSibling
+
+ while (currentNode !== undefined && currentNode !== this.endComment) {
+ children.push(currentNode)
- // 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)
+ currentNode = currentNode.nextSibling
+ }
+
+ return children
+ }
+
+ appendChild(child) {
+ this.endComment.before(child)
+ }
+
+ get firstChild() {
+ let first = this.startComment.nextSibling
+
+ if (first === this.endComment) return
+
+ return first
+ }
+
+ nextNode(reference) {
+ let next = reference.nextSibling
+
+ if (next === this.endComment) return
+
+ return next
+ }
+
+ insertBefore(newNode, reference) {
+ reference.before(newNode)
+
+ return newNode
}
}
-let patched = false
+function getFirstNode(parent) {
+ return parent.firstChild
+}
+
+function getNextSibling(parent, reference) {
+ if (reference._x_teleport) {
+ return reference._x_teleport
+ } else if (reference.teleportBack) {
+ return reference.teleportBack
+ }
+
+ let next
+
+ if (parent instanceof Block) {
+ next = parent.nextNode(reference)
+ } else {
+ next = reference.nextSibling
+ }
+
+ return next
+}
function monkeyPatchDomSetAttributeToAllowAtSymbols() {
if (patched) return
diff --git a/tests/cypress/integration/plugins/focus.spec.js b/tests/cypress/integration/plugins/focus.spec.js
index ae80459a8..284111621 100644
--- a/tests/cypress/integration/plugins/focus.spec.js
+++ b/tests/cypress/integration/plugins/focus.spec.js
@@ -29,7 +29,7 @@ test('can trap focus',
},
)
-test('works with clone',
+test.only('works with clone',
[html`