From 5b729c83c1b0bb55f7281f9fb0e001ea4be4db6a Mon Sep 17 00:00:00 2001 From: Hariharan Srinivasan Date: Wed, 20 Aug 2025 00:20:18 +0100 Subject: [PATCH 01/17] fix : remove cursor manipulation for input bindings Old Fix: Restore input binding selection position (#14649) Current Fix: Remove unnecessary cursor manipulation as the presence of runes no longer requires special handling. --- .../client/dom/elements/bindings/input.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 67e6ff1dd2be..ccb7ed29e317 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -7,7 +7,6 @@ import { is } from '../../../proxy.js'; import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { untrack } from '../../../runtime.js'; -import { is_runes } from '../../../context.js'; import { current_batch, previous_batch } from '../../../reactivity/batch.js'; /** @@ -17,7 +16,6 @@ import { current_batch, previous_batch } from '../../../reactivity/batch.js'; * @returns {void} */ export function bind_value(input, get, set = get) { - var runes = is_runes(); var batches = new WeakSet(); @@ -36,21 +34,6 @@ export function bind_value(input, get, set = get) { batches.add(current_batch); } - // In runes mode, respect any validation in accessors (doesn't apply in legacy mode, - // because we use mutable state which ensures the render effect always runs) - if (runes && value !== (value = get())) { - var start = input.selectionStart; - var end = input.selectionEnd; - - // the value is coerced on assignment - input.value = value ?? ''; - - // Restore selection - if (end !== null) { - input.selectionStart = start; - input.selectionEnd = Math.min(end, input.value.length); - } - } }); if ( From 6ca8ef3f97941bb8d8e0675b36d0c14af452364d Mon Sep 17 00:00:00 2001 From: Hariharan Srinivasan Date: Wed, 20 Aug 2025 00:43:29 +0100 Subject: [PATCH 02/17] fix : add change set to my previous commit --- .changeset/olive-cameras-juggle.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/olive-cameras-juggle.md diff --git a/.changeset/olive-cameras-juggle.md b/.changeset/olive-cameras-juggle.md new file mode 100644 index 000000000000..ad4e6638edf0 --- /dev/null +++ b/.changeset/olive-cameras-juggle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix : removed unnecessary cursor manipulation which fixes inconsistent two way input bindings From 21e1bcd74188d34bf44d7ee0647d1af30bc59b69 Mon Sep 17 00:00:00 2001 From: Hariharan Srinivasan Date: Wed, 20 Aug 2025 09:25:13 +0100 Subject: [PATCH 03/17] Revert "fix : add change set to my previous commit" This reverts commit 6ca8ef3f97941bb8d8e0675b36d0c14af452364d. --- .changeset/olive-cameras-juggle.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/olive-cameras-juggle.md diff --git a/.changeset/olive-cameras-juggle.md b/.changeset/olive-cameras-juggle.md deleted file mode 100644 index ad4e6638edf0..000000000000 --- a/.changeset/olive-cameras-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix : removed unnecessary cursor manipulation which fixes inconsistent two way input bindings From 91094949a616898729b85a95b5e1a8880b450170 Mon Sep 17 00:00:00 2001 From: Hariharan Srinivasan Date: Wed, 20 Aug 2025 09:27:37 +0100 Subject: [PATCH 04/17] fix: revert previous changeset added new to fix lint errors --- .changeset/breezy-cows-rush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/breezy-cows-rush.md diff --git a/.changeset/breezy-cows-rush.md b/.changeset/breezy-cows-rush.md new file mode 100644 index 000000000000..7f127ff83977 --- /dev/null +++ b/.changeset/breezy-cows-rush.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: removed unnecessary cursor manipulation which fixes inconsistent two way input bindings From ec93e10a6db4a9207fb5e815b056a03bbb72d383 Mon Sep 17 00:00:00 2001 From: Hariharan Srinivasan Date: Wed, 20 Aug 2025 10:45:14 +0100 Subject: [PATCH 05/17] chore : resolve lint error to fix pipeline issue --- .../svelte/src/internal/client/dom/elements/bindings/input.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index ccb7ed29e317..89fa8871343f 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -16,7 +16,6 @@ import { current_batch, previous_batch } from '../../../reactivity/batch.js'; * @returns {void} */ export function bind_value(input, get, set = get) { - var batches = new WeakSet(); listen_to_event_and_reset_event(input, 'input', (is_reset) => { @@ -33,7 +32,6 @@ export function bind_value(input, get, set = get) { if (current_batch !== null) { batches.add(current_batch); } - }); if ( From d9cc1900ca46c5fa228b98bd92feedbd779aab2e Mon Sep 17 00:00:00 2001 From: Hariharan Srinivasan Date: Fri, 22 Aug 2025 00:02:32 +0100 Subject: [PATCH 06/17] Revert "fix: revert previous changeset added new to fix lint errors" This reverts commit 91094949a616898729b85a95b5e1a8880b450170. --- .changeset/breezy-cows-rush.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/breezy-cows-rush.md diff --git a/.changeset/breezy-cows-rush.md b/.changeset/breezy-cows-rush.md deleted file mode 100644 index 7f127ff83977..000000000000 --- a/.changeset/breezy-cows-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: removed unnecessary cursor manipulation which fixes inconsistent two way input bindings From 0e40449704a4fd469d75f100d3ce5c12cf40f9d2 Mon Sep 17 00:00:00 2001 From: Hariharan Srinivasan Date: Fri, 22 Aug 2025 00:37:03 +0100 Subject: [PATCH 07/17] fix: input binding to handle code in a synchronous manner Introduced Promise.resolve to ensure that the 'set' operation completes before the 'get' operation Minimizing update delays. --- .../client/dom/elements/bindings/input.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 89fa8871343f..4c8fe9cc3ee4 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -7,6 +7,7 @@ import { is } from '../../../proxy.js'; import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { untrack } from '../../../runtime.js'; +import { is_runes } from '../../../context.js'; import { current_batch, previous_batch } from '../../../reactivity/batch.js'; /** @@ -16,6 +17,8 @@ import { current_batch, previous_batch } from '../../../reactivity/batch.js'; * @returns {void} */ export function bind_value(input, get, set = get) { + var runes = is_runes(); + var batches = new WeakSet(); listen_to_event_and_reset_event(input, 'input', (is_reset) => { @@ -27,8 +30,26 @@ export function bind_value(input, get, set = get) { /** @type {any} */ var value = is_reset ? input.defaultValue : input.value; value = is_numberlike_input(input) ? to_number(value) : value; - set(value); + // handle both async and normal set callback + Promise.resolve(set(value)).then(() => { + // In runes mode, respect any validation in accessors (doesn't apply in legacy mode, + // because we use mutable state which ensures the render effect always runs) + if (runes && value !== (value = get())) { + var start = input.selectionStart; + var end = input.selectionEnd; + + // the value is coerced on assignment + input.value = value ?? ''; + + // Restore selection + if (end !== null) { + input.selectionStart = start; + input.selectionEnd = Math.min(end, input.value.length); + } + } + }); + // This ensures that the current batch is tracked for any changes related to the input event and is done at end if (current_batch !== null) { batches.add(current_batch); } From f8f2a3d3bae2d82b3f52cde146300df6a1b7e22a Mon Sep 17 00:00:00 2001 From: Hariharan Srinivasan Date: Fri, 22 Aug 2025 00:40:26 +0100 Subject: [PATCH 08/17] Fix: resolve cursor jumps and change sets --- .changeset/rare-cups-fold.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rare-cups-fold.md diff --git a/.changeset/rare-cups-fold.md b/.changeset/rare-cups-fold.md new file mode 100644 index 000000000000..8cd5995651c2 --- /dev/null +++ b/.changeset/rare-cups-fold.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: Introduced Promise.resolve to ensure that the 'set' operation completes before the 'get' operation Minimizing update delays. From 3fa5981ceaeb39a8f02f0f4bc16b819b44981fc9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Aug 2025 13:45:19 -0400 Subject: [PATCH 09/17] better fix --- .../client/dom/elements/bindings/input.js | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 4c8fe9cc3ee4..086e2d0d9f1f 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -30,26 +30,8 @@ export function bind_value(input, get, set = get) { /** @type {any} */ var value = is_reset ? input.defaultValue : input.value; value = is_numberlike_input(input) ? to_number(value) : value; + set(value); - // handle both async and normal set callback - Promise.resolve(set(value)).then(() => { - // In runes mode, respect any validation in accessors (doesn't apply in legacy mode, - // because we use mutable state which ensures the render effect always runs) - if (runes && value !== (value = get())) { - var start = input.selectionStart; - var end = input.selectionEnd; - - // the value is coerced on assignment - input.value = value ?? ''; - - // Restore selection - if (end !== null) { - input.selectionStart = start; - input.selectionEnd = Math.min(end, input.value.length); - } - } - }); - // This ensures that the current batch is tracked for any changes related to the input event and is done at end if (current_batch !== null) { batches.add(current_batch); } @@ -109,6 +91,22 @@ export function bind_value(input, get, set = get) { // @ts-expect-error the value is coerced on assignment input.value = value ?? ''; } + + // In runes mode, respect any validation in accessors (doesn't apply in legacy mode, + // because we use mutable state which ensures the render effect always runs) + if (runes && value !== input.value) { + var start = input.selectionStart; + var end = input.selectionEnd; + + // @ts-expect-error the value is coerced on assignment + input.value = value ?? ''; + + // Restore selection + if (end !== null) { + input.selectionStart = start; + input.selectionEnd = Math.min(end, input.value.length); + } + } }); } From 477fc615da1105b4277a0d5f92f7f9f700f207cb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Aug 2025 13:52:04 -0400 Subject: [PATCH 10/17] test --- .../samples/binding-update-in-each/_config.js | 28 +++++++++++++++++++ .../binding-update-in-each/main.svelte | 8 ++++++ 2 files changed, 36 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-update-in-each/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-update-in-each/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/_config.js new file mode 100644 index 000000000000..b6371ce11c96 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/_config.js @@ -0,0 +1,28 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + html: `

a`, + + async test({ assert, target }) { + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = 'ab'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + flushSync(); + + assert.htmlEqual(target.innerHTML, `

ab`); + assert.equal(input.value, 'ab'); + + input.focus(); + input.value = 'abc'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + flushSync(); + + assert.htmlEqual(target.innerHTML, `

abc`); + assert.equal(input.value, 'abc'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/main.svelte new file mode 100644 index 000000000000..7925195ee159 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/main.svelte @@ -0,0 +1,8 @@ + + +{#each array as obj} + obj.value, (value) => array = [{ value }]} /> +

{obj.value}

+{/each} From b8473bf8643eb31161c969ebb9d9358cfe5c1609 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Aug 2025 13:55:10 -0400 Subject: [PATCH 11/17] changeset --- .changeset/tasty-chicken-care.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tasty-chicken-care.md diff --git a/.changeset/tasty-chicken-care.md b/.changeset/tasty-chicken-care.md new file mode 100644 index 000000000000..ea579efe4c6e --- /dev/null +++ b/.changeset/tasty-chicken-care.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: wait until changes propagate before updating input selection state From fb50f9fd5ad8d26db8284785de2dcd8dfd7cb28e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Aug 2025 13:55:16 -0400 Subject: [PATCH 12/17] simplify --- .../src/internal/client/dom/elements/bindings/input.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 086e2d0d9f1f..1adbe5246405 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -88,13 +88,6 @@ export function bind_value(input, get, set = get) { // don't set the value of the input if it's the same to allow // minlength to work properly if (value !== input.value) { - // @ts-expect-error the value is coerced on assignment - input.value = value ?? ''; - } - - // In runes mode, respect any validation in accessors (doesn't apply in legacy mode, - // because we use mutable state which ensures the render effect always runs) - if (runes && value !== input.value) { var start = input.selectionStart; var end = input.selectionEnd; From 184b2fc1264562fb22a658dd3d9cc989f8aba9c0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Aug 2025 16:14:29 -0400 Subject: [PATCH 13/17] failing test --- .../binding-update-while-focused-3/_config.js | 28 +++++++++++++++++++ .../main.svelte | 6 ++++ 2 files changed, 34 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js new file mode 100644 index 000000000000..14d30395bd77 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + async test({ assert, target }) { + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = 'Ab'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + + await tick(); + + assert.equal(input.value, 'AB'); + assert.htmlEqual(target.innerHTML, `

AB

`); + + input.focus(); + input.value = 'ABc'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + + await tick(); + + assert.equal(input.value, 'ABC'); + assert.htmlEqual(target.innerHTML, `

ABC

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/main.svelte new file mode 100644 index 000000000000..b61bfe4e6714 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/main.svelte @@ -0,0 +1,6 @@ + + + text, (v) => text = v.toUpperCase()} /> +

