Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
When state changes, nested components containing conditionals jump over other elements
Reproduced in 1.19.1.

When a template contains an `if`, and inside of that there is a nested component that contains another `if`, when the state changes the elements of the nested component will jump to the end of the containing element.

Words are difficult - reproduction at: https://svelte.technology/repl?version=1.19.1&gist=68736c878aaf7dfee6ab503df9259f46



Discussion:
Worth pointing out that this only occurs within the nested, truthy if-block. On the example's re-render, it self-heals to `Wat's up doooog ?`
It's still wrong for me on every re-render - it's only on the initial render that I see the nested component rendered in the right place.  No matter what I change the state to after that (toggling it between `no` and `yes` over and over again) it's always at the end of the containing element on every re-render.
Hmm... I consistently see the self-healing 🤔 

1. Click link --> blank page
2. Select `Nested.html` in file dropdown --> see text
3. The `setTimeout` fires & issue is reproduced
4. Select `App.html` in file dropdown --> see `Wat's up doooog ?`

But yeah, without step 4, manually editing the yes/no value does not self-heal.

So it seems to be narrowed to initial render + two `if` conditions.
Right, I believe switching templates causes it to re-instantiate the component, same as if you'd refreshed the page.  It just leaves the state (the JSON at the bottom right) the same.

Try updating the JSON manually between `yes` and `no` and it will re-render the state without re-instantiating the component, and you'll see that the issue remains.
This feels like a similar issue to #561. If the `{{#if}}` in the nested component is contained in another element, the issue doesn't present itself. I've been mainly playing around with this not in the REPL, because the REPL seems to be muddying the waters here. I'm going to try to put together a simple unit test that will fail.

Resolving patch:
diff --git a/src/generators/dom/visitors/IfBlock.js b/src/generators/dom/visitors/IfBlock.js
--- a/src/generators/dom/visitors/IfBlock.js
+++ b/src/generators/dom/visitors/IfBlock.js
@@ -52,13 +52,7 @@ export default function visitIfBlock ( generator, block, state, node ) {
 	const anchor = node.needsAnchor ? block.getUniqueName( `${name}_anchor` ) : ( node.next && node.next._state.name ) || 'null';
 	const params = block.params.join( ', ' );
 
-	if ( node.needsAnchor ) {
-		block.addElement( anchor, `${generator.helper( 'createComment' )}()`, state.parentNode, true );
-	} else if ( node.next ) {
-		node.next.usedAsAnchor = true;
-	}
-
-	const branches = getBranches( generator, block, state, node, generator.getUniqueName( `create_if_block` ) );
+	const branches = getBranches( generator, block, state, node );
 
 	const hasElse = isElseBranch( branches[ branches.length - 1 ] );
 	const if_name = hasElse ? '' : `if ( ${name} ) `;
@@ -78,6 +72,12 @@ export default function visitIfBlock ( generator, block, state, node ) {
 		simple( generator, block, state, node, branches[0], dynamic, vars );
 	}
 
+	if ( node.needsAnchor ) {
+		block.addElement( anchor, `${generator.helper( 'createComment' )}()`, state.parentNode, true );
+	} else if ( node.next ) {
+		node.next.usedAsAnchor = true;
+	}
+
 	block.builders.destroy.addLine(
 		`${if_name}${name}.destroy( ${state.parentNode ? 'false' : 'detach'} );`
 	);
@@ -88,11 +88,11 @@ function simple ( generator, block, state, node, branch, dynamic, { name, anchor
 		var ${name} = (${branch.condition}) && ${branch.block}( ${params}, ${block.component} );
 	` );
 
-	const isToplevel = !state.parentNode;
+	const isTopLevel = !state.parentNode;
 	const mountOrIntro = branch.hasIntroMethod ? 'intro' : 'mount';
 
-	if ( isToplevel ) {
-		block.builders.mount.addLine( `if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, null );` );
+	if ( isTopLevel ) {
+		block.builders.mount.addLine( `if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, anchor );` );
 	} else {
 		block.builders.create.addLine( `if ( ${name} ) ${name}.${mountOrIntro}( ${state.parentNode}, null );` );
 	}
@@ -169,11 +169,11 @@ function compound ( generator, block, state, node, branches, dynamic, { name, an
 		var ${name} = ${current_block_and}${current_block}( ${params}, ${block.component} );
 	` );
 
-	const isToplevel = !state.parentNode;
+	const isTopLevel = !state.parentNode;
 	const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
 
