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..5034cad98f 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -79,11 +79,18 @@ const all_registerd_events = new Set(); /** @type {Set<(events: Array) => void>} */ const root_event_handles = new Set(); +/** @type {any[] | null} */ +export let bind_this_context = null; + /** @returns {Text} */ export function empty() { return document.createTextNode(''); } +export function clear_bind_this_context() { + bind_this_context = null; +} + /** * @param {string} html * @param {boolean} is_fragment @@ -1225,14 +1232,26 @@ 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(() => { + // If a bind_this is shared across multple targets, then we + // need to ensure we correctly assign the value rather than + // only setting `null` for when they unmount. + if (is_signal(binding)) { + if (bind_this_context === null) { + bind_this_context = []; + } + bind_this_context.push(binding); + } update(element_or_component); render_effect(() => () => { untrack(() => { - update(null); + if (bind_this_context === null || !bind_this_context.includes(binding)) { + update(null); + } }); }); }); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index db5cf7be30..091d4dd041 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,7 +1,7 @@ import { DEV } from 'esm-env'; import { subscribe_to_store } from '../../store/utils.js'; import { EMPTY_FUNC, run_all } from '../common.js'; -import { unwrap } from './render.js'; +import { bind_this_context, clear_bind_this_context, unwrap } from './render.js'; import { is_array } from './utils.js'; export const SOURCE = 1; @@ -478,6 +478,9 @@ function flush_queued_effects(effects) { } } } + if (bind_this_context !== null) { + clear_bind_this_context(); + } effects.length = 0; } } 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 @@ + + +
+
+ + + +
+
+ {#if activeTab === 0} +

Tab 1

+ {/if} + {#if activeTab === 1} +

Tab 2

+ {/if} + {#if activeTab === 2} +

Tab 3

+ {/if} +
+ +