Skip to content

feat(x-anchor): allow dynamic reference to be used with x-anchor#4735

Open
maximbelyayev wants to merge 3 commits intoalpinejs:mainfrom
maximbelyayev:anchor-dynamic
Open

feat(x-anchor): allow dynamic reference to be used with x-anchor#4735
maximbelyayev wants to merge 3 commits intoalpinejs:mainfrom
maximbelyayev:anchor-dynamic

Conversation

@maximbelyayev
Copy link

@maximbelyayev maximbelyayev commented Feb 3, 2026

Addresses: #4079

Problem

Currently using x-anchor with a reference to an element does not allow for reactivity / changes to the reference. The element with x-anchor is permanently anchored to the initial reference throughout its lifecycle.

In the below example, the header element with id baz is anchored to the button with id foo on init. The button with id bar has a @click directive to change the reference variable to reference itself.

<div x-data="{ reference: null }" x-init="reference = document.getElementById('foo')">
    <button id="foo">toggle foo</button>
    <button id="bar" @click="reference = $el">toggle bar</button>
    <h1 id="baz" x-anchor="reference">contents</h1>
</div>

The expected behavior is that when clicking bar button, the header element re-anchors to bar. However, the actual behavior is that the header remains anchored to foo button.

The current behavior is detrimental in certain use-cases, such as where a dropdown menu has multiple detached triggers that can each open and anchor to itself the same content.

Ideally, x-anchor should react to changes to the reference, and immediately re-calculate the CSS positioning.

Investigation

The expression is evaluated once at mount and then passed to @floating-ui/dom autoUpdate:

Alpine.directive('anchor', Alpine.skipDuringClone((el, { expression, modifiers, value }, { cleanup, evaluate }) => {
        ...
        let reference = evaluate(expression)
        if (! reference) throw 'Alpine: no element provided to x-anchor...'
        let compute = () => {
            ...
        }
        let release = autoUpdate(reference, el, () => compute())
        cleanup(() => release())
    },

This means changes to the expression are not captured and re-evaluated to form an updated reference, which can be passed to @floating-ui/dom API.

Solution

Introduce the effect API to the directive and store the previous reference, such that if the x-anchor expression changes and does not evaluate to the previous reference, we release the autoUpdate attached to the previous reference and create a new one.

Alpine.directive('anchor', Alpine.skipDuringClone((el, { expression, modifiers, value }, { cleanup, evaluate }) => {
        ...
        let previousReference = null
        let release = null;  
        let effector = effect(() => {
            let reference = evaluate(expression)
            if (! reference) throw 'Alpine: no element provided to x-anchor...'
            if (previousReference !== reference) {
                if (release) release();
                previousReference = reference
                let compute = () => {
                   ...
                }
                release = autoUpdate(reference, el, () => compute())
            }
        })
        cleanup(() => {
            effector()
            if (release) release()
        })
    },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant