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')) + } +) +