Repository navigation
Don't re-apply x-cloak to rendered elements during morph - #4922
Merged
Merged
Conversation
Morph seeds each incoming element with Alpine.cloneNode() before patching attributes. x-cloak only removes itself in a microtask, so the incoming element still has x-cloak when attributes are patched, and it gets copied back onto the already-initialized live element. The element is then display: none until the microtask runs, so anything measuring it in that window (e.g. a MutationObserver callback) reads 0px. Strip x-cloak synchronously while cloning, like wire:cloak's interceptor already does. The cloned element is detached, so there's nothing to hide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Was the logic behind having it queue a microtask in the first place reevaluated to ensure it's still necessary? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Measuring an element from a
MutationObserverduring a morph (e.g. a chat measuring its message list after Livewire re-renders) reads 0px if that element, or one of its ancestors, usesx-cloak. Swappingx-cloakforwire:cloakmakes the measurements correct.The cause is that morph re-applies
x-cloakto elements that are already rendered and uncloaked:x-cloakon the element (the live DOM lost it after Alpine initialized).Alpine.cloneNode(from, to), which runs thex-cloakdirective on it.x-cloakonly removes itself in aqueueMicrotask, so the incoming element still hasx-cloakwhenpatchAttributes()runs, and morph copies it back onto the live element.display: noneuntil that microtask fires. Any MutationObserver callback that runs in that window sees the element, and everything inside it, at 0px.wire:cloakdoesn't hit this because Livewire strips it in anAlpine.interceptInit()callback, which runs synchronously while the incoming element is being cloned. By the time morph patches attributes,wire:cloakis already gone.The fix
While cloning, strip
x-cloakfrom the incoming element synchronously:The cloned element is detached server HTML that's never painted, so there's nothing to hide and no need for
mutateDom(). Morph then sees nox-cloakon the incoming element and leaves the live one alone. If the live element somehow still hasx-cloak, morph removes it, which is also howwire:cloakbehaves.Normal (non-morph) initialization is untouched:
x-cloakstill removes itself in a microtask.Measurements
Repro: two identical Livewire chats side by side, one using
x-cloak, one usingwire:cloak(on the list and on each message). Each has aMutationObserverthat recordsoffsetHeightof the list and its first message on every callback. Clicking "Send" three times gives:0px→162px122px→162px162px0px→210px170px→210px210px0px→258px218px→258px258px(The intermediate after-fix readings are the new message being inserted before Alpine initializes it. See the note below.)
Before: every morph briefly re-cloaks the whole
x-cloaklist, so the observer reads0pxfor the list and the first message.After: already-rendered elements are never re-cloaked, and measurements match
wire:cloak.Tests
Added
morphing does not re-apply x-cloak to an already initialized elementtomorph.spec.js. It morphs an initialized<p>against HTML that still hasx-cloakand records the element's height from aMutationObserver:expected [ 0, 19 ] to not include 0The full Cypress suite passes (629 passing, 0 failing).
Related, not fixed here
While digging I found a separate ordering issue that affects
x-cloakandwire:cloakequally. During a morph, anymutateDom()call triggersflushObserver(), which takes Alpine's pending mutation records and processes them in a laterqueueMicrotask. That microtask runs after the browser's MutationObserver notification. So nodes newly inserted by a morph (before its lastmutateDom()call) aren't initialized yet when other observers run, and a cloaked new node still measures 0px. Swapping the order of the two chats in the repro makes thewire:cloakchat's new message read 0px instead. That belongs inmutation.js, so I'm leaving it for a separate PR.🤖 Generated with Claude Code