From dba32df84e7ee41c8a3699ce347a7b272a2a2ee1 Mon Sep 17 00:00:00 2001
From: Rich Harris <richard.a.harris@gmail.com>
Date: Sun, 3 Dec 2017 16:25:01 -0500
Subject: [PATCH] client-side dynamic components mostly working (#640)

---
 src/generators/Generator.ts                   |   4 +
 src/generators/dom/preprocess.ts              |   8 +-
 src/generators/dom/visitors/Component.ts      | 141 ++++++++++++++----
 .../dom/visitors/Element/Element.ts           |   2 +-
 src/parse/state/tag.ts                        |   3 +-
 test/runtime/index.js                         |   4 +
 test/runtime/samples/switch/Bar.html          |   1 +
 test/runtime/samples/switch/Foo.html          |   1 +
 test/runtime/samples/switch/_config.js        |  19 +++
 test/runtime/samples/switch/main.html         |  12 ++
 10 files changed, 162 insertions(+), 33 deletions(-)
 create mode 100644 test/runtime/samples/switch/Bar.html
 create mode 100644 test/runtime/samples/switch/Foo.html
 create mode 100644 test/runtime/samples/switch/_config.js
 create mode 100644 test/runtime/samples/switch/main.html

diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts
index 323af454c7..2dcc3f3797 100644
--- a/src/generators/Generator.ts
+++ b/src/generators/Generator.ts
@@ -763,6 +763,10 @@ export default class Generator {
 					node.metadata = contextualise(node.expression, contextDependencies, indexes);
 					this.skip();
 				}
+
+				if (node.type === 'Element' && node.name === ':Switch') {
+					node.metadata = contextualise(node.expression, contextDependencies, indexes);
+				}
 			},
 
 			leave(node: Node, parent: Node) {
diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts
index 62768184a0..a6333c286c 100644
--- a/src/generators/dom/preprocess.ts
+++ b/src/generators/dom/preprocess.ts
@@ -436,13 +436,17 @@ const preprocessors = {
 		}
 
 		const isComponent =
-			generator.components.has(node.name) || node.name === ':Self';
+			generator.components.has(node.name) || node.name === ':Self' || node.name === ':Switch';
 
 		if (isComponent) {
 			cannotUseInnerHTML(node);
 
 			node.var = block.getUniqueName(
-				(node.name === ':Self' ? generator.name : node.name).toLowerCase()
+				(
+					node.name === ':Self' ? generator.name :
+					node.name === ':Switch' ? 'switch_instance' :
+					node.name
+				).toLowerCase()
 			);
 
 			node._state = getChildState(state, {
diff --git a/src/generators/dom/visitors/Component.ts b/src/generators/dom/visitors/Component.ts
index bfd52559d6..a9dbd4dbe2 100644
--- a/src/generators/dom/visitors/Component.ts
+++ b/src/generators/dom/visitors/Component.ts
@@ -3,6 +3,7 @@ import CodeBuilder from '../../../utils/CodeBuilder';
 import visit from '../visit';
 import { DomGenerator } from '../index';
 import Block from '../Block';
+import isDomNode from './shared/isDomNode';
 import getTailSnippet from '../../../utils/getTailSnippet';
 import getObject from '../../../utils/getObject';
 import getExpressionPrecedence from '../../../utils/getExpressionPrecedence';
@@ -67,6 +68,9 @@ export default function visitComponent(
 		.filter((a: Node) => a.type === 'Binding')
 		.map((a: Node) => mungeBinding(a, block));
 
+	const ref = node.attributes.find((a: Node) => a.type === 'Ref');
+	if (ref) generator.usesRefs = true;
+
 	if (attributes.length || bindings.length) {
 		const initialProps = attributes
 			.map((attribute: Attribute) => `${attribute.name}: ${attribute.value}`);
@@ -205,30 +209,122 @@ export default function visitComponent(
 		}
 	}
 
-	const expression = node.name === ':Self' ? generator.name : `%components-${node.name}`;
+	const isSwitch = node.name === ':Switch';
 
-	block.builders.init.addBlock(deindent`
-		${statements.join('\n')}
-		var ${name} = new ${expression}({
-			${componentInitProperties.join(',\n')}
-		});
+	const switch_vars = isSwitch && {
+		value: block.getUniqueName('switch_value'),
+		props: block.getUniqueName('switch_props')
+	};
 
-		${beforecreate}
-	`);
+	const expression = (
+		node.name === ':Self' ? generator.name :
+		isSwitch ? switch_vars.value :
+		`%components-${node.name}`
+	);
 
-	block.builders.create.addLine(`${name}._fragment.c();`);
+	if (isSwitch) {
+		block.contextualise(node.expression);
+		const { dependencies, snippet } = node.metadata;
+
+		const needsAnchor = node.next ? !isDomNode(node.next, generator) : !state.parentNode || !isDomNode(node.parent, generator);
+		const anchor = needsAnchor
+			? block.getUniqueName(`${name}_anchor`)
+			: (node.next && node.next.var) || 'null';
+
+		if (needsAnchor) {
+			block.addElement(
+				anchor,
+				`@createComment()`,
+				`@createComment()`,
+				state.parentNode
+			);
+		}
 
-	block.builders.claim.addLine(
-		`${name}._fragment.l(${state.parentNodes});`
-	);
+		const params = block.params.join(', ');
 
-	block.builders.mount.addLine(
-		`${name}._mount(${state.parentNode || '#target'}, ${state.parentNode ? 'null' : 'anchor'});`
-	);
+		block.builders.init.addBlock(deindent`
+			var ${switch_vars.value} = ${snippet};
+		`);
 
-	if (!state.parentNode) block.builders.unmount.addLine(`${name}._unmount();`);
+		block.builders.init.addBlock(deindent`
+			function ${switch_vars.props}(${params}) {
+				return {
+					${componentInitProperties.join(',\n')}
+				};
+			}
+
+			if (${switch_vars.value}) {
+				${statements.length > 0 && statements.join('\n')}
+				var ${name} = new ${expression}(${switch_vars.props}(${params}));
+
+				${beforecreate}
+			}
+		`);
+
+		block.builders.create.addLine(
+			`if (${name}) ${name}._fragment.c();`
+		);
+
+		block.builders.claim.addLine(
+			`if (${name}) ${name}._fragment.l(${state.parentNodes});`
+		);
+
+		block.builders.mount.addLine(
+			`if (${name}) ${name}._mount(${state.parentNode || '#target'}, ${state.parentNode ? 'null' : 'anchor'});`
+		);
+
+		block.builders.update.addBlock(deindent`
+			if (${switch_vars.value} !== (${switch_vars.value} = ${snippet})) {
+				if (${name}) ${name}.destroy();
 
-	block.builders.destroy.addLine(`${name}.destroy(false);`);
+				if (${switch_vars.value}) {
+					${name} = new ${switch_vars.value}(${switch_vars.props}(${params}));
+					${name}._fragment.c();
+					${name}._mount(${anchor}.parentNode, ${anchor});
+					${ref && `#component.refs.${ref.name} = ${name};`}
+				}
+
+				${ref && deindent`
+					else if (#component.refs.${ref.name} === ${name}) {
+						#component.refs.${ref.name} = null;
+					}`}
+			} else {
+				// normal update
+			}
+		`);
+
+		if (!state.parentNode) block.builders.unmount.addLine(`if (${name}) ${name}._unmount();`);
+
+		block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`);
+	} else {
+		block.builders.init.addBlock(deindent`
+			${statements.join('\n')}
+			var ${name} = new ${expression}({
+				${componentInitProperties.join(',\n')}
+			});
+
+			${beforecreate}
+
+			${ref && `#component.refs.${ref.name} = ${name};`}
+		`);
+
+		block.builders.create.addLine(`${name}._fragment.c();`);
+
+		block.builders.claim.addLine(
+			`${name}._fragment.l(${state.parentNodes});`
+		);
+
+		block.builders.mount.addLine(
+			`${name}._mount(${state.parentNode || '#target'}, ${state.parentNode ? 'null' : 'anchor'});`
+		);
+
+		if (!state.parentNode) block.builders.unmount.addLine(`${name}._unmount();`);
+
+		block.builders.destroy.addLine(deindent`
+			${name}.destroy(false);
+			${ref && `if (#component.refs.${ref.name} === ${name}) #component.refs.${ref.name} = null;`}
+		`);
+	}
 
 	// event handlers
 	node.attributes.filter((a: Node) => a.type === 'EventHandler').forEach((handler: Node) => {
@@ -274,17 +370,6 @@ export default function visitComponent(
 		`);
 	});
 
-	// refs
-	node.attributes.filter((a: Node) => a.type === 'Ref').forEach((ref: Node) => {
-		generator.usesRefs = true;
-
-		block.builders.init.addLine(`#component.refs.${ref.name} = ${name};`);
-
-		block.builders.destroy.addLine(deindent`
-			if (#component.refs.${ref.name} === ${name}) #component.refs.${ref.name} = null;
-		`);
-	});
-
 	// maintain component context
 	if (allContexts.size) {
 		const contexts = Array.from(allContexts);
diff --git a/src/generators/dom/visitors/Element/Element.ts b/src/generators/dom/visitors/Element/Element.ts
index 8de38c766d..7dc6eb7262 100644
--- a/src/generators/dom/visitors/Element/Element.ts
+++ b/src/generators/dom/visitors/Element/Element.ts
@@ -43,7 +43,7 @@ export default function visitElement(
 		}
 	}
 
-	if (generator.components.has(node.name) || node.name === ':Self') {
+	if (generator.components.has(node.name) || node.name === ':Self' || node.name === ':Switch') {
 		return visitComponent(generator, block, state, node, elementStack, componentStack);
 	}
 
diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts
index d2c17e30fa..c62053ffb5 100644
--- a/src/parse/state/tag.ts
+++ b/src/parse/state/tag.ts
@@ -171,6 +171,7 @@ export default function tag(parser: Parser) {
 		element.expression = readExpression(parser);
 		parser.allowWhitespace();
 		parser.eat('}', true);
+		parser.allowWhitespace();
 	}
 
 	const uniqueNames = new Set();
@@ -181,8 +182,6 @@ export default function tag(parser: Parser) {
 		parser.allowWhitespace();
 	}
 
-	parser.allowWhitespace();
-
 	// special cases – top-level <script> and <style>
 	if (specials.has(name) && parser.stack.length === 1) {
 		const special = specials.get(name);
diff --git a/test/runtime/index.js b/test/runtime/index.js
index af60caa84c..1ead0a11cb 100644
--- a/test/runtime/index.js
+++ b/test/runtime/index.js
@@ -1,4 +1,5 @@
 import assert from "assert";
+import chalk from 'chalk';
 import * as path from "path";
 import * as fs from "fs";
 import * as acorn from "acorn";
@@ -89,6 +90,9 @@ describe("runtime", () => {
 					}
 				} catch (err) {
 					failed.add(dir);
+					if (err.frame) {
+						console.error(chalk.red(err.frame)); // eslint-disable-line no-console
+					}
 					showOutput(cwd, { shared, format: 'cjs', store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
 					throw err;
 				}
diff --git a/test/runtime/samples/switch/Bar.html b/test/runtime/samples/switch/Bar.html
new file mode 100644
index 0000000000..7c21e68c89
--- /dev/null
+++ b/test/runtime/samples/switch/Bar.html
@@ -0,0 +1 @@
+<p>{{x}}, therefore Bar</p>
\ No newline at end of file
diff --git a/test/runtime/samples/switch/Foo.html b/test/runtime/samples/switch/Foo.html
new file mode 100644
index 0000000000..2303a92c0a
--- /dev/null
+++ b/test/runtime/samples/switch/Foo.html
@@ -0,0 +1 @@
+<p>{{x}}, therefore Foo</p>
\ No newline at end of file
diff --git a/test/runtime/samples/switch/_config.js b/test/runtime/samples/switch/_config.js
new file mode 100644
index 0000000000..915ce25717
--- /dev/null
+++ b/test/runtime/samples/switch/_config.js
@@ -0,0 +1,19 @@
+export default {
+	data: {
+		x: true
+	},
+
+	html: `
+		<p>true, therefore Foo</p>
+	`,
+
+	test(assert, component, target) {
+		component.set({
+			x: false
+		});
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>false, therefore Bar</p>
+		`);
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/switch/main.html b/test/runtime/samples/switch/main.html
new file mode 100644
index 0000000000..eb39498a5c
--- /dev/null
+++ b/test/runtime/samples/switch/main.html
@@ -0,0 +1,12 @@
+<:Switch { x ? Foo : Bar } x='{{x}}'/>
+
+<script>
+	import Foo from './Foo.html';
+	import Bar from './Bar.html';
+
+	export default {
+		data() {
+			return { Foo, Bar };
+		}
+	};
+</script>
\ No newline at end of file