fix: x-model.blur cleanup crash when element removed from DOM#4780
Open
manwithacat wants to merge 1 commit intoalpinejs:mainfrom
Open
fix: x-model.blur cleanup crash when element removed from DOM#4780manwithacat wants to merge 1 commit intoalpinejs:mainfrom
manwithacat wants to merge 1 commit intoalpinejs:mainfrom
Conversation
When an element with x-model.blur is removed from the DOM (e.g., via x-if or Livewire morphing), the cleanup callback reads el.form which returns null for detached elements. This causes a TypeError on null._x_pendingModelUpdates. Fix: capture `let form = el.form` at registration time so the cleanup closure uses a stable reference that remains valid after detachment. Adds three Cypress tests: - x-model.blur input removed via x-if (core crash case) - x-model.blur input without form removed via x-if - form submit after sibling x-model.blur input removed Ref: alpinejs#4738 Regression from: alpinejs#4729
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.
Problem
When an
<input x-model.blur>inside a<form>is removed from the DOM via Livewire morphing,Alpine.morph(), or direct DOM manipulation, the cleanup callback introduced in #4729 throws:This happens because
el.formreturnsnullfor elements that have been detached from the DOM, and the cleanup closure readsel.format teardown time rather than at registration time.Reported in Discussion #4738 — confirmed by multiple users in production with Livewire.
Why
x-ifalone doesn't crashAlpine's
x-ifruns attribute cleanup before detaching the element from the DOM, soel.formis still valid at that point. The crash only occurs when the element is removed outside Alpine's control — via Livewire morphing,Alpine.morph(), or directel.remove(). In those paths, the MutationObserver fires cleanup after the element is already detached, andel.formreturnsnull.Fix
Capture
let form = el.format registration time (line 87 ofx-model.js). All subsequent references — including the cleanup closure — use the capturedformvariable instead of re-reading the live DOM property.if (el.form) { + let form = el.form let syncCallback = () => syncValue({ target: el }) - if (!el.form._x_pendingModelUpdates) el.form._x_pendingModelUpdates = [] - el.form._x_pendingModelUpdates.push(syncCallback) - cleanup(() => el.form._x_pendingModelUpdates.splice(...)) + if (!form._x_pendingModelUpdates) form._x_pendingModelUpdates = [] + form._x_pendingModelUpdates.push(syncCallback) + cleanup(() => form._x_pendingModelUpdates.splice(...)) }Tests
Three new Cypress tests in
x-model.spec.js:x-model.blurinput inside a form, removed viax-if. Verifies cleanup completes and Alpine remains functional.x-model.blurinputs in a form, one removed, then form submitted. Verifies the form's_x_pendingModelUpdatesarray isn't corrupted.All 36 x-model tests pass (33 existing + 3 new).
A note on testing the crash path
The real crash requires the element to be detached from the DOM before cleanup runs — which is the Livewire/morph path, not the
x-ifpath. Cypress (and Playwright) test thex-ifpath, where Alpine runs cleanup before detachment, soel.formis still valid and the TypeError doesn't fire during the test.However, the tests are still valuable: they exercise the exact code path that was changed (the
x-model.blurcleanup with a form), and they verify the behavioral contract — that removing and re-addingx-model.blurinputs doesn't break Alpine's reactivity or corrupt_x_pendingModelUpdates. The fix itself is correct regardless of cleanup ordering: capturing the form reference at registration time is always safer than re-reading a live DOM property in a deferred callback.For a visual demonstration of the actual crash (using
el.remove()to simulate Livewire morph), see the interactive demo below.Interactive demo
Side-by-side comparison loading builds from the same base commit (
1735d0bf), one buggy, one fixed. Usesel.remove()to trigger the real crash path:https://manwithacat.github.io/alpine/
Scope
Only the
el.formreferences inside the.blurpending-update block (lines 86–93) are affected. The otherel.formusage at line 136 (reset listener) is safe becauseon()captureslistenerTargetinternally.