Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
Component nested in if_block will fail to unmount if it has an element using an outro nested in a truthy if_block
This is a concise report for the bug encountered in #4620, #4064, #3685, #3410 and #3202.

[Absolute minimum REPL]( https://svelte.dev/repl/1f6f0ae29a6747368a2eb25ea2741703?version=3.20.1)

1) New Component
    * Declares a variable and a way to change it
    * Has an if_block whose condition is that variable
    * That if_block has an element using the outro directive
    * The variable happens to be truthy
2) Put Component inside an if_block that turns truthy
   a) i[f_block depends on a variable](https://svelte.dev/repl/022934a5b647432cb4a442eced244914?version=3.20.1)
        ✔ Variable was changed from outside the Component
        💥Variable was changed directly from inside the Component through a binding
    b) [if_block depends on a store](https://svelte.dev/repl/e409d8cf82794982a3a1e19c991f7689?version=3.20.1)
        ✔ Component does not access that specific store using the $store syntax
        💥Component accesses that store using $store syntax at least once


Discussion:

Resolving patch:
diff --git a/src/compiler/compile/render_dom/wrappers/IfBlock.ts b/src/compiler/compile/render_dom/wrappers/IfBlock.ts
--- a/src/compiler/compile/render_dom/wrappers/IfBlock.ts
+++ b/src/compiler/compile/render_dom/wrappers/IfBlock.ts
@@ -520,28 +520,22 @@ export default class IfBlockWrapper extends Wrapper {
 		if (branch.dependencies.length > 0) {
 			const update_mount_node = this.get_update_mount_node(anchor);
 
-			const enter = dynamic
-				? b`
-					if (${name}) {
-						${name}.p(#ctx, #dirty);
-						${has_transitions && b`@transition_in(${name}, 1);`}
-					} else {
-						${name} = ${branch.block.name}(#ctx);
-						${name}.c();
-						${has_transitions && b`@transition_in(${name}, 1);`}
-						${name}.m(${update_mount_node}, ${anchor});
-					}
-				`
-				: b`
-					if (!${name}) {
-						${name} = ${branch.block.name}(#ctx);
-						${name}.c();
-						${has_transitions && b`@transition_in(${name}, 1);`}
-						${name}.m(${update_mount_node}, ${anchor});
-					} else {
-						${has_transitions && b`@transition_in(${name}, 1);`}
+			const enter = b`
+				if (${name}) {
+					${dynamic && b`${name}.p(#ctx, #dirty);`}
+					${
+						has_transitions &&
+						b`if (${block.renderer.dirty(branch.dependencies)}) {
+							@transition_in(${name}, 1);
+						}`
 					}
-				`;
+				} else {
+					${name} = ${branch.block.name}(#ctx);
+					${name}.c();
+					${has_transitions && b`@transition_in(${name}, 1);`}
+					${name}.m(${update_mount_node}, ${anchor});
+				}
+			`;
 
 			if (branch.snippet) {
 				block.chunks.update.push(b`if (${block.renderer.dirty(branch.dependencies)}) ${branch.condition} = ${branch.snippet}`);


Test patch:
diff --git a/test/js/samples/if-block-simple/expected.js b/test/js/samples/if-block-simple/expected.js
--- a/test/js/samples/if-block-simple/expected.js
+++ b/test/js/samples/if-block-simple/expected.js
@@ -42,12 +42,12 @@ function create_fragment(ctx) {
 		},
 		p(ctx, [dirty]) {
 			if (/*foo*/ ctx[0]) {
-				if (!if_block) {
+				if (if_block) {
+					
+				} else {
 					if_block = create_if_block(ctx);
 					if_block.c();
 					if_block.m(if_block_anchor.parentNode, if_block_anchor);
-				} else {
-					
 				}
 			} else if (if_block) {
 				if_block.d(1);
diff --git a/test/js/samples/transition-local/expected.js b/test/js/samples/transition-local/expected.js
--- a/test/js/samples/transition-local/expected.js
+++ b/test/js/samples/transition-local/expected.js
@@ -28,13 +28,15 @@ function create_if_block(ctx) {
 		},
 		p(ctx, dirty) {
 			if (/*y*/ ctx[1]) {
-				if (!if_block) {
+				if (if_block) {
+					if (dirty & /*y*/ 2) {
+						transition_in(if_block, 1);
+					}
+				} else {
 					if_block = create_if_block_1(ctx);
 					if_block.c();
 					transition_in(if_block, 1);
 					if_block.m(if_block_anchor.parentNode, if_block_anchor);
-				} else {
-					transition_in(if_block, 1);
 				}
 			} else if (if_block) {
 				if_block.d(1);
diff --git a/test/js/samples/transition-repeated-outro/expected.js b/test/js/samples/transition-repeated-outro/expected.js
--- a/test/js/samples/transition-repeated-outro/expected.js
+++ b/test/js/samples/transition-repeated-outro/expected.js
@@ -63,13 +63,15 @@ function create_fragment(ctx) {
 		},
 		p(ctx, [dirty]) {
 			if (/*num*/ ctx[0] < 5) {
-				if (!if_block) {
+				if (if_block) {
+					if (dirty & /*num*/ 1) {
+						transition_in(if_block, 1);
+					}
+				} else {
 					if_block = create_if_block(ctx);
 					if_block.c();
 					transition_in(if_block, 1);
 					if_block.m(if_block_anchor.parentNode, if_block_anchor);
-				} else {
-					transition_in(if_block, 1);
 				}
 			} else if (if_block) {
 				group_outros();
diff --git a/test/js/samples/use-elements-as-anchors/expected.js b/test/js/samples/use-elements-as-anchors/expected.js
--- a/test/js/samples/use-elements-as-anchors/expected.js
+++ b/test/js/samples/use-elements-as-anchors/expected.js
@@ -157,12 +157,12 @@ function create_fragment(ctx) {
 		},
 		p(ctx, [dirty]) {
 			if (/*a*/ ctx[0]) {
-				if (!if_block0) {
+				if (if_block0) {
+					
+				} else {
 					if_block0 = create_if_block_4(ctx);
 					if_block0.c();
 					if_block0.m(div, t0);
-				} else {
-					
 				}
 			} else if (if_block0) {
 				if_block0.d(1);
@@ -170,12 +170,12 @@ function create_fragment(ctx) {
 			}
 
 			if (/*b*/ ctx[1]) {
-				if (!if_block1) {
+				if (if_block1) {
+					
+				} else {
 					if_block1 = create_if_block_3(ctx);
 					if_block1.c();
 					if_block1.m(div, t3);
-				} else {
-					
 				}
 			} else if (if_block1) {
 				if_block1.d(1);
@@ -183,12 +183,12 @@ function create_fragment(ctx) {
 			}
 
 			if (/*c*/ ctx[2]) {
-				if (!if_block2) {
+				if (if_block2) {
+					
+				} else {
 					if_block2 = create_if_block_2(ctx);
 					if_block2.c();
 					if_block2.m(div, t4);
-				} else {
-					
 				}
 			} else if (if_block2) {
 				if_block2.d(1);
@@ -196,12 +196,12 @@ function create_fragment(ctx) {
 			}
 
 			if (/*d*/ ctx[3]) {
-				if (!if_block3) {
+				if (if_block3) {
+					
+				} else {
 					if_block3 = create_if_block_1(ctx);
 					if_block3.c();
 					if_block3.m(div, null);
-				} else {
-					
 				}
 			} else if (if_block3) {
 				if_block3.d(1);
@@ -209,12 +209,12 @@ function create_fragment(ctx) {
 			}
 
 			if (/*e*/ ctx[4]) {
-				if (!if_block4) {
+				if (if_block4) {
+					
+				} else {
 					if_block4 = create_if_block(ctx);
 					if_block4.c();
 					if_block4.m(if_block4_anchor.parentNode, if_block4_anchor);
-				} else {
-					
 				}
 			} else if (if_block4) {
 				if_block4.d(1);
diff --git a/test/runtime/samples/transition-js-if-outro-unrelated-component-binding-update/Component.svelte b/test/runtime/samples/transition-js-if-outro-unrelated-component-binding-update/Component.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-if-outro-unrelated-component-binding-update/Component.svelte
@@ -0,0 +1,18 @@
+<script>
+	export let condition;
+	function foo(node, params) {
+		return {
+			duration: 100,
+			tick: t => {
+				node.foo = t;
+			}
+		};
+	}
+	let bool = true;
+</script>
+
+<button on:click={() => (condition = false)} />
+<button on:click={() => (bool = !bool)} />
+{#if bool}
+	<div out:foo />
+{/if}
diff --git a/test/runtime/samples/transition-js-if-outro-unrelated-component-binding-update/_config.js b/test/runtime/samples/transition-js-if-outro-unrelated-component-binding-update/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-if-outro-unrelated-component-binding-update/_config.js
@@ -0,0 +1,9 @@
+export default {
+	async test({ assert, target, window, raf }) {
+		const button = target.querySelector("button");
+		const event = new window.MouseEvent("click");
+		await button.dispatchEvent(event);
+		raf.tick(500);
+		assert.htmlEqual(target.innerHTML, "");
+	},
+};
diff --git a/test/runtime/samples/transition-js-if-outro-unrelated-component-binding-update/main.svelte b/test/runtime/samples/transition-js-if-outro-unrelated-component-binding-update/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-if-outro-unrelated-component-binding-update/main.svelte
@@ -0,0 +1,8 @@
+<script>
+	import Component from "./Component.svelte";
+	let condition = true;
+</script>
+
+{#if condition}
+	<Component bind:condition />
+{/if}
diff --git a/test/runtime/samples/transition-js-if-outro-unrelated-component-store-update/Component.svelte b/test/runtime/samples/transition-js-if-outro-unrelated-component-store-update/Component.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-if-outro-unrelated-component-store-update/Component.svelte
@@ -0,0 +1,18 @@
+<script>
+	export let condition;
+	function foo(node, params) {
+		return {
+			duration: 100,
+			tick: t => {
+				node.foo = t;
+			}
+		};
+	}
+	$condition;
+	let bool = true;
+</script>
+
+<button on:click={() => (bool = !bool)} />
+{#if bool}
+	<div out:foo />
+{/if}
diff --git a/test/runtime/samples/transition-js-if-outro-unrelated-component-store-update/_config.js b/test/runtime/samples/transition-js-if-outro-unrelated-component-store-update/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-if-outro-unrelated-component-store-update/_config.js
@@ -0,0 +1,7 @@
+export default {
+	async test({ assert, target, component, raf }) {
+		await component.condition.set(false);
+		raf.tick(500);
+		assert.htmlEqual(target.innerHTML, "");
+	},
+};
diff --git a/test/runtime/samples/transition-js-if-outro-unrelated-component-store-update/main.svelte b/test/runtime/samples/transition-js-if-outro-unrelated-component-store-update/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-if-outro-unrelated-component-store-update/main.svelte
@@ -0,0 +1,10 @@
+<script>
+	import { writable } from "svelte/store";
+	import Component from "./Component.svelte";
+	export let condition = writable(true);
+</script>
+
+{#if $condition}
+	<button on:click={() => ($condition = false)} id="1" />
+	<Component {condition} />
+{/if}