Skip to content

Commit df10204

Browse files
authored
fix: improve bind:this support for each blocks (#10510)
1 parent 1700e47 commit df10204

File tree

6 files changed

+102
-7
lines changed

6 files changed

+102
-7
lines changed

.changeset/cool-rabbits-tickle.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: improve bind:this support for each blocks

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -972,7 +972,11 @@ function serialize_inline_component(node, component_name, context) {
972972
const assignment = b.assignment('=', bind_this, b.id('$$value'));
973973
const bind_this_id = /** @type {import('estree').Expression} */ (
974974
// if expression is not an identifier, we know it can't be a signal
975-
bind_this.type === 'Identifier' ? bind_this : undefined
975+
bind_this.type === 'Identifier'
976+
? bind_this
977+
: bind_this.type === 'MemberExpression' && bind_this.object.type === 'Identifier'
978+
? bind_this.object
979+
: undefined
976980
);
977981
fn = (node_id) =>
978982
b.call(
@@ -2742,7 +2746,11 @@ export const template_visitors = {
27422746
setter,
27432747
/** @type {import('estree').Expression} */ (
27442748
// if expression is not an identifier, we know it can't be a signal
2745-
expression.type === 'Identifier' ? expression : undefined
2749+
expression.type === 'Identifier'
2750+
? expression
2751+
: expression.type === 'MemberExpression' && expression.object.type === 'Identifier'
2752+
? expression.object
2753+
: undefined
27462754
)
27472755
);
27482756
break;

packages/svelte/src/internal/client/render.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import {
6969
} from './utils.js';
7070
import { is_promise } from '../common.js';
7171
import { bind_transition, trigger_transitions } from './transitions.js';
72-
import { proxy } from './proxy.js';
72+
import { STATE_SYMBOL, proxy } from './proxy.js';
7373

7474
/** @type {Set<string>} */
7575
const all_registerd_events = new Set();
@@ -1295,16 +1295,29 @@ export function bind_prop(props, prop, value) {
12951295
}
12961296
}
12971297

1298+
/**
1299+
* @param {unknown} value
1300+
*/
1301+
function is_state_object(value) {
1302+
return value != null && typeof value === 'object' && STATE_SYMBOL in value;
1303+
}
1304+
12981305
/**
12991306
* @param {Element} element_or_component
13001307
* @param {(value: unknown) => void} update
13011308
* @param {import('./types.js').MaybeSignal} binding
13021309
* @returns {void}
13031310
*/
13041311
export function bind_this(element_or_component, update, binding) {
1305-
untrack(() => {
1306-
update(element_or_component);
1307-
render_effect(() => () => {
1312+
render_effect(() => {
1313+
// If we are reading from a proxied state binding, then we don't need to untrack
1314+
// the update function as it will be fine-grain.
1315+
if (is_state_object(binding) || (is_signal(binding) && is_state_object(binding.v))) {
1316+
update(element_or_component);
1317+
} else {
1318+
untrack(() => update(element_or_component));
1319+
}
1320+
return () => {
13081321
// Defer to the next tick so that all updates can be reconciled first.
13091322
// This solves the case where one variable is shared across multiple this-bindings.
13101323
render_effect(() => {
@@ -1314,7 +1327,7 @@ export function bind_this(element_or_component, update, binding) {
13141327
}
13151328
});
13161329
});
1317-
});
1330+
};
13181331
});
13191332
}
13201333

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
const { text } = $props();
3+
4+
let boundParagraph = $state();
5+
6+
export function changeBackgroundToRed() {
7+
boundParagraph.style.backgroundColor = 'red';
8+
}
9+
</script>
10+
11+
<p bind:this={boundParagraph}>
12+
{text}
13+
</p>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { flushSync } from '../../../../src/main/main-client';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [btn, btn2, btn3] = target.querySelectorAll('button');
7+
8+
flushSync(() => {
9+
btn?.click();
10+
});
11+
12+
assert.htmlEqual(
13+
target.innerHTML,
14+
`<div><p style="background-color: red;">b1</p><button>change</button><button>delete</button></div><div><p>b2</p><button>change</button><button>delete</button></div>`
15+
);
16+
17+
flushSync(() => {
18+
btn2?.click();
19+
});
20+
21+
assert.htmlEqual(
22+
target.innerHTML,
23+
`<div><p>b2</p><button>change</button><button>delete</button></div>`
24+
);
25+
26+
flushSync(() => {
27+
btn3?.click();
28+
});
29+
30+
assert.htmlEqual(
31+
target.innerHTML,
32+
`<div><p style="background-color: red;">b2</p><button>change</button><button>delete</button></div>`
33+
);
34+
}
35+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script>
2+
import Paragraph from './Paragraph.svelte';
3+
let boundParagraphs = $state([]);
4+
5+
let store = $state([
6+
{ id: 1, text: 'b1' },
7+
{ id: 2, text: 'b2' }
8+
]);
9+
</script>
10+
11+
{#each store as text, i (text.id)}
12+
<div>
13+
<Paragraph bind:this={boundParagraphs[i]} text={text.text}></Paragraph>
14+
<button onclick={() => boundParagraphs[i].changeBackgroundToRed()}>
15+
change
16+
</button>
17+
<button onclick={() => store.splice(store.indexOf(text), 1)}>
18+
delete
19+
</button>
20+
</div>
21+
{/each}

0 commit comments

Comments
 (0)