Browse Source

Fix x-anchor being used with morphdom

Caleb Porzio 1 year ago
parent
commit
69b2fbfd6f

+ 2 - 1
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, cloneNode, skipDuringClone, onlyDuringClone } from './clone'
+import { clone, cloneNode, skipDuringClone, onlyDuringClone, interceptClone } from './clone'
 import { interceptor } from './interceptor'
 import { getBinding as bound, extractProp } from './utils/bind'
 import { debounce } from './utils/debounce'
@@ -39,6 +39,7 @@ let Alpine = {
     onlyDuringClone,
     addRootSelector,
     addInitSelector,
+    interceptClone,
     addScopeToNode,
     deferMutations,
     mapAttributes,

+ 8 - 23
packages/alpinejs/src/clone.js

@@ -12,18 +12,15 @@ export function onlyDuringClone(callback) {
     return (...args) => isCloning && callback(...args)
 }
 
+let interceptors = []
+
+export function interceptClone(callback) {
+    interceptors.push(callback)
+}
+
 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)
-    }
+    interceptors.forEach(i => i(from, to))
 
     isCloning = true
 
@@ -41,7 +38,7 @@ export function cloneNode(from, to)
     isCloning = false
 }
 
-let isCloningLegacy = false
+export let isCloningLegacy = false
 
 /** deprecated */
 export function clone(oldEl, newEl) {
@@ -90,15 +87,3 @@ 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')
-}
-

+ 25 - 1
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 { shouldSkipRegisteringDataDuringClone } from '../clone'
+import { interceptClone, isCloning, isCloningLegacy } from '../clone'
 import { addScopeToNode } from '../scope'
 import { injectMagics, magic } from '../magics'
 import { reactive } from '../reactivity'
@@ -41,3 +41,27 @@ directive('data', ((el, { expression }, { cleanup }) => {
         undo()
     })
 }))
+
+interceptClone((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)
+    }
+})
+
+// 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.
+function shouldSkipRegisteringDataDuringClone(el) {
+    if (! isCloning) return false
+    if (isCloningLegacy) return true
+
+    return el.hasAttribute('data-has-alpine-state')
+}

+ 39 - 19
packages/anchor/src/index.js

@@ -7,47 +7,53 @@ export default function (Alpine) {
         return el._x_anchor
     })
 
-    Alpine.directive('anchor', (el, { expression, modifiers, value }, { cleanup, evaluate }) => {
+    Alpine.interceptClone((from, to) => {
+        if (from && from._x_anchor && ! to._x_anchor) {
+            to._x_anchor = from._x_anchor
+        }
+    })
+
+    Alpine.directive('anchor', Alpine.skipDuringClone((el, { expression, modifiers, value }, { cleanup, evaluate }) => {
+        let { placement, offsetValue, unstyled } = getOptions(modifiers)
+
         el._x_anchor = Alpine.reactive({ x: 0, y: 0 })
 
         let reference = evaluate(expression)
 
         if (! reference) throw 'Alpine: no element provided to x-anchor...'
 
-        let positions = ['top', 'top-start', 'top-end', 'right', 'right-start', 'right-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end']
-        let placement = positions.find(i => modifiers.includes(i))
-
-        let offsetValue = 0
-
-        let unstyled = modifiers.includes('no-style')
-
-        if (modifiers.includes('offset')) {
-            let idx = modifiers.findIndex(i => i === 'offset')
-
-            offsetValue = modifiers[idx + 1] !== undefined ? Number(modifiers[idx + 1]) : offsetValue
-        }
-
-        let release = autoUpdate(reference, el, () => {
+        let compute = () => {
             let previousValue
 
             computePosition(reference, el, {
                 placement,
                 middleware: [flip(), shift({padding: 5}), offset(offsetValue)],
             }).then(({ x, y }) => {
+                unstyled || setStyles(el, x, y)
+
                 // Only trigger Alpine reactivity when the value actually changes...
                 if (JSON.stringify({ x, y }) !== previousValue) {
-                    unstyled || setStyles(el, x, y)
-
                     el._x_anchor.x = x
                     el._x_anchor.y = y
                 }
 
                 previousValue = JSON.stringify({ x, y })
             })
-        })
+        }
+
+        let release = autoUpdate(reference, el, () => compute())
 
         cleanup(() => release())
-    })
+    },
+
+    // When cloning (or "morphing"), we will graft the style and position data from the live tree...
+    (el, { expression, modifiers, value }, { cleanup, evaluate }) => {
+        let { placement, offsetValue, unstyled } = getOptions(modifiers)
+
+        if (el._x_anchor) {
+            unstyled || setStyles(el, el._x_anchor.x, el._x_anchor.y)
+        }
+    }))
 }
 
 function setStyles(el, x, y) {
@@ -55,3 +61,17 @@ function setStyles(el, x, y) {
         left: x+'px', top: y+'px', position: 'absolute',
     })
 }
+
+function getOptions(modifiers) {
+    let positions = ['top', 'top-start', 'top-end', 'right', 'right-start', 'right-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end']
+    let placement = positions.find(i => modifiers.includes(i))
+    let offsetValue = 0
+    if (modifiers.includes('offset')) {
+        let idx = modifiers.findIndex(i => i === 'offset')
+
+        offsetValue = modifiers[idx + 1] !== undefined ? Number(modifiers[idx + 1]) : offsetValue
+    }
+    let unstyled = modifiers.includes('no-style')
+
+    return { placement, offsetValue, unstyled }
+}