diff --git a/.changeset/rude-frogs-train.md b/.changeset/rude-frogs-train.md new file mode 100644 index 0000000000..06da5dcc1e --- /dev/null +++ b/.changeset/rude-frogs-train.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: track the user's getter of `bind:this` diff --git a/documentation/docs/03-template-syntax/12-bind.md b/documentation/docs/03-template-syntax/12-bind.md index c21ed35919..be84969b87 100644 --- a/documentation/docs/03-template-syntax/12-bind.md +++ b/documentation/docs/03-template-syntax/12-bind.md @@ -364,6 +364,8 @@ Components also support `bind:this`, allowing you to interact with component ins ``` +> [!NOTE] In case of using [the function bindings](#Function-bindings), the getter is required to ensure that the correct value is nullified on component or element destruction. + ## bind:_property_ for components ```svelte diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 7d64d60bca..731569aaae 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -192,17 +192,18 @@ function build_assignment(operator, left, right, context) { path.at(-1) === 'Component' || path.at(-1) === 'SvelteComponent' || (path.at(-1) === 'ArrowFunctionExpression' && - path.at(-2) === 'SequenceExpression' && - (path.at(-3) === 'Component' || - path.at(-3) === 'SvelteComponent' || - path.at(-3) === 'BindDirective')) + (path.at(-2) === 'BindDirective' || + (path.at(-2) === 'Component' && path.at(-3) === 'Fragment') || + (path.at(-2) === 'SequenceExpression' && + (path.at(-3) === 'Component' || + path.at(-3) === 'SvelteComponent' || + path.at(-3) === 'BindDirective')))) ) { should_transform = false; } if (left.type === 'MemberExpression' && should_transform) { const callee = callees[operator]; - return /** @type {Expression} */ ( context.visit( b.call( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index ba140a153e..a42063b2e2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -209,10 +209,8 @@ export function parse_directive_name(name) { * @param {import('zimmerframe').Context} context */ export function build_bind_this(expression, value, { state, visit }) { - if (expression.type === 'SequenceExpression') { - const [get, set] = /** @type {SequenceExpression} */ (visit(expression)).expressions; - return b.call('$.bind_this', value, set, get); - } + const [getter, setter] = + expression.type === 'SequenceExpression' ? expression.expressions : [null, null]; /** @type {Identifier[]} */ const ids = []; @@ -229,7 +227,7 @@ export function build_bind_this(expression, value, { state, visit }) { // Note that we only do this for each context variables, the consequence is that the value might be stale in // some scenarios where the value is a member expression with changing computed parts or using a combination of multiple // variables, but that was the same case in Svelte 4, too. Once legacy mode is gone completely, we can revisit this. - walk(expression, null, { + walk(getter ?? expression, null, { Identifier(node, { path }) { if (seen.includes(node.name)) return; seen.push(node.name); @@ -260,9 +258,17 @@ export function build_bind_this(expression, value, { state, visit }) { const child_state = { ...state, transform }; - const get = /** @type {Expression} */ (visit(expression, child_state)); - const set = /** @type {Expression} */ ( - visit(b.assignment('=', expression, b.id('$$value')), child_state) + let get = /** @type {Expression} */ (visit(getter ?? expression, child_state)); + let set = /** @type {Expression} */ ( + visit( + setter ?? + b.assignment( + '=', + /** @type {Identifier | MemberExpression} */ (expression), + b.id('$$value') + ), + child_state + ) ); // If we're mutating a property, then it might already be non-existent. @@ -275,13 +281,25 @@ export function build_bind_this(expression, value, { state, visit }) { node = node.object; } - return b.call( - '$.bind_this', - value, - b.arrow([b.id('$$value'), ...ids], set), - b.arrow([...ids], get), - values.length > 0 && b.thunk(b.array(values)) - ); + get = + get.type === 'ArrowFunctionExpression' + ? b.arrow([...ids], get.body) + : get.type === 'FunctionExpression' + ? b.function(null, [...ids], get.body) + : getter + ? get + : b.arrow([...ids], get); + + set = + set.type === 'ArrowFunctionExpression' + ? b.arrow([set.params[0] ?? b.id('_'), ...ids], set.body) + : set.type === 'FunctionExpression' + ? b.function(null, [set.params[0] ?? b.id('_'), ...ids], set.body) + : setter + ? set + : b.arrow([b.id('$$value'), ...ids], set); + + return b.call('$.bind_this', value, set, get, values.length > 0 && b.thunk(b.array(values))); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/_config.js new file mode 100644 index 0000000000..9c8b4fc6c0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [btn] = target.querySelectorAll('button'); + + flushSync(() => { + btn.click(); + }); + assert.htmlEqual( + target.innerHTML, + '
51423
51423' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/main.svelte new file mode 100644 index 0000000000..60444e8978 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/main.svelte @@ -0,0 +1,13 @@ + + +
+{#each arr as item, i (item)} + elements[i], (v) => elements[i] = v }>{item} +{/each} +
+{#each elements as elem} + {elem.textContent} +{/each} \ No newline at end of file