diff --git a/packages/alpinejs/src/alpine.js b/packages/alpinejs/src/alpine.js index afd8e7c39..f0bb58ce8 100644 --- a/packages/alpinejs/src/alpine.js +++ b/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, diff --git a/packages/alpinejs/src/clone.js b/packages/alpinejs/src/clone.js index 0089bbd73..e93ff688c 100644 --- a/packages/alpinejs/src/clone.js +++ b/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') -} - diff --git a/packages/alpinejs/src/directives/x-data.js b/packages/alpinejs/src/directives/x-data.js index c5723b3b4..0bc0ed972 100644 --- a/packages/alpinejs/src/directives/x-data.js +++ b/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') +} diff --git a/packages/anchor/src/index.js b/packages/anchor/src/index.js index 46ca10fc8..cbfdcb9c5 100644 --- a/packages/anchor/src/index.js +++ b/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 } +}