diff --git a/.changeset/soft-clocks-remember.md b/.changeset/soft-clocks-remember.md new file mode 100644 index 0000000000..69e8aca06e --- /dev/null +++ b/.changeset/soft-clocks-remember.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve consistency issues around binding invalidation diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 6579d17c01..6e3e570454 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -245,7 +245,7 @@ function setup_select_synchronization(value_binding, context) { context.state.init.push( b.stmt( b.call( - '$.pre_effect', + '$.invalidate_effect', b.thunk( b.block([ b.stmt( diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5d92c436e8..f075d2d5a3 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1273,6 +1273,17 @@ export function pre_effect(init) { ); } +/** + * This effect is used to ensure binding are kept in sync. We use a pre effect to ensure we run before the + * bindings which are in later effects. However, we don't use a pre_effect directly as we don't want to flush anything. + * + * @param {() => void | (() => void)} init + * @returns {import('./types.js').EffectSignal} + */ +export function invalidate_effect(init) { + return internal_create_effect(PRE_EFFECT, init, true, current_block, true); +} + /** * @param {() => void | (() => void)} init * @returns {import('./types.js').EffectSignal} diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 4d3205b824..a94a0de180 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -12,6 +12,7 @@ export { user_effect, render_effect, pre_effect, + invalidate_effect, flushSync, bubble_event, safe_equal, diff --git a/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js new file mode 100644 index 0000000000..b4ba660ad4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js @@ -0,0 +1,16 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual(target.innerHTML, 'a\n<select></select><button>change</button'); + + const [b1] = target.querySelectorAll('button'); + b1.click(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + 'a\n<select></select>b\n<select></select><button>change</button' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/invalidate-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/main.svelte new file mode 100644 index 0000000000..895a940451 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/main.svelte @@ -0,0 +1,13 @@ +<script> + let entries = $state([{selected: 'a'}]) +</script> + +{#each entries as entry} + {entry.selected} <select bind:value={entry.selected}></select> +{/each} + +<button + on:click={ + () => entries = [{selected: 'a'}, {selected: 'b'}] + } + >change</button>