Skip to content

Commit 69b2fbf

Browse files
committed
Fix x-anchor being used with morphdom
1 parent 6960327 commit 69b2fbf

File tree

4 files changed

+74
-44
lines changed

4 files changed

+74
-44
lines changed

packages/alpinejs/src/alpine.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { onElRemoved, onAttributeRemoved, onAttributesAdded, mutateDom, deferMut
55
import { mergeProxies, closestDataStack, addScopeToNode, scope as $data } from './scope'
66
import { setEvaluator, evaluate, evaluateLater, dontAutoEvaluateFunctions } from './evaluator'
77
import { transition } from './directives/x-transition'
8-
import { clone, cloneNode, skipDuringClone, onlyDuringClone } from './clone'
8+
import { clone, cloneNode, skipDuringClone, onlyDuringClone, interceptClone } from './clone'
99
import { interceptor } from './interceptor'
1010
import { getBinding as bound, extractProp } from './utils/bind'
1111
import { debounce } from './utils/debounce'
@@ -39,6 +39,7 @@ let Alpine = {
3939
onlyDuringClone,
4040
addRootSelector,
4141
addInitSelector,
42+
interceptClone,
4243
addScopeToNode,
4344
deferMutations,
4445
mapAttributes,

packages/alpinejs/src/clone.js

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,15 @@ export function onlyDuringClone(callback) {
1212
return (...args) => isCloning && callback(...args)
1313
}
1414

15+
let interceptors = []
16+
17+
export function interceptClone(callback) {
18+
interceptors.push(callback)
19+
}
20+
1521
export function cloneNode(from, to)
1622
{
17-
// Transfer over existing runtime Alpine state from
18-
// the existing dom tree over to the new one...
19-
if (from._x_dataStack) {
20-
to._x_dataStack = from._x_dataStack
21-
22-
// Set a flag to signify the new tree is using
23-
// pre-seeded state (used so x-data knows when
24-
// and when not to initialize state)...
25-
to.setAttribute('data-has-alpine-state', true)
26-
}
23+
interceptors.forEach(i => i(from, to))
2724

2825
isCloning = true
2926

@@ -41,7 +38,7 @@ export function cloneNode(from, to)
4138
isCloning = false
4239
}
4340

44-
let isCloningLegacy = false
41+
export let isCloningLegacy = false
4542

4643
/** deprecated */
4744
export function clone(oldEl, newEl) {
@@ -90,15 +87,3 @@ function dontRegisterReactiveSideEffects(callback) {
9087

9188
overrideEffect(cache)
9289
}
93-
94-
// If we are cloning a tree, we only want to evaluate x-data if another
95-
// x-data context DOESN'T exist on the component.
96-
// The reason a data context WOULD exist is that we graft root x-data state over
97-
// from the live tree before hydrating the clone tree.
98-
export function shouldSkipRegisteringDataDuringClone(el) {
99-
if (! isCloning) return false
100-
if (isCloningLegacy) return true
101-
102-
return el.hasAttribute('data-has-alpine-state')
103-
}
104-

packages/alpinejs/src/directives/x-data.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { directive, prefix } from '../directives'
22
import { initInterceptors } from '../interceptor'
33
import { injectDataProviders } from '../datas'
44
import { addRootSelector } from '../lifecycle'
5-
import { shouldSkipRegisteringDataDuringClone } from '../clone'
5+
import { interceptClone, isCloning, isCloningLegacy } from '../clone'
66
import { addScopeToNode } from '../scope'
77
import { injectMagics, magic } from '../magics'
88
import { reactive } from '../reactivity'
@@ -41,3 +41,27 @@ directive('data', ((el, { expression }, { cleanup }) => {
4141
undo()
4242
})
4343
}))
44+
45+
interceptClone((from, to) => {
46+
// Transfer over existing runtime Alpine state from
47+
// the existing dom tree over to the new one...
48+
if (from._x_dataStack) {
49+
to._x_dataStack = from._x_dataStack
50+
51+
// Set a flag to signify the new tree is using
52+
// pre-seeded state (used so x-data knows when
53+
// and when not to initialize state)...
54+
to.setAttribute('data-has-alpine-state', true)
55+
}
56+
})
57+
58+
// If we are cloning a tree, we only want to evaluate x-data if another
59+
// x-data context DOESN'T exist on the component.
60+
// The reason a data context WOULD exist is that we graft root x-data state over
61+
// from the live tree before hydrating the clone tree.
62+
function shouldSkipRegisteringDataDuringClone(el) {
63+
if (! isCloning) return false
64+
if (isCloningLegacy) return true
65+
66+
return el.hasAttribute('data-has-alpine-state')
67+
}

packages/anchor/src/index.js

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,71 @@ export default function (Alpine) {
77
return el._x_anchor
88
})
99

