Skip to content

Commit

Permalink
Fix x-anchor being used with morphdom
Browse files Browse the repository at this point in the history
  • Loading branch information
calebporzio committed Nov 4, 2023
1 parent 6960327 commit 69b2fbf
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 44 deletions.
3 changes: 2 additions & 1 deletion packages/alpinejs/src/alpine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -39,6 +39,7 @@ let Alpine = {
onlyDuringClone,
addRootSelector,
addInitSelector,
interceptClone,
addScopeToNode,
deferMutations,
mapAttributes,
Expand Down
31 changes: 8 additions & 23 deletions packages/alpinejs/src/clone.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -41,7 +38,7 @@ export function cloneNode(from, to)
isCloning = false
}

let isCloningLegacy = false
export let isCloningLegacy = false

/** deprecated */
export function clone(oldEl, newEl) {
Expand Down Expand Up @@ -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')
}

26 changes: 25 additions & 1 deletion packages/alpinejs/src/directives/x-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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')
}
58 changes: 39 additions & 19 deletions packages/anchor/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,71 @@ 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) {
Object.assign(el.style, {
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 }
}

0 comments on commit 69b2fbf

Please sign in to comment.