-	if ( isToplevel ) {
-		block.builders.mount.addLine( `${if_name}${name}.${mountOrIntro}( ${block.target}, null );` );
+	if ( isTopLevel ) {
+		block.builders.mount.addLine( `${if_name}${name}.${mountOrIntro}( ${block.target}, anchor );` );
 	} else {
 		block.builders.create.addLine( `${if_name}${name}.${mountOrIntro}( ${state.parentNode}, null );` );
 	}
@@ -244,13 +244,14 @@ function compoundWithOutros ( generator, block, state, node, branches, dynamic,
 		` );
 	}
 
-	const isToplevel = !state.parentNode;
+	const isTopLevel = !state.parentNode;
 	const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
-	const initialTarget = isToplevel ? block.target : state.parentNode;
 
-	( isToplevel ? block.builders.mount : block.builders.create ).addBlock(
-		`${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${initialTarget}, null );`
-	);
+	if ( isTopLevel ) {
+		block.builders.mount.addLine( `${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${block.target}, anchor );` );
+	} else {
+		block.builders.create.addLine( `${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${state.parentNode}, null );` );
+	}
 
 	const parentNode = state.parentNode || `${anchor}.parentNode`;
 


Test patch:
diff --git a/test/js/samples/if-block-no-update/expected.js b/test/js/samples/if-block-no-update/expected.js
--- a/test/js/samples/if-block-no-update/expected.js
+++ b/test/js/samples/if-block-no-update/expected.js
@@ -1,8 +1,6 @@
 import { appendNode, assign, createComment, createElement, createText, detachNode, dispatchObservers, insertNode, proto } from "svelte/shared.js";
 
 function create_main_fragment ( state, component ) {
-	var if_block_anchor = createComment();
-
 	function get_block ( state ) {
 		if ( state.foo ) return create_if_block;
 		return create_if_block_1;
@@ -11,10 +9,12 @@ function create_main_fragment ( state, component ) {
 	var current_block = get_block( state );
 	var if_block = current_block( state, component );
 
+	var if_block_anchor = createComment();
+
 	return {
 		mount: function ( target, anchor ) {
+			if_block.mount( target, anchor );
 			insertNode( if_block_anchor, target, anchor );
-			if_block.mount( target, null );
 		},
 
 		update: function ( changed, state ) {
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
@@ -1,14 +1,14 @@
 import { appendNode, assign, createComment, createElement, createText, detachNode, dispatchObservers, insertNode, proto } from "svelte/shared.js";
 
 function create_main_fragment ( state, component ) {
-	var if_block_anchor = createComment();
-
 	var if_block = (state.foo) && create_if_block( state, component );
 
+	var if_block_anchor = createComment();
+
 	return {
 		mount: function ( target, anchor ) {
+			if ( if_block ) if_block.mount( target, anchor );
 			insertNode( if_block_anchor, target, anchor );
-			if ( if_block ) if_block.mount( target, null );
 		},
 
 		update: function ( changed, state ) {
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
@@ -35,16 +35,17 @@ function create_main_fragment ( state, component ) {
 	var text_7 = createText( "\n\n\t" );
 	appendNode( text_7, div );
 	var text_8 = createText( "\n\n" );
-	var if_block_4_anchor = createComment();
 
 	var if_block_4 = (state.e) && create_if_block_4( state, component );
 
+	var if_block_4_anchor = createComment();
+
 	return {
 		mount: function ( target, anchor ) {
 			insertNode( div, target, anchor );
 			insertNode( text_8, target, anchor );
+			if ( if_block_4 ) if_block_4.mount( target, anchor );
 			insertNode( if_block_4_anchor, target, anchor );
-			if ( if_block_4 ) if_block_4.mount( target, null );
 		},
 
 		update: function ( changed, state ) {
diff --git a/test/runtime/samples/component-if-placement/Component.html b/test/runtime/samples/component-if-placement/Component.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-if-placement/Component.html
@@ -0,0 +1,3 @@
+{{#if true}}
+	<span>Component</span>
+{{/if}}
diff --git a/test/runtime/samples/component-if-placement/_config.js b/test/runtime/samples/component-if-placement/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-if-placement/_config.js
@@ -0,0 +1,20 @@
+export default {
+	data: {
+		flag: true
+	},
+
+	html: `
+		<span>Before</span>
+		<span>Component</span>
+		<span>After</span>
+	`,
+
+	test ( assert, component, target ) {
+		component.set( { flag: false } );
+		assert.htmlEqual( target.innerHTML, `
+			<span>Before</span>
+			<span>Component</span>
+			<span>After</span>
+		`);
+	}
+};
diff --git a/test/runtime/samples/component-if-placement/main.html b/test/runtime/samples/component-if-placement/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-if-placement/main.html
@@ -0,0 +1,14 @@
+<span>Before</span>
+{{#if flag}}
+	<Component/>
+{{else}}
+	<Component/>
+{{/if}}
+<span>After</span>
+
+<script>
+	import Component from './Component.html';
+	export default {
+		components: { Component }
+	};
+</script>