diff --git a/.changeset/witty-seas-learn.md b/.changeset/witty-seas-learn.md new file mode 100644 index 000000000000..aa94c7c35f36 --- /dev/null +++ b/.changeset/witty-seas-learn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure guards (eg. if, each, key) run before their contents diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 102d0670b664..17a8756f2b8a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -626,8 +626,28 @@ function flush_queued_effects(effects) { // TODO this feels incorrect! it gets the tests passing old_values.clear(); - for (const e of eager_block_effects) { - update_effect(e); + /** + * @type {Array<{ effect: Effect; depth: number }>} sorted + */ + const sorted = Array(eager_block_effects.length); + for (let j = 0; j < eager_block_effects.length; j++) { + var depth = 0; + var parent = eager_block_effects[j].parent; + + while (parent !== null) { + depth++; + parent = parent.parent; + } + + sorted[j] = { effect: eager_block_effects[j], depth }; + } + + // Sort by depth + sorted.sort((a, b) => a.depth - b.depth); + + // Update effects in reverse order to ensure outer guards are processed before their contents + for (const e of sorted) { + update_effect(e.effect); } eager_block_effects = []; diff --git a/packages/svelte/tests/runtime-runes/samples/guard-else-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/guard-else-effect/_config.js new file mode 100644 index 000000000000..0b564d1f651d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/guard-else-effect/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + mode: ['client'], + async test({ target, assert }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + + assert.notInclude(target.textContent?.trim(), 'hit: true'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/guard-else-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/guard-else-effect/main.svelte new file mode 100644 index 000000000000..c752507c90c5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/guard-else-effect/main.svelte @@ -0,0 +1,32 @@ + + + + + +hit: {hit} + +{#if v === "one"} +
if1 matched!
+{:else if v === "two"} +
if2 matched!
+{:else} +
nothing matched {hitElse()}
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/guard-if-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/guard-if-nested/_config.js new file mode 100644 index 000000000000..881c1545ee9f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/guard-if-nested/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + mode: ['client'], + async test({ target, assert }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + + assert.equal(target.textContent?.trim(), 'Trigger'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/guard-if-nested/main.svelte b/packages/svelte/tests/runtime-runes/samples/guard-if-nested/main.svelte new file mode 100644 index 000000000000..c0528680c029 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/guard-if-nested/main.svelte @@ -0,0 +1,19 @@ + + + {#if centerRow?.nested} + {#if centerRow?.nested?.optional != undefined && centerRow.nested.optional > 0} + op: {centerRow.nested.optional}
+ {:else} + req: {centerRow.nested.required}
+ {/if} + {/if} + +