From 1ef9ac89b1cb86e3fcc68478e2546338444367dc Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Fri, 30 Jan 2026 14:19:47 +1000 Subject: [PATCH] fix(x-model): flush `x-model.blur` value before form submit handlers run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using `x-model.blur` on an input inside a form, submitting via Enter key does not sync the value before the submit handler executes. The browser fires events in order: keydown → submit → blur, so the blur listener never runs before the submit handler reads the value. Register pending model update callbacks on the form element and flush them before any submit handler runs via the `on()` utility. --- packages/alpinejs/src/directives/x-model.js | 13 +++++++++++++ packages/alpinejs/src/utils/on.js | 12 ++++++++++++ .../integration/directives/x-model.spec.js | 18 ++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/packages/alpinejs/src/directives/x-model.js b/packages/alpinejs/src/directives/x-model.js index 95508f079..ef3437530 100644 --- a/packages/alpinejs/src/directives/x-model.js +++ b/packages/alpinejs/src/directives/x-model.js @@ -77,6 +77,19 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => { if (hasBlurModifier) { listeners.push(on(el, 'blur', modifiers, syncValue)) + + // The browser fires "submit" before "blur", so if this input + // is inside a form, the model value would be stale when the + // submit handler runs. Register a pending update on the form + // so it can be flushed before submit handlers execute. + if (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(el.form._x_pendingModelUpdates.indexOf(syncCallback), 1)) + } } if (hasEnterModifier) { diff --git a/packages/alpinejs/src/utils/on.js b/packages/alpinejs/src/utils/on.js index 778467182..3a1eddb72 100644 --- a/packages/alpinejs/src/utils/on.js +++ b/packages/alpinejs/src/utils/on.js @@ -66,6 +66,18 @@ export default function on (el, event, modifiers, callback) { if (modifiers.includes('self')) handler = wrapHandler(handler, (next, e) => { e.target === el && next(e) }) + // Flush any pending model updates before submit handlers run + // (e.g. x-model.blur inputs that haven't synced yet). + if (event === 'submit') { + handler = wrapHandler(handler, (next, e) => { + if (e.target._x_pendingModelUpdates) { + e.target._x_pendingModelUpdates.forEach(fn => fn()) + } + + next(e) + }) + } + // Handle :keydown and :keyup listeners. // Handle :click and :auxclick listeners. if (isKeyEvent(event) || isClickEvent(event)) { diff --git a/tests/cypress/integration/directives/x-model.spec.js b/tests/cypress/integration/directives/x-model.spec.js index 4578cb5c8..fa956304e 100644 --- a/tests/cypress/integration/directives/x-model.spec.js +++ b/tests/cypress/integration/directives/x-model.spec.js @@ -642,3 +642,21 @@ test('x-model.enter.blur updates on enter OR blur (enter should work)', } ) +test('x-model.blur syncs value before form submit handler runs', + html` +
+
+ +
+ +
+ `, + ({ get }) => { + get('input').type('hello') + get('#captured').should(haveText('')) + // Submit the form without blurring the input first + get('form').then(([form]) => form.requestSubmit()) + get('#captured').should(haveText('hello')) + } +) +