From 92c9c963eaeec8e9c870fcadd15b8d36147ecd95 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Jul 2025 09:42:20 -0400 Subject: [PATCH 1/3] fix: don't update a focused input with values from its own past --- .changeset/fast-mails-fail.md | 5 +++ .../client/dom/elements/bindings/input.js | 4 +- .../src/internal/client/reactivity/batch.js | 8 ++++ .../_config.js | 37 +++++++++++++++++++ .../main.svelte | 25 +++++++++++++ 5 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 .changeset/fast-mails-fail.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte diff --git a/.changeset/fast-mails-fail.md b/.changeset/fast-mails-fail.md new file mode 100644 index 000000000000..027cb01548c2 --- /dev/null +++ b/.changeset/fast-mails-fail.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't update a focused input with values from its own past 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 7c1fccea0fbc..6fbdca587976 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -8,7 +8,7 @@ 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 } from '../../../reactivity/batch.js'; +import { current_batch, previous_batch } from '../../../reactivity/batch.js'; /** * @param {HTMLInputElement} input @@ -76,7 +76,7 @@ export function bind_value(input, get, set = get) { var value = get(); - if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) { + if (input === document.activeElement && batches.has(/** @type {Batch} */ (previous_batch))) { // Never rewrite the contents of a focused input. We can get here if, for example, // an update is deferred because of async work depending on the input: // diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 89bad947c7fa..a7b848daf318 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -38,6 +38,9 @@ const batches = new Set(); /** @type {Batch | null} */ export let current_batch = null; +/** @type {Batch | null} */ +export let previous_batch = current_batch; + /** * When time travelling, we re-evaluate deriveds based on the temporary * values of their dependencies rather than their actual values, and cache @@ -72,7 +75,10 @@ let is_flushing = false; let is_flushing_sync = false; +let uid = 1; export class Batch { + id = uid++; + /** * The current values of any sources that are updated in this batch * They keys of this map are identical to `this.#previous` @@ -218,6 +224,7 @@ export class Batch { // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. + previous_batch = current_batch; current_batch = null; flush_queued_effects(render_effects); @@ -350,6 +357,7 @@ export class Batch { deactivate() { current_batch = null; + previous_batch = null; for (const update of effect_pending_updates) { effect_pending_updates.delete(update); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js new file mode 100644 index 000000000000..b0772ad3c071 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js @@ -0,0 +1,37 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, instance }) { + instance.shift(); + await tick(); + + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = '1'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.htmlEqual(target.innerHTML, `

0

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

0

`); + assert.equal(input.value, '2'); + + instance.shift(); + await tick(); + assert.htmlEqual(target.innerHTML, `

1

`); + assert.equal(input.value, '2'); + + instance.shift(); + await tick(); + assert.htmlEqual(target.innerHTML, `

2

`); + assert.equal(input.value, '2'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte new file mode 100644 index 000000000000..2fc898e6540d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte @@ -0,0 +1,25 @@ + + + + +

{await push(count)}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
From ab0b2591616ac0bce20ed8cf037ba77afc334fd9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Jul 2025 09:49:50 -0400 Subject: [PATCH 2/3] remove --- packages/svelte/src/internal/client/reactivity/batch.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index a7b848daf318..0bfca49e91e9 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -74,11 +74,7 @@ let last_scheduled_effect = null; let is_flushing = false; let is_flushing_sync = false; - -let uid = 1; export class Batch { - id = uid++; - /** * The current values of any sources that are updated in this batch * They keys of this map are identical to `this.#previous` From d5f0b6cf22372eb1bdc41d9c8cb380d1a7675853 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Jul 2025 10:01:31 -0400 Subject: [PATCH 3/3] fix --- .../src/internal/client/dom/elements/bindings/input.js | 9 +++++++-- .../svelte/src/internal/client/reactivity/batch.js | 10 ++++++++-- 2 files changed, 15 insertions(+), 4 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 6fbdca587976..7c73280dd664 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -76,13 +76,18 @@ export function bind_value(input, get, set = get) { var value = get(); - if (input === document.activeElement && batches.has(/** @type {Batch} */ (previous_batch))) { + if (input === document.activeElement) { + // we need both, because in non-async mode, render effects run before previous_batch is set + var batch = /** @type {Batch} */ (previous_batch ?? current_batch); + // Never rewrite the contents of a focused input. We can get here if, for example, // an update is deferred because of async work depending on the input: // // //

{await find(query)}

- return; + if (batches.has(batch)) { + return; + } } if (is_numberlike_input(input) && value === to_number(input.value)) { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 0bfca49e91e9..123bc95d163a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -38,8 +38,12 @@ const batches = new Set(); /** @type {Batch | null} */ export let current_batch = null; -/** @type {Batch | null} */ -export let previous_batch = current_batch; +/** + * This is needed to avoid overwriting inputs in non-async mode + * TODO 6.0 remove this, as non-async mode will go away + * @type {Batch | null} + */ +export let previous_batch = null; /** * When time travelling, we re-evaluate deriveds based on the temporary @@ -175,6 +179,8 @@ export class Batch { process(root_effects) { queued_root_effects = []; + previous_batch = null; + /** @type {Map | null} */ var current_values = null; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy