fix: track the user's getter of `bind:this`

fix-16547
7nik 1 day ago
parent 06bd6a8fbd
commit 3b6e40e9b1

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: track the user's getter of `bind:this`

@ -364,6 +364,8 @@ Components also support `bind:this`, allowing you to interact with component ins
</script> </script>
``` ```
> [!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 ## bind:_property_ for components
```svelte ```svelte

@ -192,17 +192,18 @@ function build_assignment(operator, left, right, context) {
path.at(-1) === 'Component' || path.at(-1) === 'Component' ||
path.at(-1) === 'SvelteComponent' || path.at(-1) === 'SvelteComponent' ||
(path.at(-1) === 'ArrowFunctionExpression' && (path.at(-1) === 'ArrowFunctionExpression' &&
path.at(-2) === 'SequenceExpression' && (path.at(-2) === 'BindDirective' ||
(path.at(-3) === 'Component' || (path.at(-2) === 'Component' && path.at(-3) === 'Fragment') ||
path.at(-3) === 'SvelteComponent' || (path.at(-2) === 'SequenceExpression' &&
path.at(-3) === 'BindDirective')) (path.at(-3) === 'Component' ||
path.at(-3) === 'SvelteComponent' ||
path.at(-3) === 'BindDirective'))))
) { ) {
should_transform = false; should_transform = false;
} }
if (left.type === 'MemberExpression' && should_transform) { if (left.type === 'MemberExpression' && should_transform) {
const callee = callees[operator]; const callee = callees[operator];
return /** @type {Expression} */ ( return /** @type {Expression} */ (
context.visit( context.visit(
b.call( b.call(

@ -209,10 +209,8 @@ export function parse_directive_name(name) {
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} context * @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} context
*/ */
export function build_bind_this(expression, value, { state, visit }) { export function build_bind_this(expression, value, { state, visit }) {
if (expression.type === 'SequenceExpression') { const [getter, setter] =
const [get, set] = /** @type {SequenceExpression} */ (visit(expression)).expressions; expression.type === 'SequenceExpression' ? expression.expressions : [null, null];
return b.call('$.bind_this', value, set, get);
}
/** @type {Identifier[]} */ /** @type {Identifier[]} */
const ids = []; 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 // 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 // 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. // 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 }) { Identifier(node, { path }) {
if (seen.includes(node.name)) return; if (seen.includes(node.name)) return;
seen.push(node.name); seen.push(node.name);
@ -260,9 +258,17 @@ export function build_bind_this(expression, value, { state, visit }) {
const child_state = { ...state, transform }; const child_state = { ...state, transform };
const get = /** @type {Expression} */ (visit(expression, child_state)); let get = /** @type {Expression} */ (visit(getter ?? expression, child_state));
const set = /** @type {Expression} */ ( let set = /** @type {Expression} */ (
visit(b.assignment('=', expression, b.id('$$value')), child_state) 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. // 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; node = node.object;
} }
return b.call( get =
'$.bind_this', get.type === 'ArrowFunctionExpression'
value, ? b.arrow([...ids], get.body)
b.arrow([b.id('$$value'), ...ids], set), : get.type === 'FunctionExpression'
b.arrow([...ids], get), ? b.function(null, [...ids], get.body)
values.length > 0 && b.thunk(b.array(values)) : 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)));
} }
/** /**

@ -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,
'<button>Shuffle</button> <br> <b>5</b><b>1</b><b>4</b><b>2</b><b>3</b> <br> 51423'
);
}
});

@ -0,0 +1,13 @@
<script>
let arr = $state([1, 2, 3, 4, 5]);
let elements = $state([]);
</script>
<button onclick={() => arr = [5, 1, 4, 2, 3]}>Shuffle</button><br>
{#each arr as item, i (item)}
<b bind:this={() => elements[i], (v) => elements[i] = v }>{item}</b>
{/each}
<br>
{#each elements as elem}
{elem.textContent}
{/each}
Loading…
Cancel
Save