10-
Alpine.directive('anchor', (el, { expression, modifiers, value }, { cleanup, evaluate }) => {
10+
Alpine.interceptClone((from, to) => {
11+
if (from && from._x_anchor && ! to._x_anchor) {
12+
to._x_anchor = from._x_anchor
13+
}
14+
})
15+
16+
Alpine.directive('anchor', Alpine.skipDuringClone((el, { expression, modifiers, value }, { cleanup, evaluate }) => {
17+
let { placement, offsetValue, unstyled } = getOptions(modifiers)
18+
1119
el._x_anchor = Alpine.reactive({ x: 0, y: 0 })
1220

1321
let reference = evaluate(expression)
1422

1523
if (! reference) throw 'Alpine: no element provided to x-anchor...'
1624

17-
let positions = ['top', 'top-start', 'top-end', 'right', 'right-start', 'right-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end']
18-
let placement = positions.find(i => modifiers.includes(i))
19-
20-
let offsetValue = 0
21-
22-
let unstyled = modifiers.includes('no-style')
23-
24-
if (modifiers.includes('offset')) {
25-
let idx = modifiers.findIndex(i => i === 'offset')
26-
27-
offsetValue = modifiers[idx + 1] !== undefined ? Number(modifiers[idx + 1]) : offsetValue
28-
}
29-
30-
let release = autoUpdate(reference, el, () => {
25+
let compute = () => {
3126
let previousValue
3227

3328
computePosition(reference, el, {
3429
placement,
3530
middleware: [flip(), shift({padding: 5}), offset(offsetValue)],
3631
}).then(({ x, y }) => {
32+
unstyled || setStyles(el, x, y)
33+
3734
// Only trigger Alpine reactivity when the value actually changes...
3835
if (JSON.stringify({ x, y }) !== previousValue) {
39-
unstyled || setStyles(el, x, y)
40-
4136
el._x_anchor.x = x
4237
el._x_anchor.y = y
4338
}
4439

4540
previousValue = JSON.stringify({ x, y })
4641
})
47-
})
42+
}
43+
44+
let release = autoUpdate(reference, el, () => compute())
4845

4946
cleanup(() => release())
50-
})
47+
},
48+
49+
// When cloning (or "morphing"), we will graft the style and position data from the live tree...
50+
(el, { expression, modifiers, value }, { cleanup, evaluate }) => {
51+
let { placement, offsetValue, unstyled } = getOptions(modifiers)
52+
53+
if (el._x_anchor) {
54+
unstyled || setStyles(el, el._x_anchor.x, el._x_anchor.y)
55+
}
56+
}))
5157
}
5258

5359
function setStyles(el, x, y) {
5460
Object.assign(el.style, {
5561
left: x+'px', top: y+'px', position: 'absolute',
5662
})
5763
}
64+
65+
function getOptions(modifiers) {
66+
let positions = ['top', 'top-start', 'top-end', 'right', 'right-start', 'right-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end']
67+
let placement = positions.find(i => modifiers.includes(i))
68+
let offsetValue = 0
69+
if (modifiers.includes('offset')) {
70+
let idx = modifiers.findIndex(i => i === 'offset')
71+
72+
offsetValue = modifiers[idx + 1] !== undefined ? Number(modifiers[idx + 1]) : offsetValue
73+
}
74+
let unstyled = modifiers.includes('no-style')
75+
76+
return { placement, offsetValue, unstyled }
77+
}

0 commit comments

Comments
 (0)