Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fast-mails-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: don't update a focused input with values from its own past
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
//
Expand Down
8 changes: 8 additions & 0 deletions packages/svelte/src/internal/client/reactivity/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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, `<input type="number" /> <p>0</p>`);
assert.equal(input.value, '1');

input.focus();
input.value = '2';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();

assert.htmlEqual(target.innerHTML, `<input type="number" /> <p>0</p>`);
assert.equal(input.value, '2');

instance.shift();
await tick();
assert.htmlEqual(target.innerHTML, `<input type="number" /> <p>1</p>`);
assert.equal(input.value, '2');

instance.shift();
await tick();
assert.htmlEqual(target.innerHTML, `<input type="number" /> <p>2</p>`);
assert.equal(input.value, '2');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
let count = $state(0);

let deferreds = [];

export function shift() {
const d = deferreds.shift();
d.d.resolve(d.v);
}

function push(v) {
const d = Promise.withResolvers();
deferreds.push({ d, v });
return d.promise;
}
</script>

<svelte:boundary>
<input type="number" bind:value={count} />
<p>{await push(count)}</p>

{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
Loading