Repository: sveltejs/svelte (JavaScript)
Date: 2018-05-25 03:43:31+00:00
Task type: Bug Fix

Issue:
outroing is not defined with nestedTransitions: true
With the `nestedTransitions: true` option, it's possible to end up in a situation where one branch of an `if` block potentially has outros, but another doesn't, and Svelte checks for a non-existent `outroing` variable in the block that doesn't upon update.

```html
{#if y}
	<Foo/>
{:else}
	<span>{x}</span>
{/if}
```

Noticed a couple of other things:

* we don't really need to update the `if` block at all; perhaps that should be short-circuited anyway
* `transitionManager` gets added even though there's no transitions, because it's trying to group the outros (in case `<Foo>` has some). I wonder if we can separate `groupOutros` from `transitionManager` so that it doesn't get pulled in


Issue discussion:

Solution patch:
diff --git a/src/compile/dom/Block.ts b/src/compile/dom/Block.ts
--- a/src/compile/dom/Block.ts
+++ b/src/compile/dom/Block.ts
@@ -44,7 +44,9 @@ export default class Block {
 
 	maintainContext: boolean;
 	hasAnimation: boolean;
-	hasIntroMethod: boolean;
+	hasIntros: boolean;
+	hasOutros: boolean;
+	hasIntroMethod: boolean; // could have the method without the transition, due to siblings
 	hasOutroMethod: boolean;
 	outros: number;
 
@@ -127,11 +129,11 @@ export default class Block {
 	}
 
 	addIntro() {
-		this.hasIntroMethod = this.compiler.target.hasIntroTransitions = true;
+		this.hasIntros = this.hasIntroMethod = this.compiler.target.hasIntroTransitions = true;
 	}
 
 	addOutro() {
-		this.hasOutroMethod = this.compiler.target.hasOutroTransitions = true;
+		this.hasOutros = this.hasOutroMethod = this.compiler.target.hasOutroTransitions = true;
 		this.outros += 1;
 	}
 
diff --git a/src/compile/nodes/Attribute.ts b/src/compile/nodes/Attribute.ts
--- a/src/compile/nodes/Attribute.ts
+++ b/src/compile/nodes/Attribute.ts
@@ -232,7 +232,7 @@ export default class Attribute extends Node {
 			if (this.dependencies.size || isSelectValueAttribute) {
 				const dependencies = Array.from(this.dependencies);
 				const changedCheck = (
-					( block.hasOutroMethod ? `#outroing || ` : '' ) +
+					( block.hasOutros ? `#outroing || ` : '' ) +
 					dependencies.map(dependency => `changed.${dependency}`).join(' || ')
 				);
 
@@ -308,7 +308,7 @@ export default class Attribute extends Node {
 				if (propDependencies.size) {
 					const dependencies = Array.from(propDependencies);
 					const condition = (
-						(block.hasOutroMethod ? `#outroing || ` : '') +
+						(block.hasOutros ? `#outroing || ` : '') +
 						dependencies.map(dependency => `changed.${dependency}`).join(' || ')
 					);
 
diff --git a/src/compile/nodes/AwaitBlock.ts b/src/compile/nodes/AwaitBlock.ts
--- a/src/compile/nodes/AwaitBlock.ts
+++ b/src/compile/nodes/AwaitBlock.ts
@@ -61,8 +61,8 @@ export default class AwaitBlock extends Node {
 				block.addDependencies(child.block.dependencies);
 			}
 
-			if (child.block.hasIntroMethod) hasIntros = true;
-			if (child.block.hasOutroMethod) hasOutros = true;
+			if (child.block.hasIntros) hasIntros = true;
+			if (child.block.hasOutros) hasOutros = true;
 		});
 
 		this.pending.block.hasUpdateMethod = isDynamic;
diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts
--- a/src/compile/nodes/EachBlock.ts
+++ b/src/compile/nodes/EachBlock.ts
@@ -119,7 +119,7 @@ export default class EachBlock extends Node {
 			this.else.block.hasUpdateMethod = this.else.block.dependencies.size > 0;
 		}
 
-		if (this.block.hasOutroMethod || (this.else && this.else.block.hasOutroMethod)) {
+		if (this.block.hasOutros || (this.else && this.else.block.hasOutros)) {
 			block.addOutro();
 		}
 	}
@@ -317,14 +317,14 @@ export default class EachBlock extends Node {
 		const rects = block.getUniqueName('rects');
 		const destroy = this.block.hasAnimation
 			? `@fixAndOutroAndDestroyBlock`
-			: this.block.hasOutroMethod
+			: this.block.hasOutros
 				? `@outroAndDestroyBlock`
 				: `@destroyBlock`;
 
 		block.builders.update.addBlock(deindent`
 			const ${this.each_block_value} = ${snippet};
 
-			${this.block.hasOutroMethod && `@transitionManager.groupOutros();`}
+			${this.block.hasOutros && `@transitionManager.groupOutros();`}
 			${this.block.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].r();`}
 			${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.each_block_value}, ${lookup}, ${updateMountNode}, ${destroy}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.get_each_context});
 			${this.block.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].a();`}
@@ -393,7 +393,7 @@ export default class EachBlock extends Node {
 			allDependencies.add(dependency);
 		});
 
-		const outro = this.block.hasOutroMethod && block.getUniqueName('outro')
+		const outro = this.block.hasOutros && block.getUniqueName('outro')
 		if (outro) {
 			block.builders.init.addBlock(deindent`
 				function ${outro}(i, detach, fn) {
@@ -415,7 +415,7 @@ export default class EachBlock extends Node {
 
 		if (condition !== '') {
 			const forLoopBody = this.block.hasUpdateMethod
-				? (this.block.hasIntroMethod || this.block.hasOutroMethod)
+				? (this.block.hasIntros || this.block.hasOutros)
 					? deindent`
 						if (${iterations}[#i]) {
 							${iterations}[#i].p(changed, child_ctx);
@@ -444,7 +444,7 @@ export default class EachBlock extends Node {
 
 			let destroy;
 
-			if (this.block.hasOutroMethod) {
+			if (this.block.hasOutros) {
 				destroy = deindent`
 					@transitionManager.groupOutros();
 					for (; #i < ${iterations}.length; #i += 1) ${outro}(#i, 1);
diff --git a/src/compile/nodes/IfBlock.ts b/src/compile/nodes/IfBlock.ts
--- a/src/compile/nodes/IfBlock.ts
+++ b/src/compile/nodes/IfBlock.ts
@@ -68,8 +68,8 @@ export default class IfBlock extends Node {
 				block.addDependencies(node.block.dependencies);
 			}
 
-			if (node.block.hasIntroMethod) hasIntros = true;
-			if (node.block.hasOutroMethod) hasOutros = true;
+			if (node.block.hasIntros) hasIntros = true;
+			if (node.block.hasOutros) hasOutros = true;
 
 			if (isElseIf(node.else)) {
 				attachBlocks(node.else.children[0]);
@@ -302,8 +302,8 @@ export default class IfBlock extends Node {
 		const destroyOldBlock = deindent`
 			@transitionManager.groupOutros();
 			${name}.o(function() {
-				${if_blocks}[ ${previous_block_index} ].d(1);
-				${if_blocks}[ ${previous_block_index} ] = null;
+				${if_blocks}[${previous_block_index}].d(1);
+				${if_blocks}[${previous_block_index}] = null;
 			});
 		`;
 
@@ -355,9 +355,7 @@ export default class IfBlock extends Node {
 		}
 
 		block.builders.destroy.addLine(deindent`
-			${if_current_block_type_index}{
-				${if_blocks}[${current_block_type_index}].d(${parentNode ? '' : 'detach'});
-			}
+			${if_current_block_type_index}${if_blocks}[${current_block_type_index}].d(${parentNode ? '' : 'detach'});
 		`);
 	}
 
diff --git a/src/compile/nodes/Title.ts b/src/compile/nodes/Title.ts
--- a/src/compile/nodes/Title.ts
+++ b/src/compile/nodes/Title.ts
@@ -81,7 +81,7 @@ export default class Title extends Node {
 			if (allDependencies.size) {
 				const dependencies = Array.from(allDependencies);
 				const changedCheck = (
-					( block.hasOutroMethod ? `#outroing || ` : '' ) +
+					( block.hasOutros ? `#outroing || ` : '' ) +
 					dependencies.map(dependency => `changed.${dependency}`).join(' || ')
 				);
 
diff --git a/src/compile/nodes/shared/Tag.ts b/src/compile/nodes/shared/Tag.ts
--- a/src/compile/nodes/shared/Tag.ts
+++ b/src/compile/nodes/shared/Tag.ts
@@ -35,7 +35,7 @@ export default class Tag extends Node {
 
 		if (dependencies.size) {
 			const changedCheck = (
-				(block.hasOutroMethod ? `#outroing || ` : '') +
+				(block.hasOutros ? `#outroing || ` : '') +
 				[...dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')
 			);
 
diff --git a/src/parse/read/directives.ts b/src/parse/read/directives.ts
--- a/src/parse/read/directives.ts
+++ b/src/parse/read/directives.ts
@@ -77,7 +77,7 @@ const DIRECTIVES: Record<string, {
 		attribute(start, end, type, name, expression) {
 			return { start, end, type, name, expression };
 		},
-		allowedExpressionTypes: [ 'Identifier', 'MemberExpression', 'ObjectExpression', 'Literal', 'CallExpression' ],
+		allowedExpressionTypes: ['Identifier', 'MemberExpression', 'ObjectExpression', 'Literal', 'CallExpression'],
 		error: 'Data passed to actions must be an identifier (e.g. `foo`), a member expression ' +
 			'(e.g. `foo.bar` or `foo[baz]`), a method call (e.g. `foo()`), or a literal (e.g. `true` or `\'a string\'`'
 	},


Test patch:
diff --git a/test/runtime/samples/if-block-else-partial-outro/Foo.html b/test/runtime/samples/if-block-else-partial-outro/Foo.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/if-block-else-partial-outro/Foo.html
@@ -0,0 +1 @@
+foo
\ No newline at end of file
diff --git a/test/runtime/samples/if-block-else-partial-outro/_config.js b/test/runtime/samples/if-block-else-partial-outro/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/if-block-else-partial-outro/_config.js
@@ -0,0 +1,19 @@
+export default {
+	data: {
+		x: 1,
+		y: false,
+	},
+
+	html: `
+		<span>1</span>
+	`,
+
+	nestedTransitions: true,
+
+	test(assert, component, target) {
+		component.set({ x: 2 });
+		assert.htmlEqual(target.innerHTML, `
+			<span>2</span>
+		`);
+	},
+};
diff --git a/test/runtime/samples/if-block-else-partial-outro/main.html b/test/runtime/samples/if-block-else-partial-outro/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/if-block-else-partial-outro/main.html
@@ -0,0 +1,20 @@
+{#if y}
+	<Foo/>
+{:else}
+	<span>{x}</span>
+{/if}
+
+<script>
+	export default {
+		components: {
+			Foo: './Foo.html'
+		},
+
+		data() {
+			return {
+				x: 'x',
+				y: false
+			};
+		}
+	};
+</script>
\ No newline at end of file