diff --git a/packages/alpinejs/src/lifecycle.js b/packages/alpinejs/src/lifecycle.js index e4d787685..53d95bded 100644 --- a/packages/alpinejs/src/lifecycle.js +++ b/packages/alpinejs/src/lifecycle.js @@ -82,12 +82,22 @@ let initInterceptors = [] export function interceptInit(callback) { initInterceptors.push(callback) } +let markerDispenser = 1 + export function initTree(el, walker = walk, intercept = () => {}) { - // Don't init a tree within a parent that is being ignored. + // Don't init a tree within a parent that is being ignored... if (findClosest(el, i => i._x_ignore)) return deferHandlingDirectives(() => { walker(el, (el, skip) => { + // If the element has a marker, it's already been initialized... + if (el._x_marker) return + + // Add a marker to the element so we can tell if it's been initialized... + // This is important so that we can prevent double-initialization of + // elements that are moved around on the page. + el._x_marker = markerDispenser++ + intercept(el, skip) initInterceptors.forEach(i => i(el, skip)) @@ -103,6 +113,7 @@ export function destroyTree(root, walker = walk) { walker(root, el => { cleanupElement(el) cleanupAttributes(el) + delete el._x_marker }) } diff --git a/packages/alpinejs/src/mutation.js b/packages/alpinejs/src/mutation.js index 109786fb3..cf25975c3 100644 --- a/packages/alpinejs/src/mutation.js +++ b/packages/alpinejs/src/mutation.js @@ -118,8 +118,8 @@ function onMutate(mutations) { return } - let addedNodes = new Set - let removedNodes = new Set + let addedNodes = [] + let removedNodes = [] let addedAttributes = new Map let removedAttributes = new Map @@ -127,8 +127,19 @@ function onMutate(mutations) { if (mutations[i].target._x_ignoreMutationObserver) continue if (mutations[i].type === 'childList') { - mutations[i].addedNodes.forEach(node => node.nodeType === 1 && addedNodes.add(node)) - mutations[i].removedNodes.forEach(node => node.nodeType === 1 && removedNodes.add(node)) + mutations[i].removedNodes.forEach(node => { + if (node.nodeType !== 1) return + if (! node._x_marker) return + + removedNodes.push(node) + }) + + mutations[i].addedNodes.forEach(node => { + if (node.nodeType !== 1) return + if (node._x_marker) return + + addedNodes.push(node) + }) } if (mutations[i].type === 'attributes') { @@ -170,42 +181,26 @@ function onMutate(mutations) { onAttributeAddeds.forEach(i => i(el, attrs)) }) + // There are two special scenarios we need to account for when using the mutation + // observer to init and destroy elements. First, when a node is "moved" on the page, + // it's registered as both an "add" and a "remove", so we want to skip those. + // (This is handled above by the ._x_marker conditionals...) + // Second, when a node is "wrapped", it gets registered as a "removal" and the wrapper + // as an "addition". We don't want to remove, then re-initialize the node, so we look + // and see if it's inside any added nodes (wrappers) and skip it. + // (This is handled below by the .contains conditional...) + for (let node of removedNodes) { - // If an element gets moved on a page, it's registered - // as both an "add" and "remove", so we want to skip those. - if (addedNodes.has(node)) continue + if (addedNodes.some(i => i.contains(node))) continue onElRemoveds.forEach(i => i(node)) } - // Mutations are bundled together by the browser but sometimes - // for complex cases, there may be javascript code adding a wrapper - // and then an alpine component as a child of that wrapper in the same - // function and the mutation observer will receive 2 different mutations. - // when it comes time to run them, the dom contains both changes so the child - // element would be processed twice as Alpine calls initTree on - // both mutations. We mark all nodes as _x_ignored and only remove the flag - // when processing the node to avoid those duplicates. - addedNodes.forEach((node) => { - node._x_ignoreSelf = true - node._x_ignore = true - }) for (let node of addedNodes) { - // If the node was eventually removed as part of one of his - // parent mutations, skip it - if (removedNodes.has(node)) continue if (! node.isConnected) continue - delete node._x_ignoreSelf - delete node._x_ignore onElAddeds.forEach(i => i(node)) - node._x_ignore = true - node._x_ignoreSelf = true } - addedNodes.forEach((node) => { - delete node._x_ignoreSelf - delete node._x_ignore - }) addedNodes = null removedNodes = null