Skip to content

Skip x-modelable when cloning during a morph - #4929

Open
HawkForge wants to merge 1 commit into
alpinejs:mainfrom
HawkForge:fix/modelable-morph
Open

HawkForge wants to merge 1 commit into
alpinejs:mainfrom
HawkForge:fix/modelable-morph

Conversation

@HawkForge

Copy link
Copy Markdown

Fixes #4928.

Alpine.morph() clones every element it patches with Alpine.cloneNode(). On the clone, x-model already skips its listener, but x-modelable still queues its entangle microtask. That microtask runs after cloneNode() has returned, when effects are real again, so the clone ends up with a live entanglement over the live component's data (the clone shares its data stack).

This causes two problems:

  1. The entanglement's first run copies the outer value onto the inner one, so a change made to the inner value right after a morph is overwritten.
  2. The clone is thrown away without cleanup, so the entanglement is never released. Each morph adds one more, which adds up in Livewire, where components are morphed on every request.

The fix wraps the x-modelable directive in skipDuringClone(), the same as x-init, x-effect, x-on, x-for and x-if (as @tevinch also suggested in the issue). The clone is only used to seed the morph, so it doesn't need a binding of its own.

Tests: two new Cypress tests in morph.spec.js. One checks that a change made right after a morph is kept; the other checks that changing the value after five morphs reads the outer model the same number of times as before morphing. Both fail without the change.

Alpine.morph() clones every element it patches. On the clone, x-modelable
still queued its entangle microtask, which ran after cloneNode() had
returned and so registered a real effect against the live component's
data. That effect copied the outer value back over any inner change made
in the meantime, and since the clone is thrown away without cleanup, one
entanglement leaked per morph.

Wrap the directive in skipDuringClone() like x-init, x-effect and x-on.

Fixes alpinejs#4928
@tevinch

tevinch commented Oct 14, 2026

Copy link
Copy Markdown

Thanks for adding the regression tests. I checked d2a24b6 with the standalone pagination example using Playwright and Chrome 155.0.8059.41. All 12 checks passed; npm 3.17.4 still reproduced the three known failures.

The pagination case completed 11 real attribute-observer updates and retained every selected ID. Unchecking and rechecking a row, disabling automatic selection, keeping the notes and focus, and removing and recreating the component also worked. The nested-array case kept two model reads after 0, 1, 5, 10 and 20 morphs.

The source files were checked against the base and PR Git blob hashes before compilation. These checks cover standalone Alpine and Morph; the original Livewire application remains untested here.

@HawkForge

Copy link
Copy Markdown
Author

Thanks for testing it so thoroughly, especially the morph-count and component re-creation cases.

This branch has not been deployed

No deployments
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.

2 participants