diff --git a/.changeset/cool-rabbits-tickle.md b/.changeset/cool-rabbits-tickle.md new file mode 100644 index 0000000000..1d684821ac --- /dev/null +++ b/.changeset/cool-rabbits-tickle.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve bind:this support for each blocks 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 e17d22c726..af73c0f137 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 @@ -972,7 +972,11 @@ function serialize_inline_component(node, component_name, context) { const assignment = b.assignment('=', bind_this, b.id('$$value')); const bind_this_id = /** @type {import('estree').Expression} */ ( // if expression is not an identifier, we know it can't be a signal - bind_this.type === 'Identifier' ? bind_this : undefined + bind_this.type === 'Identifier' + ? bind_this + : bind_this.type === 'MemberExpression' && bind_this.object.type === 'Identifier' + ? bind_this.object + : undefined ); fn = (node_id) => b.call( @@ -2742,7 +2746,11 @@ export const template_visitors = { setter, /** @type {import('estree').Expression} */ ( // if expression is not an identifier, we know it can't be a signal - expression.type === 'Identifier' ? expression : undefined + expression.type === 'Identifier' + ? expression + : expression.type === 'MemberExpression' && expression.object.type === 'Identifier' + ? expression.object + : undefined ) ); break; diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 51807c0143..9cc6b8225d 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -69,7 +69,7 @@ import { } from './utils.js'; import { is_promise } from '../common.js'; import { bind_transition, trigger_transitions } from './transitions.js'; -import { proxy } from './proxy.js'; +import { STATE_SYMBOL, proxy } from './proxy.js'; /** @type {Set} */ const all_registerd_events = new Set(); @@ -1295,6 +1295,13 @@ export function bind_prop(props, prop, value) { } } +/** + * @param {unknown} value + */ +function is_state_object(value) { + return value != null && typeof value === 'object' && STATE_SYMBOL in value; +} + /** * @param {Element} element_or_component * @param {(value: unknown) => void} update @@ -1302,9 +1309,15 @@ export function bind_prop(props, prop, value) { * @returns {void} */ export function bind_this(element_or_component, update, binding) { - untrack(() => { - update(element_or_component); - render_effect(() => () => { + render_effect(() => { + // If we are reading from a proxied state binding, then we don't need to untrack + // the update function as it will be fine-grain. + if (is_state_object(binding) || (is_signal(binding) && is_state_object(binding.v))) { + update(element_or_component); + } else { + untrack(() => update(element_or_component)); + } + return () => { // Defer to the next tick so that all updates can be reconciled first. // This solves the case where one variable is shared across multiple this-bindings. render_effect(() => { @@ -1314,7 +1327,7 @@ export function bind_this(element_or_component, update, binding) { } }); }); - }); + }; }); } diff --git a/packages/svelte/tests/runtime-runes/samples/state-each-bind-this/Paragraph.svelte b/packages/svelte/tests/runtime-runes/samples/state-each-bind-this/Paragraph.svelte new file mode 100644 index 0000000000..461fae954b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-each-bind-this/Paragraph.svelte @@ -0,0 +1,13 @@ + + +

+ {text} +

diff --git a/packages/svelte/tests/runtime-runes/samples/state-each-bind-this/_config.js b/packages/svelte/tests/runtime-runes/samples/state-each-bind-this/_config.js new file mode 100644 index 0000000000..88fefd5717 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-each-bind-this/_config.js @@ -0,0 +1,35 @@ +import { flushSync } from '../../../../src/main/main-client'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [btn, btn2, btn3] = target.querySelectorAll('button'); + + flushSync(() => { + btn?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

b1

b2

` + ); + + flushSync(() => { + btn2?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

b2

` + ); + + flushSync(() => { + btn3?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

b2

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/state-each-bind-this/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-each-bind-this/main.svelte new file mode 100644 index 0000000000..121afe01c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-each-bind-this/main.svelte @@ -0,0 +1,21 @@ + + +{#each store as text, i (text.id)} +
+ + + +
+{/each}