{text}

From b8d2c91ad2624b20667bd8a75872e2480e54a442 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Aug 2025 16:20:02 -0400 Subject: [PATCH 14/17] gah we can't fix the input in an effect, need to do it here, but after a tick so that changes have been flushed through each blocks --- .../client/dom/elements/bindings/input.js | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 1adbe5246405..8ca28545f9c2 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -6,7 +6,7 @@ import * as e from '../../../errors.js'; import { is } from '../../../proxy.js'; import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; -import { untrack } from '../../../runtime.js'; +import { tick, untrack } from '../../../runtime.js'; import { is_runes } from '../../../context.js'; import { current_batch, previous_batch } from '../../../reactivity/batch.js'; @@ -21,7 +21,7 @@ export function bind_value(input, get, set = get) { var batches = new WeakSet(); - listen_to_event_and_reset_event(input, 'input', (is_reset) => { + listen_to_event_and_reset_event(input, 'input', async (is_reset) => { if (DEV && input.type === 'checkbox') { // TODO should this happen in prod too? e.bind_invalid_checkbox_value(); @@ -35,6 +35,24 @@ export function bind_value(input, get, set = get) { if (current_batch !== null) { batches.add(current_batch); } + + await tick(); + + // In runes mode, respect any validation in accessors (doesn't apply in legacy mode, + // because we use mutable state which ensures the render effect always runs) + if (runes && value !== (value = get())) { + var start = input.selectionStart; + var end = input.selectionEnd; + + // the value is coerced on assignment + input.value = value ?? ''; + + // Restore selection + if (end !== null) { + input.selectionStart = start; + input.selectionEnd = Math.min(end, input.value.length); + } + } }); if ( @@ -88,17 +106,8 @@ export function bind_value(input, get, set = get) { // don't set the value of the input if it's the same to allow // minlength to work properly if (value !== input.value) { - var start = input.selectionStart; - var end = input.selectionEnd; - // @ts-expect-error the value is coerced on assignment input.value = value ?? ''; - - // Restore selection - if (end !== null) { - input.selectionStart = start; - input.selectionEnd = Math.min(end, input.value.length); - } } }); } From ce7f4c41eb944243d3a5dc242231c88084404f1d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Aug 2025 16:22:39 -0400 Subject: [PATCH 15/17] add explanatory comment --- .../svelte/src/internal/client/dom/elements/bindings/input.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 8ca28545f9c2..0ae4556547fb 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -36,6 +36,9 @@ export function bind_value(input, get, set = get) { batches.add(current_batch); } + // Because `{#each ...}` blocks work by updating sources inside the flush, + // we need to wait a tick before checking to see if we should forcibly + // update the input and reset the selection state await tick(); // In runes mode, respect any validation in accessors (doesn't apply in legacy mode, From 8640ca73ddcb3a6f08c306e172e9b23da05135ee Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Aug 2025 16:43:09 -0400 Subject: [PATCH 16/17] fix test --- .../samples/binding-update-while-focused-3/_config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js index 14d30395bd77..0909dee7a8b9 100644 --- a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js @@ -11,6 +11,7 @@ export default test({ input.value = 'Ab'; input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); await tick(); assert.equal(input.value, 'AB'); @@ -20,6 +21,7 @@ export default test({ input.value = 'ABc'; input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); await tick(); assert.equal(input.value, 'ABC'); From 0357b5fa753cd31b760c031ffe4e55fc22d989a7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Aug 2025 16:57:58 -0400 Subject: [PATCH 17/17] this seems to work? --- .../src/internal/client/dom/elements/bindings/input.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 0ae4556547fb..815acde7c53b 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -17,8 +17,6 @@ import { current_batch, previous_batch } from '../../../reactivity/batch.js'; * @returns {void} */ export function bind_value(input, get, set = get) { - var runes = is_runes(); - var batches = new WeakSet(); listen_to_event_and_reset_event(input, 'input', async (is_reset) => { @@ -41,9 +39,8 @@ export function bind_value(input, get, set = get) { // update the input and reset the selection state await tick(); - // In runes mode, respect any validation in accessors (doesn't apply in legacy mode, - // because we use mutable state which ensures the render effect always runs) - if (runes && value !== (value = get())) { + // Respect any validation in accessors + if (value !== (value = get())) { var start = input.selectionStart; var end = input.selectionEnd;