Fix: x-model.blur cleanup crash on DOM removal

Discussion #4738 · Regression from PR #4729 · Base: 1735d0bf

Before Fix

upstream/main @ 1735d0bf

Alpine built from upstream main — contains the PR #4729 regression. Removing an x-model.blur input via direct DOM removal (as Livewire morphing does) crashes cleanup.

Waiting…

After Fix

1735d0bf + fix

Same commit, one-line fix: let form = el.form captured at registration time. Cleanup uses the stable reference.

Waiting…

Why x-if alone doesn't crash

Alpine's x-if runs attribute cleanup before detaching the element — so el.form is still valid. The crash only occurs when the element is removed outside Alpine's control — via Livewire morphing, Alpine.morph(), or direct el.remove(). In those cases, the MutationObserver fires cleanup after the element is already detached, and el.form returns null.

The demo above uses el.remove() to simulate this — click "Remove via DOM" in each panel.

// packages/alpinejs/src/directives/x-model.js line 86
if (el.form) {
+ let form = el.form
let syncCallback = () => syncValue({ target: el })
- if (!el.form._x_pendingModelUpdates) …
- cleanup(() => el.form._x_pendingModelUpdates.splice(…))
+ if (!form._x_pendingModelUpdates) …
+ cleanup(() => form._x_pendingModelUpdates.splice(…))
}
Both panels load Alpine built locally from the same base commit (1735d0bf). The only difference is the four-line fix shown above. Error logs below each panel show pageerror and console.warn events captured from the iframe.