diff --git a/.changeset/weak-drinks-speak.md b/.changeset/weak-drinks-speak.md new file mode 100644 index 0000000000..dc3847b112 --- /dev/null +++ b/.changeset/weak-drinks-speak.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: ensure bind:this unmount behavior for members is conditional 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 46327006a2..8e675ce692 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 @@ -917,6 +917,15 @@ function serialize_bind_this(bind_this, context, node) { /** @type {import('estree').Expression[]} */ const args = [node, b.arrow([b.id('$$value'), ...ids], update), b.arrow([...ids], bind_this_id)]; + // If we're mutating a property, then it might already be non-existent. + // If we make all the object nodes optional, then it avoids any runtime exceptions. + /** @type {import('estree').Expression | import('estree').Super} */ + let bind_node = bind_this_id; + + while (bind_node?.type === 'MemberExpression') { + bind_node.optional = true; + bind_node = bind_node.object; + } if (each_ids.size) { args.push(b.thunk(b.array(Array.from(each_ids.values()).map((id) => id[1])))); } diff --git a/packages/svelte/tests/runtime-runes/samples/each-bind-this-member-2/Child.svelte b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member-2/Child.svelte new file mode 100644 index 0000000000..410b337a2a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member-2/Child.svelte @@ -0,0 +1,7 @@ + + +
+ {item.text} +
diff --git a/packages/svelte/tests/runtime-runes/samples/each-bind-this-member-2/_config.js b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member-2/_config.js new file mode 100644 index 0000000000..a43638dab0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member-2/_config.js @@ -0,0 +1,25 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, component }) { + const [b1, b2] = target.querySelectorAll('button'); + + flushSync(() => { + b1.click(); + b1.click(); + b1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
Item 1
Item 2
Item 3
` + ); + + flushSync(() => { + b2.click(); + }); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-bind-this-member-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member-2/main.svelte new file mode 100644 index 0000000000..cf2ddee635 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member-2/main.svelte @@ -0,0 +1,23 @@ + + + + + +{#each items as item, index (item.id)} + +{/each}