فهرست منبع

Merge branch 'main' into add-id-magic

Caleb Porzio 3 سال پیش
والد
کامیت
06dd0b7e0b

+ 7 - 6
package-lock.json

@@ -7798,14 +7798,15 @@
             }
         },
         "packages/alpinejs": {
-            "version": "3.4.2",
+            "version": "3.5.0",
             "license": "MIT",
             "dependencies": {
                 "@vue/reactivity": "~3.1.1"
             }
         },
         "packages/collapse": {
-            "version": "3.4.2",
+            "name": "@alpinejs/collapse",
+            "version": "3.5.0",
             "license": "MIT"
         },
         "packages/csp": {
@@ -7818,7 +7819,7 @@
         },
         "packages/docs": {
             "name": "@alpinejs/docs",
-            "version": "3.4.2-revision.2",
+            "version": "3.5.0-revision.1",
             "license": "MIT"
         },
         "packages/history": {
@@ -7831,7 +7832,7 @@
         },
         "packages/intersect": {
             "name": "@alpinejs/intersect",
-            "version": "3.4.2",
+            "version": "3.5.0",
             "license": "MIT"
         },
         "packages/morph": {
@@ -7844,12 +7845,12 @@
         },
         "packages/persist": {
             "name": "@alpinejs/persist",
-            "version": "3.4.2",
+            "version": "3.5.0",
             "license": "MIT"
         },
         "packages/trap": {
             "name": "@alpinejs/trap",
-            "version": "3.4.2",
+            "version": "3.5.0",
             "license": "MIT",
             "dependencies": {
                 "focus-trap": "^6.6.1"

+ 1 - 1
packages/alpinejs/package.json

@@ -1,6 +1,6 @@
 {
     "name": "alpinejs",
-    "version": "3.5.0",
+    "version": "3.5.2",
     "description": "The rugged, minimal JavaScript framework",
     "author": "Caleb Porzio",
     "license": "MIT",

+ 5 - 3
packages/alpinejs/src/alpine.js

@@ -3,17 +3,17 @@ import { mutateDom, deferMutations, flushAndStopDeferringMutations } from './mut
 import { mapAttributes, directive, setPrefix as prefix } from './directives'
 import { start, addRootSelector, closestRoot, initTree } from './lifecycle'
 import { setEvaluator, evaluate, evaluateLater } from './evaluator'
+import { mergeProxies, closestDataStack } from './scope'
 import { transition } from './directives/x-transition'
+import { clone, skipDuringClone } from './clone'
 import { interceptor } from './interceptor'
-import { setStyles } from './utils/styles'
 import { debounce } from './utils/debounce'
 import { throttle } from './utils/throttle'
-import { mergeProxies } from './scope'
+import { setStyles } from './utils/styles'
 import { nextTick } from './nextTick'
 import { plugin } from './plugin'
 import { magic } from './magics'
 import { store } from './store'
-import { clone } from './clone'
 import { data } from './datas'
 
 let Alpine = {
@@ -25,6 +25,8 @@ let Alpine = {
     flushAndStopDeferringMutations,
     disableEffectScheduling,
     setReactivityEngine,
+    closestDataStack,
+    skipDuringClone,
     addRootSelector,
     deferMutations,
     mapAttributes,

+ 3 - 7
packages/alpinejs/src/clone.js

@@ -4,24 +4,20 @@ import { walk } from "./utils/walk"
 
 let isCloning = false
 
-export function skipDuringClone(callback) {
-    return (...args) => isCloning || callback(...args)
+export function skipDuringClone(callback, fallback = () => {}) {
+    return (...args) => isCloning ? fallback(...args) : callback(...args)
 }
 
 export function onlyDuringClone(callback) {
     return (...args) => isCloning && callback(...args)
 }
 
-export function skipWalkingSubClone(callback) {
-    return (...args) => isCloning || callback(...args)
-}
-
 export function interuptCrawl(callback) {
     return (...args) => isCloning || callback(...args)
 }
 
 export function clone(oldEl, newEl) {
-    newEl._x_dataStack = oldEl._x_dataStack
+    if (! newEl._x_dataStack) newEl._x_dataStack = oldEl._x_dataStack
 
     isCloning = true
 

+ 6 - 0
packages/alpinejs/src/evaluator.js

@@ -97,11 +97,17 @@ function generateEvaluatorFromString(dataStack, expression, el) {
             if (func.finished) {
                 // Return the immediate result.
                 runIfTypeOfFunction(receiver, func.result, completeScope, params, el)
+                // Once the function has run, we clear func.result so we don't create
+                // memory leaks. func is stored in the evaluatorMemo and every time
+                // it runs, it assigns the evaluated expression to result which could
+                // potentially store a reference to the DOM element that will be removed later on.
+                func.result = undefined
             } else {
                 // If not, return the result when the promise resolves.
                 promise.then(result => {
                     runIfTypeOfFunction(receiver, result, completeScope, params, el)
                 }).catch( error => handleError( error, el, expression ) )
+                .finally( () => func.result = undefined )
             }
         }
     }

+ 21 - 1
packages/alpinejs/src/mutation.js

@@ -160,13 +160,33 @@ function onMutate(mutations) {
         onAttributeAddeds.forEach(i => i(el, attrs))
     })
 
+    // 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 an element gets moved on a page, it's registered
+        // If an element gets moved on a page, it's registered
         // as both an "add" and "remove", so we want to skip those.
         if (removedNodes.includes(node)) 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
+    })
 
     for (let node of removedNodes) {
         // If an element gets moved on a page, it's registered

+ 1 - 1
packages/collapse/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@alpinejs/collapse",
-    "version": "3.5.0",
+    "version": "3.5.2",
     "description": "Collapse and expand elements with robust animations",
     "author": "Caleb Porzio",
     "license": "MIT",

+ 23 - 6
packages/collapse/src/index.js

@@ -4,7 +4,12 @@ export default function (Alpine) {
         let floor = 0
 
         if (! el._x_isShown) el.style.height = `${floor}px`
-        if (! el._x_isShown) el.style.removeProperty('display')
+        // We use the hidden attribute for the benefit of Tailwind
+        // users as the .space utility will ignore [hidden] elements.
+        // We also use display:none as the hidden attribute has very
+        // low CSS specificity and could be accidentally overriden
+        // by a user.
+        if (! el._x_isShown) el.hidden = true
         if (! el._x_isShown) el.style.overflow = 'hidden'
 
         // Override the setStyles function with one that won't
@@ -24,9 +29,12 @@ export default function (Alpine) {
 
         el._x_transition = {
             in(before = () => {}, after = () => {}) {
+                el.hidden = false;
+
                 let current = el.getBoundingClientRect().height
 
                 Alpine.setStyles(el, {
+                    display: null,
                     height: 'auto',
                 })
 
@@ -46,17 +54,26 @@ export default function (Alpine) {
             },
 
             out(before = () => {}, after = () => {}) {
-                Alpine.setStyles(el, {
-                    overflow: 'hidden'
-                })
-
                 let full = el.getBoundingClientRect().height
 
                 Alpine.transition(el, setFunction, {
                     during: transitionStyles,
                     start: { height: full+'px' },
                     end: { height: floor+'px' },
-                }, () => {}, () => el._x_isShown = false)
+                }, () => {}, () => {
+                    el._x_isShown = false
+
+                    // check if element is fully collapsed
+                    if (el.style.height == `${floor}px`) {
+                        Alpine.nextTick(() => {
+                            Alpine.setStyles(el, {
+                                display: 'none',
+                                overflow: 'hidden'
+                            })
+                            el.hidden = true;
+                        })
+                    }
+                })
             },
         }
     })

+ 1 - 1
packages/docs/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@alpinejs/docs",
-    "version": "3.5.0-revision.1",
+    "version": "3.5.2-revision.1",
     "description": "The documentation for Alpine",
     "author": "Caleb Porzio",
     "license": "MIT"

+ 1 - 1
packages/docs/src/en/essentials/installation.md

@@ -33,7 +33,7 @@ This is by far the simplest way to get started with Alpine. Include the followin
 Notice the `@3.x.x` in the provided CDN link. This will pull the latest version of Alpine version 3. For stability in production, it's recommended that you hardcode the latest version in the CDN link.
 
 ```alpine
-<script defer src="https://unpkg.com/alpinejs@3.5.0/dist/cdn.min.js"></script>
+<script defer src="https://unpkg.com/alpinejs@3.5.2/dist/cdn.min.js"></script>
 ```
 
 That's it! Alpine is now available for use inside your page.

+ 2 - 2
packages/docs/src/en/essentials/lifecycle.md

@@ -70,7 +70,7 @@ The two main behavioral differences with this approach are:
 ## Alpine initialization
 
 <a name="alpine-initializing"></a>
-### `Alpine.initializing`
+### `alpine:init`
 
 Ensuring a bit of code executes after Alpine is loaded, but BEFORE it initializes itself on the page is a necessary task.
 
@@ -85,7 +85,7 @@ document.addEventListener('alpine:init', () => {
 ```
 
 <a name="alpine-initialized"></a>
-### `Alpine.initialized`
+### `alpine:initialized`
 
 Alpine also offers a hook that you can use to execute code After it's done initializing called `alpine:initialized`:
 

+ 251 - 0
packages/docs/src/en/plugins/morph.md

@@ -0,0 +1,251 @@
+---
+order: 5
+title: Morph
+description: Morph an element into the provided HTML
+graph_image: https://alpinejs.dev/social_morph.jpg
+---
+
+# Morph Plugin
+
+Alpine's Morph plugin allows you to "morph" an element on the page into the provided HTML template, all while preserving any browser or Alpine state within the "morphed" element.
+
+This is useful for updating HTML from a server request without loosing Alpine's on-page state. A utility like this is at the core of full-stack frameworks like [Laravel Livewire](https://laravel-livewire.com/) and [Phoenix LiveView](https://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript).
+
+The best way to understand its purpose is with the following interactive visualization. Give it a try!
+
+<!-- START_VERBATIM -->
+<div x-data="{ slide: 1 }" class="border rounded">
+    <div>
+        <img :src="'/img/morphs/morph'+slide+'.png'">
+    </div>
+
+    <div class="flex w-full justify-between" style="padding-bottom: 1rem">
+        <div class="w-1/2 px-4">
+            <button @click="slide = (slide === 1) ? 13 : slide - 1" class="w-full bg-brand rounded-full text-center py-3 font-bold text-white">Previous</button>
+        </div>
+        <div class="w-1/2 px-4">
+            <button @click="slide = (slide % 13) + 1" class="w-full bg-brand rounded-full text-center py-3 font-bold text-white">Next</button>
+        </div>
+    </div>
+</div>
+<!-- END_VERBATIM -->
+
+<a name="installation"></a>
+## Installation
+
+You can use this plugin by either including it from a `<script>` tag or installing it via NPM:
+
+### Via CDN
+
+You can include the CDN build of this plugin as a `<script>` tag, just make sure to include it BEFORE Alpine's core JS file.
+
+```alpine
+<!-- Alpine Plugins -->
+<script defer src="https://unpkg.com/@alpinejs/morph@3.x.x/dist/cdn.min.js"></script>
+
+<!-- Alpine Core -->
+<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
+```
+
+### Via NPM
+
+You can install Morph from NPM for use inside your bundle like so:
+
+```shell
+npm install @alpinejs/morph
+```
+
+Then initialize it from your bundle:
+
+```js
+import Alpine from 'alpinejs'
+import morph from '@alpinejs/morph'
+
+Alpine.plugin(morph)
+
+...
+```
+
+<a name="alpine-morph"></a>
+## Alpine.morph()
+
+The `Alpine.morph(el, newHtml)` allows you to imperatively morph a dom node based on passed in HTML. It accepts the following parameters:
+
+| Parameter | Description |
+| ---       | --- |
+| `el`      | A DOM element on the page. |
+| `newHtml` | A string of HTML to use as the template to morph the dom element into. |
+| `options` (optional) | An options object used mainly for [injecting lifecycle hooks](#lifecycle-hooks). |
+
+Here's an example of using `Alpine.morph()` to update an Alpine component with new HTML: (In real apps, this new HTML would likely be coming from the server)
+
+```alpine
+<div x-data="{ message: 'Change me, then press the button!' }">
+    <input type="text" x-model="message"> 
+    <span x-text="message"></span> 
+</div>
+
+<button>Run Morph</button>
+
+<script>
+    document.querySelector('button').addEventListener('click', () => {
+        let el = document.querySelector('div')
+
+        Alpine.morph(el, `
+            <div x-data="{ message: 'Change me, then press the button!' }">
+                <h2>See how new elements have been added</h2>
+                
+                <input type="text" x-model="message"> 
+                <span x-text="message"></span> 
+
+                <h2>but the state of this component hasn't changed? Magical.</h2>
+            </div>
+        `)
+    })
+</script>
+```
+
+<!-- START_VERBATIM -->
+<div class="demo">
+    <div x-data="{ message: 'Change me, then press the button!' }" id="morph-demo-1" class="space-y-2">
+        <input type="text" x-model="message" class="w-full">
+        <span x-text="message"></span> 
+    </div>
+    
+    <button id="morph-button-1" class="mt-4">Run Morph</button>
+</div>
+
+<script>
+    document.querySelector('#morph-button-1').addEventListener('click', () => {
+        let el = document.querySelector('#morph-demo-1')
+
+        Alpine.morph(el, `
+            <div x-data="{ message: 'Change me, then press the button!' }" id="morph-demo-1" class="space-y-2">
+                <h4>See how new elements have been added</h4>
+                <input type="text" x-model="message" class="w-full"> 
+                <span x-text="message"></span> 
+                <h4>but the state of this component hasn't changed? Magical.</h4>
+            </div>
+        `)
+    })
+</script>
+<!-- END_VERBATIM -->
+
+<a name="lifecycle-hooks"></a>
+### Lifecycle Hooks
+
+The "Morph" plugin works by comparing two DOM trees, the live element, and the passed in HTML.
+
+Morph walks both trees simultaneusly and compares each node and its children. If it finds differences, it "patches" (changes) the current DOM tree to match the passed in HTML's tree.
+
+While the default algorithm is very capable, there are cases where you may want to hook into its lifecycle and observe or change its behavior as it's happening.
+
+Before we jump into the available Lifecycle hooks themselves, let's first list out all the potential parameters they receive and explain what each one is:
+
+| Parameter | Description |
+| ---       | --- |
+| `el` | This is always the actual, current, DOM element on the page that will be "patched" (changed by Morph). |
+| `toEl` | This is a "template element". It's a temporary element representing what the live `el` will be patched to. It will never actually live on the page and should only be used for reference purposes. |
+| `childrenOnly()` | This is a function that can be called inside the hook to tell Morph to skip the current element and only "patch" its children. |
+| `skip()` | A function that when called within the hook will "skip" comparing/patching itself and the children of the current element. |
+
+Here are the available lifecycle hooks (passed in as the third parameter to `Alpine.morph(..., options)`):
+
+| Option | Description |
+| ---       | --- |
+| `updating(el, toEl, childrenOnly, skip)` | Called before patching the `el` with the comparison `toEl`.  |
+| `updated(el, toEl)` | Called after Morph has patched `el`. |
+| `removing(el, skip)` | Called before Morph removes an element from the live DOM. |
+| `removed(el)` | Called after Morph has removed an element from the live DOM. |
+| `adding(el, sip)` | Called before adding a new element. |
+| `added(el)` | Called after adding a new element to the live DOM tree. |
+| `key(el)` | A re-usable function to determine how Morph "keys" elements in the tree before comparing/patching. [More on that here](#keys) |
+| `lookahead` | A boolean value telling Morph to enable an extra feature in its algorithm that "looks ahead" to make sure a DOM element that's about to be removed should instead just be "moved" to a later sibling. |
+
+Here is code of all these lifecycle hooks for a more concrete reference:
+
+```js
+Alpine.morph(el, newHtml, {
+    updating(el, toEl, childrenOnly, skip) {
+        //
+    },
+
+    updated(el, toEl) {
+        //
+    },
+
+    removing(el, skip) {
+        //
+    },
+
+    removed(el) {
+        //
+    },
+
+    adding(el, skip) {
+        //
+    },
+
+    added(el) {
+        //
+    },
+
+    key(el) {
+        // By default Alpine uses the `key=""` HTML attribute.
+        return el.id
+    },
+
+    lookahead: true, // Default: false
+})
+```
+
+<a name="keys"></a>
+### Keys
+
+Dom-diffing utilities like Morph try their best to accurately "morph" the original DOM into the new HTML. However, there are cases where it's impossible to determine if an element should be just changed, or replaced completely.
+
+Because of this limitation, Morph has a "key" system that allows developers to "force" preserving certain elements rather than replacing them.
+
+The most common use-case for them is a list of siblings within a loop. Below is an example of why keys are necessary sometimes:
+
+```html
+<!-- "Live" Dom on the page: -->
+<ul>
+    <li>Mark</li>
+    <li>Tom</li>
+    <li>Travis</li>
+</ul>
+
+<!-- New HTML to "morph to": -->
+<ul>
+    <li>Travis</li>
+    <li>Mark</li>
+    <li>Tom</li>
+</ul>
+```
+
+Given the above situation, Morph has no way to know that the "Travis" node has been moved in the DOM tree. It just thinks that "Mark" has been changed to "Travis" and "Travis" changed to "Tom".
+
+This is not what we actually want, we want Morph to preserve the original elements and instead of changing them, MOVE them within the `<ul>`.
+
+By adding keys to each node, we can accomplish this like so:
+
+```html
+<!-- "Live" Dom on the page: -->
+<ul>
+    <li key="1">Mark</li>
+    <li key="2">Tom</li>
+    <li key="3">Travis</li>
+</ul>
+
+<!-- New HTML to "morph to": -->
+<ul>
+    <li key="3">Travis</li>
+    <li key="1">Mark</li>
+    <li key="2">Tom</li>
+</ul>
+```
+
+Now that there are "keys" on the `<li>`s, Morph will match them in both trees and move them accordingly.
+
+You can configure what Morph considers a "key" with the `key:` configutation option. [More on that here](#lifecycle-hooks)

+ 4 - 4
packages/docs/src/en/plugins/trap.md

@@ -191,8 +191,8 @@ By adding `.inert` to `x-trap`, when focus is trapped, all other elements on the
 
 ```alpine
 <!-- When `open` is `false`: -->
-<body>
-    <div x-trap="open" ...>
+<body x-data="{ open: false }">
+    <div x-trap.inert="open" ...>
         ...
     </div>
 
@@ -202,8 +202,8 @@ By adding `.inert` to `x-trap`, when focus is trapped, all other elements on the
 </body>
 
 <!-- When `open` is `true`: -->
-<body>
-    <div x-trap="open" ...>
+<body x-data="{ open: true }">
+    <div x-trap.inert="open" ...>
         ...
     </div>
 

+ 1 - 1
packages/intersect/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@alpinejs/intersect",
-    "version": "3.5.0",
+    "version": "3.5.2",
     "description": "Trigger JavaScript when an element enters the viewport",
     "author": "Caleb Porzio",
     "license": "MIT",

+ 2 - 5
packages/morph/package.json

@@ -1,12 +1,9 @@
 {
     "name": "@alpinejs/morph",
-    "version": "3.0.0-alpha.0",
+    "version": "3.5.2",
     "description": "Diff and patch a block of HTML on a page with an HTML template",
     "author": "Caleb Porzio",
     "license": "MIT",
     "main": "dist/module.cjs.js",
-    "module": "dist/module.esm.js",
-    "dependencies": {
-        "@vue/reactivity": "^3.0.2"
-    }
+    "module": "dist/module.esm.js"
 }

+ 1 - 11
packages/morph/src/index.js

@@ -1,17 +1,7 @@
 import { morph } from './morph'
 
 export default function (Alpine) {
-    Alpine.directive('morph', (el, { expression }, { effect, evaluateLater }) => {
-        let evaluate = evaluateLater(expression)
-
-        effect(() => {
-            evaluate(value => {
-                let child = el.firstElementChild || el.firstChild || el.appendChild(document.createTextNode(''))
-
-                morph(child, value)
-            })
-        })
-    })
+    Alpine.morph = morph
 }
 
 export { morph }

+ 90 - 115
packages/morph/src/morph.js

@@ -1,12 +1,40 @@
+let resolveStep = () => {}
 
-export function morph(dom, toHtml, options) {
+let logger = () => {}
+
+function breakpoint(message) {
+    if (! debug) return
+
+    message && logger(message.replace('\n', '\\n'))
+   
+    return new Promise(resolve => resolveStep = () => resolve())
+}
+
+export async function morph(dom, toHtml, options) {
     assignOptions(options)
+    
+    let toEl = createElement(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 && ! dom._x_dataStack) {
+        toEl._x_dataStack = window.Alpine.closestDataStack(dom)
+        
+        toEl._x_dataStack && window.Alpine.clone(dom, toEl)
+    }
+    
+    await breakpoint()
 
-    patch(dom, createElement(toHtml))
+    patch(dom, toEl)
 
     return dom
 }
 
+morph.step = () => resolveStep()
+morph.log = (theLogger) => {
+    logger = theLogger
+}
+
 let key
 ,lookahead
 ,updating
@@ -15,52 +43,59 @@ let key
 ,removed
 ,adding
 ,added
+,debug
 
 let noop = () => {}
 
 function assignOptions(options = {}) {
     let defaultGetKey = el => el.getAttribute('key')
 
-    key = options.key || defaultGetKey
-    lookahead = options.lookahead || false
     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 || true 
+    debug = options.debug || false
 }
 
 function createElement(html) {
     return document.createRange().createContextualFragment(html).firstElementChild
 }
 
-function patch(dom, to) {
+async function patch(dom, to) {
     if (dom.isEqualNode(to)) return
-
+   
     if (differentElementNamesTypesOrKeys(dom, to)) {
-        return patchElement(dom, to)
+        let result = patchElement(dom, to)
+        
+        await breakpoint('Swap elements')
+       
+        return result
     }
 
     let updateChildrenOnly = false
 
     if (shouldSkip(updating, dom, to, () => updateChildrenOnly = true)) return
 
-    // window.Alpine && initializeAlpineOnTo(dom, to, () => updateChildrenOnly = true)
+    window.Alpine && initializeAlpineOnTo(dom, to, () => updateChildrenOnly = true)
 
     if (textOrComment(to)) {
-        patchNodeValue(dom, to)
+        await patchNodeValue(dom, to)
         updated(dom, to)
+        
         return
     }
 
     if (! updateChildrenOnly) {
-        patchAttributes(dom, to)
+        await patchAttributes(dom, to)
     }
 
     updated(dom, to)
 
-    patchChildren(dom, to)
+    await patchChildren(dom, to)
 }
 
 function differentElementNamesTypesOrKeys(dom, to) {
@@ -75,11 +110,11 @@ function textOrComment(el) {
 }
 
 function patchElement(dom, to) {
-    if(shouldSkip(removing, dom)) return
+    if (shouldSkip(removing, dom)) return
 
     let toCloned = to.cloneNode(true)
 
-    if(shouldSkip(adding, toCloned)) return
+    if (shouldSkip(adding, toCloned)) return
 
     dom.parentNode.replaceChild(toCloned, dom)
 
@@ -87,20 +122,22 @@ function patchElement(dom, to) {
     added(toCloned)
 }
 
-function patchNodeValue(dom, to) {
+async function patchNodeValue(dom, to) {
     let value = to.nodeValue
 
-    if (dom.nodeValue !== value) dom.nodeValue = value
+    if (dom.nodeValue !== value) {
+        dom.nodeValue = value
+
+        await breakpoint('Change text node to: ' + value)
+    }
 }
 
-function patchAttributes(dom, to) {
+async function patchAttributes(dom, to) {
     if (dom._x_isShown && ! to._x_isShown) {
         return
-        // dom._x_hide()
     }
     if (! dom._x_isShown && to._x_isShown) {
         return
-        // dom._x_show()
     }
 
     let domAttributes = Array.from(dom.attributes)
@@ -109,18 +146,26 @@ function patchAttributes(dom, to) {
     for (let i = domAttributes.length - 1; i >= 0; i--) {
         let name = domAttributes[i].name;
 
-        if (! to.hasAttribute(name)) dom.removeAttribute(name)
+        if (! to.hasAttribute(name)) {
+            dom.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 (dom.getAttribute(name) !== value) dom.setAttribute(name, value)
+        if (dom.getAttribute(name) !== value) {
+            dom.setAttribute(name, value)
+
+            await breakpoint(`Set [${name}] attribute to: "${value}"`)
+        }
     }
 }
 
-function patchChildren(dom, to) {
+async function patchChildren(dom, to) {
     let domChildren = dom.childNodes
     let toChildren = to.childNodes
 
@@ -143,9 +188,15 @@ function patchChildren(dom, to) {
 
                 dom.appendChild(holdover)
                 currentFrom = holdover
+
+                await breakpoint('Add element (from key)')
             } else {
-                addNodeTo(currentTo, dom)
+                let added = addNodeTo(currentTo, dom)
+
+                await breakpoint('Add element: ' + added.outerHTML || added.nodeValue)
+
                 currentTo = currentTo.nextSibling
+
                 continue
             }
         }
@@ -157,6 +208,8 @@ function patchChildren(dom, to) {
                 currentFrom = addNodeBefore(currentTo, currentFrom)
 
                 domKey = getKey(currentFrom)
+                
+                await breakpoint('Move element (lookahead)')
             }
         }
 
@@ -167,6 +220,9 @@ function patchChildren(dom, to) {
                 domKeyHoldovers[domKey].remove()
                 currentFrom = currentFrom.nextSibling
                 currentTo = currentTo.nextSibling
+
+                await breakpoint('No "to" key')
+
                 continue
             }
 
@@ -174,6 +230,8 @@ function patchChildren(dom, to) {
                 if (domKeyDomNodeMap[toKey]) {
                     currentFrom.parentElement.replaceChild(domKeyDomNodeMap[toKey], currentFrom)
                     currentFrom = domKeyDomNodeMap[toKey]
+                    
+                    await breakpoint('No "from" key')
                 }
             }
 
@@ -184,19 +242,24 @@ function patchChildren(dom, to) {
                 if (domKeyNode) {
                     currentFrom.parentElement.replaceChild(domKeyNode, currentFrom)
                     currentFrom = domKeyNode
+                    
+                    await breakpoint('Move "from" key')
                 } else {
                     domKeyHoldovers[domKey] = currentFrom
                     currentFrom = addNodeBefore(currentTo, currentFrom)
                     domKeyHoldovers[domKey].remove()
                     currentFrom = currentFrom.nextSibling
                     currentTo = currentTo.nextSibling
+                   
+                    await breakpoint('I dont even know what this does')
+                    
                     continue
                 }
             }
         }
 
         // Patch elements
-        patch(currentFrom, currentTo)
+        await patch(currentFrom, currentTo)
 
         currentTo = currentTo && currentTo.nextSibling
         currentFrom = currentFrom && currentFrom.nextSibling
@@ -209,6 +272,8 @@ function patchChildren(dom, to) {
 
             dom.removeChild(domForRemoval)
 
+            await breakpoint('remove el')
+
             removed(domForRemoval)
         }
 
@@ -249,6 +314,8 @@ function addNodeTo(node, parent) {
         parent.appendChild(clone);
 
         added(clone)
+
+        return clone
     }
 }
 
@@ -275,96 +342,4 @@ function initializeAlpineOnTo(from, to, childrenOnly) {
         // This should simulate backend Livewire being aware of Alpine changes.
         window.Alpine.clone(from, to)
     }
-
-    // x-show elements require care because of transitions.
-    if (
-        Array.from(from.attributes)
-            .map(attr => attr.name)
-            .some(name => /x-show/.test(name))
-    ) {
-        if (from._x_transition) {
-            // This covers @entangle('something')
-            // childrenOnly()
-        } else {
-            // This covers x-show="$wire.something"
-            //
-            // If the element has x-show, we need to "reverse" the damage done by "clone",
-            // so that if/when the element has a transition on it, it will occur naturally.
-            // if (isHiding(from, to)) {
-            //     let style = to.getAttribute('style')
-            //     to.setAttribute('style', style.replace('display: none;', ''))
-            // } else if (isShowing(from, to)) {
-            //     to.style.display = from.style.display
-            // }
-        }
-    }
-}
-
-function isHiding(from, to) {
-    return from._x_isShown && ! to._x_isShown
 }
-
-function isShowing(from, to) {
-    return ! from._x_isShown && to._x_isShown
-}
-
-
-// This is from Livewire:
-
-
-// function alpinifyElementsForMorphdom(from, to) {
-//     // If the element we are updating is an Alpine component...
-//     if (from.__x) {
-//         // 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.__x, to)
-//     }
-
-//     // x-show elements require care because of transitions.
-//     if (
-//         Array.from(from.attributes)
-//             .map(attr => attr.name)
-//             .some(name => /x-show/.test(name))
-//     ) {
-//         if (from.__x_transition) {
-//             // This covers @entangle('something')
-//             from.skipElUpdatingButStillUpdateChildren = true
-//         } else {
-//             // This covers x-show="$wire.something"
-//             //
-//             // If the element has x-show, we need to "reverse" the damage done by "clone",
-//             // so that if/when the element has a transition on it, it will occur naturally.
-//             if (isHiding(from, to)) {
-//                 let style = to.getAttribute('style')
-
-//                 if (style) {
-//                     to.setAttribute('style', style.replace('display: none;', ''))
-//                 }
-//             } else if (isShowing(from, to)) {
-//                 to.style.display = from.style.display
-//             }
-//         }
-//     }
-// }
-
-// function isHiding(from, to) {
-//     if (beforeAlpineTwoPointSevenPointThree()) {
-//         return from.style.display === '' && to.style.display === 'none'
-//     }
-
-//     return from._x_isShown && ! to._x_isShown
-// }
-
-// function isShowing(from, to) {
-//     if (beforeAlpineTwoPointSevenPointThree()) {
-//         return from.style.display === 'none' && to.style.display === ''
-//     }
-
-//     return ! from._x_isShown && to._x_isShown
-// }
-
-// function beforeAlpineTwoPointSevenPointThree() {
-//     let [major, minor, patch] = window.Alpine.version.split('.').map(i => Number(i))
-
-//     return major <= 2 && minor <= 7 && patch <= 2
-// }

+ 1 - 1
packages/persist/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@alpinejs/persist",
-    "version": "3.5.0",
+    "version": "3.5.2",
     "description": "Persist Alpine data across page loads",
     "author": "Caleb Porzio",
     "license": "MIT",

+ 1 - 1
packages/persist/src/index.js

@@ -27,7 +27,7 @@ export default function (Alpine) {
         })
     }
 
-    Alpine.$persist = persist()
+    Object.defineProperty(Alpine, '$persist', { get: () => persist() })
     Alpine.magic('persist', persist)
 }
 

+ 1 - 1
packages/trap/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@alpinejs/trap",
-    "version": "3.5.0",
+    "version": "3.5.2",
     "description": "Trap focus on a page within a specific element",
     "author": "Caleb Porzio",
     "license": "MIT",

+ 49 - 41
packages/trap/src/index.js

@@ -1,47 +1,55 @@
 import { createFocusTrap } from 'focus-trap';
 
 export default function (Alpine) {
-    Alpine.directive('trap', (el, { expression, modifiers }, { effect, evaluateLater }) => {
-        let evaluator = evaluateLater(expression)
-
-        let oldValue = false
-
-        let trap = createFocusTrap(el, { 
-            escapeDeactivates: false,
-            allowOutsideClick: true,
-            fallbackFocus: () => el,
-        })
-
-        let undoInert = () => {}
-        let undoDisableScrolling = () => {}
-
-        effect(() => evaluator(value => {
-            if (oldValue === value) return
-
-            // Start trapping.
-            if (value && ! oldValue) {
-                setTimeout(() => {
-                    if (modifiers.includes('inert')) undoInert = setInert(el)
-                    if (modifiers.includes('noscroll')) undoDisableScrolling = disableScrolling()
-
-                    trap.activate()
-                });
-            }
-
-            // Stop trapping.
-            if (! value && oldValue) {
-                undoInert()
-                undoInert = () => {}
-
-                undoDisableScrolling()
-                undoDisableScrolling = () => {}
-
-                trap.deactivate()
-            }
-
-            oldValue = !! value
-        }))
-    })
+    Alpine.directive('trap', Alpine.skipDuringClone(
+        (el, { expression, modifiers }, { effect, evaluateLater }) => {
+            let evaluator = evaluateLater(expression)
+
+            let oldValue = false
+
+            let trap = createFocusTrap(el, { 
+                escapeDeactivates: false,
+                allowOutsideClick: true,
+                fallbackFocus: () => el,
+            })
+
+            let undoInert = () => {}
+            let undoDisableScrolling = () => {}
+
+            effect(() => evaluator(value => {
+                if (oldValue === value) return
+
+                // Start trapping.
+                if (value && ! oldValue) {
+                    setTimeout(() => {
+                        if (modifiers.includes('inert')) undoInert = setInert(el)
+                        if (modifiers.includes('noscroll')) undoDisableScrolling = disableScrolling()
+
+                        trap.activate()
+                    });
+                }
+
+                // Stop trapping.
+                if (! value && oldValue) {
+                    undoInert()
+                    undoInert = () => {}
+
+                    undoDisableScrolling()
+                    undoDisableScrolling = () => {}
+
+                    trap.deactivate()
+                }
+
+                oldValue = !! value
+            }))
+        },
+        // When cloning, we only want to add aria-hidden attributes to the
+        // DOM and not try to actually trap, as trapping can mess with the
+        // live DOM and isn't just isolated to the cloned DOM.
+        (el, { expression, modifiers }, { evaluate }) => {
+            if (modifiers.includes('inert') && evaluate(expression)) setInert(el)
+        },
+    ))
 }
 
 function setInert(el) {

+ 6 - 0
scripts/release.js

@@ -50,6 +50,9 @@ function writeNewAlpineVersion() {
 
     writeToPackageDotJson('collapse', 'version', version)
     console.log('Bumping @alpinejs/collapse package.json: '+version)
+
+    writeToPackageDotJson('morph', 'version', version)
+    console.log('Bumping @alpinejs/morph package.json: '+version)
 }
 
 function writeNewDocsVersion() {
@@ -83,6 +86,9 @@ function publish() {
     console.log('Publishing @alpinejs/collapse on NPM...');
     runFromPackage('collapse', 'npm publish --access public')
 
+    console.log('Publishing @alpinejs/morph on NPM...');
+    runFromPackage('morph', 'npm publish --access public')
+
     log('\n\nFinished!')
 }
 

+ 28 - 0
tests/cypress/integration/mutation.spec.js

@@ -110,3 +110,31 @@ test('can pause and queue mutations for later resuming/flushing',
         get('h1').should(haveText('3'))
     }
 )
+
+test('does not initialise components twice when contained in multiple mutations',
+    html`
+        <div x-data="{
+            foo: 0,
+            bar: 0,
+            test() {
+                container = document.createElement('div')
+                this.$root.appendChild(container)
+                alpineElement = document.createElement('span')
+                alpineElement.setAttribute('x-data', '{init() {this.bar++}}')
+                alpineElement.setAttribute('x-init', 'foo++')
+                container.appendChild(alpineElement)
+            }
+        }">
+            <span id="one" x-text="foo"></span>
+            <span id="two" x-text="bar"></span>
+            <button @click="test">Test</button>
+        </div>
+    `,
+    ({ get }) => {
+        get('span#one').should(haveText('0'))
+        get('span#two').should(haveText('0'))
+        get('button').click()
+        get('span#one').should(haveText('1'))
+        get('span#two').should(haveText('1'))
+    }
+)

+ 29 - 4
tests/cypress/integration/plugins/collapse.spec.js

@@ -1,19 +1,23 @@
-import { haveAttribute, haveComputedStyle, html, test } from '../../utils'
+import { haveAttribute, haveComputedStyle, html, notHaveAttribute, test } from '../../utils'
 
 test('can collapse and expand element',
     [html`
         <div x-data="{ expanded: false }">
             <button @click="expanded = ! expanded">toggle</button>
-            <h1 x-show="expanded" x-collapse>contents</h1>
+            <h1 x-show="expanded" x-collapse>contents <a href="#">focusable content</a></h1>
         </div>
     `],
     ({ get }, reload) => {
         get('h1').should(haveComputedStyle('height', '0px'))
-        get('h1').should(haveAttribute('style', 'height: 0px; overflow: hidden;'))
+        get('h1').should(haveAttribute('style', 'display: none; height: 0px; overflow: hidden;'))
+        get('h1').should(haveAttribute('hidden', 'hidden'))
         get('button').click()
         get('h1').should(haveAttribute('style', 'height: auto;'))
+        get('h1').should(notHaveAttribute('hidden'))
         get('button').click()
         get('h1').should(haveComputedStyle('height', '0px'))
+        get('h1').should(haveAttribute('style', 'height: 0px; display: none; overflow: hidden;'))
+        get('h1').should(haveAttribute('hidden', 'hidden'))
     },
 )
 
@@ -32,7 +36,6 @@ test('@click.away with x-collapse (prevent race condition)',
     }
 )
 
-
 test('@click.away with x-collapse and borders (prevent race condition)',
     html`
         <div x-data="{ show: false }">
@@ -47,3 +50,25 @@ test('@click.away with x-collapse and borders (prevent race condition)',
         get('h1').should(haveAttribute('style', 'height: auto;'))
     }
 )
+
+// https://github.com/alpinejs/alpine/issues/2335
+test('double-click on x-collapse does not mix styles up',
+    [html`
+        <div x-data="{ expanded: false }">
+            <button @click="expanded = ! expanded">toggle</button>
+            <h1 x-show="expanded" x-collapse>contents</h1>
+        </div>
+    `],
+    ({ get }, reload) => {
+        get('h1').should(haveComputedStyle('height', '0px'))
+        get('h1').should(haveAttribute('style', 'display: none; height: 0px; overflow: hidden;'))
+        get('button').click()
+        get('button').click()
+        get('h1').should(haveAttribute('style', 'height: 0px; display: none; overflow: hidden;'))
+        get('button').click()
+        get('h1').should(haveAttribute('style', 'height: auto;'))
+        get('button').click()
+        get('button').click()
+        get('h1').should(haveAttribute('style', 'height: auto;'))
+    },
+)

+ 74 - 45
tests/cypress/integration/plugins/morph.spec.js

@@ -1,65 +1,94 @@
 import { haveText, html, test } from '../../utils'
 
-test('can morph components',
+test('can morph components and preserve Alpine state',
     [html`
-        <div x-data="{ frame: 0 }">
-            <template x-ref="0">
-                <h1><div></div>foo</h1>
-            </template>
-
-            <template x-ref="1">
-                <h1><div x-data="{ text: 'yo' }" x-text="text"></div> foo</h1>
-            </template>
+        <div x-data="{ foo: 'bar' }">
+            <button @click="foo = 'baz'">Change Foo</button>
+            <span x-text="foo"></span>
+        </div>
+    `],
+    ({ get }, reload, window, document) => {
+        let toHtml = document.querySelector('div').outerHTML
 
-            <template x-ref="2">
-                <h1><div x-data="{ text: 'yo' }" x-text="text + 'yo'"></div> foo</h1>
-            </template>
+        get('span').should(haveText('bar'))
+        get('button').click()
+        get('span').should(haveText('baz'))
+        
+        get('div').then(([el]) => window.Alpine.morph(el, toHtml))
 
-            <button @click="frame++">morph</button>
+        get('span').should(haveText('baz'))
+    },
+)
 
-            <article x-morph="$refs[frame % 3].innerHTML"></article>
-        </div>
+test('morphing target uses outer Alpine scope',
+    [html`
+        <article x-data="{ foo: 'bar' }">
+            <div>
+                <button @click="foo = 'baz'">Change Foo</button>
+                <span x-text="foo"></span>
+            </div>
+        </article>
     `],
-    ({ get }) => {
-        get('article h1').should(haveText('foo'))
-        get('button').click()
-        get('article h1').should(haveText('yo foo'))
+    ({ get }, reload, window, document) => {
+        let toHtml = document.querySelector('div').outerHTML
+
+        get('span').should(haveText('bar'))
         get('button').click()
-        get('article h1').should(haveText('yoyo foo'))
+        get('span').should(haveText('baz'))
+        
+        get('div').then(([el]) => window.Alpine.morph(el, toHtml))
+
+        get('span').should(haveText('baz'))
     },
 )
 
-test('components within morph retain state between',
+test('can morph with HTML change and preserve Alpine state',
     [html`
-        <div x-data="{ frame: 0 }">
-            <template x-ref="0">
-                <div x-data="{ count: 1 }">
-                    <button @click="count++">inc</button>
-
-                    <span x-text="String(frame) + count"></span>
-                </div>
-            </template>
+        <div x-data="{ foo: 'bar' }">
+            <button @click="foo = 'baz'">Change Foo</button>
+            <span x-text="foo"></span>
+        </div>
+    `],
+    ({ get }, reload, window, document) => {
+        let toHtml = document.querySelector('div').outerHTML.replace('Change Foo', 'Changed Foo')
 
-            <template x-ref="1">
-                <div x-data="{ count: 1 }">
-                    <button @click="count++">inc</button>
+        get('span').should(haveText('bar'))
+        get('button').click()
+        get('span').should(haveText('baz'))
+        get('button').should(haveText('Change Foo'))
 
-                    <span x-text="String(frame) + 'foo' + count"></span>
-                </div>
-            </template>
+        get('div').then(([el]) => window.Alpine.morph(el, toHtml))
+        
+        get('span').should(haveText('baz'))
+        get('button').should(haveText('Changed Foo'))
+    },
+)
 
-            <button @click="frame++" id="morph">morph</button>
+test('morphing an element with multiple nested Alpine components preserves scope',
+    [html`
+        <div x-data="{ foo: 'bar' }">
+            <button @click="foo = 'baz'">Change Foo</button>
+            <span x-text="foo"></span>
 
-            <article x-morph="$refs[frame % 2].innerHTML"></article>
+            <div x-data="{ bob: 'lob' }">
+                <a href="#" @click.prevent="bob = 'law'">Change Bob</a>
+                <h1 x-text="bob"></h1>
+            </div>
         </div>
     `],
-    ({ get }) => {
-        get('article span').should(haveText('01'))
-        get('article button').click()
-        get('article span').should(haveText('02'))
-        get('#morph').click()
-        get('article span').should(haveText('1foo2'))
-        get('article button').click()
-        get('article span').should(haveText('1foo3'))
+    ({ get }, reload, window, document) => {
+        let toHtml = document.querySelector('div').outerHTML
+
+        get('span').should(haveText('bar'))
+        get('h1').should(haveText('lob'))
+        get('button').click()
+        get('a').click()
+        get('span').should(haveText('baz'))
+        get('h1').should(haveText('law'))
+
+        get('div').then(([el]) => window.Alpine.morph(el, toHtml))
+        
+        get('span').should(haveText('baz'))
+        get('h1').should(haveText('law'))
     },
 )

+ 26 - 0
tests/cypress/integration/plugins/persist.spec.js

@@ -226,3 +226,29 @@ test('can persist using global Alpine.$persist within Alpine.store',
         get('span').should(haveText('Malcolm'))
     },
 )
+
+test('multiple aliases work when using global Alpine.$persist',
+    [html`
+        <div x-data>
+            <input x-model="$store.name.firstName">
+
+            <span x-text="$store.name.firstName"></span>
+            <p x-text="$store.name.lastName"></p>
+        </div>
+    `, `
+        Alpine.store('name', {
+            firstName: Alpine.$persist('John').as('first-name'),
+            lastName: Alpine.$persist('Doe').as('name-name')
+        })
+    `],
+    ({ get, window }, reload) => {
+        get('span').should(haveText('John'))
+        get('p').should(haveText('Doe'))
+        get('input').clear().type('Joe')
+        get('span').should(haveText('Joe'))
+        get('p').should(haveText('Doe'))
+        reload()
+        get('span').should(haveText('Joe'))
+        get('p').should(haveText('Doe'))
+    },
+)

+ 8 - 5
tests/cypress/integration/plugins/trap.spec.js

@@ -90,10 +90,13 @@ test('can trap focus with noscroll',
             <div style="height: 100vh">&nbsp;</div>
         </div>
     `],
-    ({ get }, reload) => {
-        get('#open').click()
-        get('html').should(haveAttribute('style', 'overflow: hidden; padding-right: 0px;'))
-        get('#close').click()
-        get('html').should(notHaveAttribute('style', 'overflow: hidden; padding-right: 0px;'))
+    ({ get, window }, reload) => {
+        window().then((win) => {
+            let scrollbarWidth = win.innerWidth - win.document.documentElement.clientWidth
+            get('#open').click()
+            get('html').should(haveAttribute('style', `overflow: hidden; padding-right: ${scrollbarWidth}px;`))
+            get('#close').click()
+            get('html').should(notHaveAttribute('style', `overflow: hidden; padding-right: ${scrollbarWidth}px;`))
+        })
     },
 )

+ 28 - 0
tests/cypress/manual-memory.html

@@ -0,0 +1,28 @@
+<html>
+    <script src="/../../packages/alpinejs/dist/cdn.js" defer></script>
+    <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
+
+    <table class="w-dull">
+        <tr>
+            <td class="p-10 w-1/3"><code>MemoEvaluator cache</code></td>
+            <td class="p-10 w-1/3">
+                <p>
+                    In chrome, open dev tools > Memory tag, click on
+                    "Take heap snapshot". First of all, check that there are not existing leaks affecting the test results clicking on
+                    "Take heap snapshot": it should not found any results;
+                    if it does, you might want to run the test in incognito
+                    mode so it does not load any chrome extensions).
+                    Once verified, click the "test" button, go to the memory
+                    tab, click "Collect garbage" a couple of times, take another snapshot and verify we don't have any new
+                    detached node in memory.
+                </p>
+            </td>
+            <td class="p-10 w-1/3">
+                <div id="one" x-data="{ foo: 'bar' }">
+                    <span x-text="foo"></span>
+                    <button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" @click="document.getElementById('one').remove()">Test</button>
+                </div>
+            </td>
+        </tr>
+    </table>
+</html>

+ 2 - 2
tests/cypress/manual-transition-test.html

@@ -174,8 +174,8 @@
                 <div x-data="{ show: false }">
                     <button @click="show = !show">Show</button>
 
-                    <div x-show="show" x-collapse>
-                        <div class="ring-2 ring-offset-2 ring-red-400">my red ring should be fully visible</div>
+                    <div x-show="show" x-collapse.duration.2000ms>
+                        <div class="ring-2 ring-offset-2 ring-red-400 py-10 bg-gray-200">my red ring should be fully visible</div>
                     </div>
                   </div>
                 </div>

+ 3 - 1
tests/cypress/utils.js

@@ -75,7 +75,9 @@ function injectHtmlAndBootAlpine(cy, templateAndPotentiallyScripts, callback, pa
             })
         }
 
-        callback(cy, reload)
+        cy.window().then(window => {
+            callback(cy, reload, window, window.document)
+        }) 
     })
 }
 

+ 0 - 102
tests/jest/morph/alpine-scope.spec.js

@@ -1,102 +0,0 @@
-let { morph } = require('@alpinejs/morph')
-let Alpine = require('alpinejs').default
-let createElement = require('./createElement.js')
-
-test('morphing an element with changed Alpine scope', () => {
-    let template = `<div x-data="{ foo: 'bar' }">
-        <button @click="foo = 'baz'">Change Foo</button>
-        <span x-text="foo"></span>
-    </div>`
-
-    let dom = createElement(template)
-
-    document.body.appendChild(dom)
-
-    window.Alpine = Alpine
-    window.Alpine.start()
-
-    dom.querySelector('button').click()
-
-    expect(dom.querySelector('span').textContent).toEqual('baz')
-
-    morph(dom, template)
-
-    expect(dom.querySelector('span').textContent).toEqual('baz')
-})
-
-test('morphing element with changed HTML AND Alpine scope', () => {
-    let template = `<div x-data="{ foo: 'bar' }">
-        <button @click="foo = 'baz'">Change Foo</button>
-        <span x-text="foo"></span>
-    </div>`
-
-    let dom = createElement(template)
-
-    document.body.appendChild(dom)
-
-    window.Alpine = Alpine
-    window.Alpine.start()
-
-    dom.querySelector('button').click()
-
-    expect(dom.querySelector('span').textContent).toEqual('baz')
-    expect(dom.querySelector('button').textContent).toEqual('Change Foo')
-
-    morph(dom, template.replace('Change Foo', 'Changed Foo'))
-
-    expect(dom.querySelector('span').textContent).toEqual('baz')
-    expect(dom.querySelector('button').textContent).toEqual('Changed Foo')
-})
-
-test('morphing an element with multiple nested Alpine components preserves scope', () => {
-    let template = `<div x-data="{ foo: 'bar' }">
-        <button @click="foo = 'baz'">Change Foo</button>
-        <span x-text="foo"></span>
-
-        <div x-data="{ bob: 'lob' }">
-            <a href="#" @click.prevent="bob = 'law'">Change Bob</a>
-            <h1 x-text="bob"></h1>
-        </div>
-    </div>`
-
-    let dom = createElement(template)
-
-    document.body.appendChild(dom)
-
-    window.Alpine = Alpine
-    window.Alpine.start()
-
-    dom.querySelector('button').click()
-    dom.querySelector('a').click()
-
-    expect(dom.querySelector('span').textContent).toEqual('baz')
-    expect(dom.querySelector('h1').textContent).toEqual('law')
-
-    morph(dom, template)
-
-    expect(dom.querySelector('span').textContent).toEqual('baz')
-    expect(dom.querySelector('h1').textContent).toEqual('law')
-})
-
-test('morphing an alpine component with static javascript re-evaluates', () => {
-    window.count = 1
-
-    let template = `<div x-data>
-        <span x-text="window.count"></span>
-    </div>`
-
-    let dom = createElement(template)
-
-    document.body.appendChild(dom)
-
-    window.Alpine = Alpine
-    window.Alpine.start()
-
-    expect(dom.querySelector('span').textContent).toEqual('1')
-
-    window.count++
-
-    morph(dom, template)
-
-    expect(dom.querySelector('span').textContent).toEqual('2')
-})