Skip to content

Commit 93012e1

Browse files
authored
fix: track the user's getter of bind:this (#16916)
1 parent 297afd0 commit 93012e1

File tree

6 files changed

+75
-20
lines changed

6 files changed

+75
-20
lines changed

.changeset/rude-frogs-train.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: track the user's getter of `bind:this`

documentation/docs/03-template-syntax/12-bind.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,8 @@ Components also support `bind:this`, allowing you to interact with component ins
364364
</script>
365365
```
366366

367+
> [!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.
368+
367369
## bind:_property_ for components
368370

369371
```svelte

packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,17 +192,18 @@ function build_assignment(operator, left, right, context) {
192192
path.at(-1) === 'Component' ||
193193
path.at(-1) === 'SvelteComponent' ||
194194
(path.at(-1) === 'ArrowFunctionExpression' &&
195-
path.at(-2) === 'SequenceExpression' &&
196-
(path.at(-3) === 'Component' ||
197-
path.at(-3) === 'SvelteComponent' ||
198-
path.at(-3) === 'BindDirective'))
195+
(path.at(-2) === 'BindDirective' ||
196+
(path.at(-2) === 'Component' && path.at(-3) === 'Fragment') ||
197+
(path.at(-2) === 'SequenceExpression' &&
198+
(path.at(-3) === 'Component' ||
199+
path.at(-3) === 'SvelteComponent' ||
200+
path.at(-3) === 'BindDirective'))))
199201
) {
200202
should_transform = false;
201203
}
202204

203205
if (left.type === 'MemberExpression' && should_transform) {
204206
const callee = callees[operator];
205-
206207
return /** @type {Expression} */ (
207208
context.visit(
208209
b.call(

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,8 @@ export function parse_directive_name(name) {
209209
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} context
210210
*/
211211
export function build_bind_this(expression, value, { state, visit }) {
212-
if (expression.type === 'SequenceExpression') {
213-
const [get, set] = /** @type {SequenceExpression} */ (visit(expression)).expressions;
214-
return b.call('$.bind_this', value, set, get);
215-
}
212+
const [getter, setter] =
213+
expression.type === 'SequenceExpression' ? expression.expressions : [null, null];
216214

217215
/** @type {Identifier[]} */
218216
const ids = [];
@@ -229,7 +227,7 @@ export function build_bind_this(expression, value, { state, visit }) {
229227
// Note that we only do this for each context variables, the consequence is that the value might be stale in
230228
// some scenarios where the value is a member expression with changing computed parts or using a combination of multiple
231229
// variables, but that was the same case in Svelte 4, too. Once legacy mode is gone completely, we can revisit this.
232-
walk(expression, null, {
230+
walk(getter ?? expression, null, {
233231
Identifier(node, { path }) {
234232
if (seen.includes(node.name)) return;
235233
seen.push(node.name);
@@ -260,9 +258,17 @@ export function build_bind_this(expression, value, { state, visit }) {
260258

261259
const child_state = { ...state, transform };
262260

263-
const get = /** @type {Expression} */ (visit(expression, child_state));
264-
const set = /** @type {Expression} */ (
265-
visit(b.assignment('=', expression, b.id('$$value')), child_state)
261+
let get = /** @type {Expression} */ (visit(getter ?? expression, child_state));
262+
let set = /** @type {Expression} */ (
263+
visit(
264+
setter ??
265+
b.assignment(
266+
'=',
267+
/** @type {Identifier | MemberExpression} */ (expression),
268+
b.id('$$value')
269+
),
270+
child_state
271+
)
266272
);
267273

268274
// 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 }) {
275281
node = node.object;
276282
}
277283

278-
return b.call(
279-
'$.bind_this',
280-
value,
281-
b.arrow([b.id('$$value'), ...ids], set),
282-
b.arrow([...ids], get),
283-
values.length > 0 && b.thunk(b.array(values))
284-
);
284+
get =
285+
get.type === 'ArrowFunctionExpression'
286+
? b.arrow([...ids], get.body)
287+
: get.type === 'FunctionExpression'
288+
? b.function(null, [...ids], get.body)
289+
: getter
290+
? get
291+
: b.arrow([...ids], get);
292+
293+
set =
294+
set.type === 'ArrowFunctionExpression'
295+
? b.arrow([set.params[0] ?? b.id('_'), ...ids], set.body)
296+
: set.type === 'FunctionExpression'
297+
? b.function(null, [set.params[0] ?? b.id('_'), ...ids], set.body)
298+
: setter
299+
? set
300+
: b.arrow([b.id('$$value'), ...ids], set);
301+
302+
return b.call('$.bind_this', value, set, get, values.length > 0 && b.thunk(b.array(values)));
285303
}
286304

287305
/**
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [btn] = target.querySelectorAll('button');
7+
8+
flushSync(() => {
9+
btn.click();
10+
});
11+
assert.htmlEqual(
12+
target.innerHTML,
13+
'<button>Shuffle</button> <br> <b>5</b><b>1</b><b>4</b><b>2</b><b>3</b> <br> 51423'
14+
);
15+
}
16+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let arr = $state([1, 2, 3, 4, 5]);
3+
let elements = $state([]);
4+
</script>
5+
6+
<button onclick={() => arr = [5, 1, 4, 2, 3]}>Shuffle</button><br>
7+
{#each arr as item, i (item)}
8+
<b bind:this={() => elements[i], (v) => elements[i] = v }>{item}</b>
9+
{/each}
10+
<br>
11+
{#each elements as elem}
12+
{elem.textContent}
13+
{/each}

0 commit comments

Comments
 (0)