diff --git a/.changeset/dirty-tips-add.md b/.changeset/dirty-tips-add.md new file mode 100644 index 0000000000..305dbd1b16 --- /dev/null +++ b/.changeset/dirty-tips-add.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correct bind this multiple bindings 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 3108216242..718d43235a 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 @@ -891,6 +891,7 @@ function serialize_inline_component(node, component_name, context) { if (bind_this !== null) { const prev = fn; const assignment = b.assignment('=', bind_this, b.id('$$value')); + const bind_this_id = bind_this; fn = (node_id) => b.call( '$.bind_this', @@ -898,7 +899,8 @@ function serialize_inline_component(node, component_name, context) { b.arrow( [b.id('$$value')], serialize_set_binding(assignment, context, () => context.visit(assignment)) - ) + ), + bind_this_id ); } @@ -2620,7 +2622,7 @@ export const template_visitors = { } case 'this': - call_expr = b.call(`$.bind_this`, state.node, setter); + call_expr = b.call(`$.bind_this`, state.node, setter, node.expression); break; case 'textContent': diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index f32e83ed1d..a20d6126a3 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -2,7 +2,6 @@ import { DEV } from 'esm-env'; import { append_child, child, - child_frag, clone_node, create_element, init_operations, @@ -61,7 +60,8 @@ import { push, current_component_context, pop, - schedule_task + schedule_task, + managed_render_effect } from './runtime.js'; import { current_hydration_fragment, @@ -1225,14 +1225,21 @@ export function bind_prop(props, prop, value) { /** * @param {Element} element_or_component * @param {(value: unknown) => void} update + * @param {import('./types.js').MaybeSignal} binding * @returns {void} */ -export function bind_this(element_or_component, update) { +export function bind_this(element_or_component, update, binding) { untrack(() => { update(element_or_component); render_effect(() => () => { - untrack(() => { - update(null); + // Defer to the next tick so that all updates can be reconciled first. + // This solves the case where one variable is shared across multiple this-bindings. + render_effect(() => { + untrack(() => { + if (!is_signal(binding) || binding.v === element_or_component) { + update(null); + } + }); }); }); }); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/_config.js new file mode 100644 index 0000000000..07356ad6fd --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/_config.js @@ -0,0 +1,25 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, component, target }) { + const [b1, b2, b3] = target.querySelectorAll('button'); + const first_h1 = target.querySelector('h1'); + + assert.deepEqual(component.log, [undefined, first_h1]); + + flushSync(() => { + b3.click(); + }); + + const third_h1 = target.querySelector('h1'); + + assert.deepEqual(component.log, [undefined, first_h1, third_h1]); + + flushSync(() => { + b1.click(); + }); + + assert.deepEqual(component.log, [undefined, first_h1, third_h1, target.querySelector('h1')]); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/main.svelte b/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/main.svelte new file mode 100644 index 0000000000..bde2158c51 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/main.svelte @@ -0,0 +1,27 @@ + + +