Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
Event listeners in slots
REPL: https://v3.svelte.technology/repl?version=3.0.0-alpha16&gist=0bad4c3fd7428de50069a24a759dab95


Discussion:

Resolving patch:
diff --git a/src/compile/render-dom/Block.ts b/src/compile/render-dom/Block.ts
--- a/src/compile/render-dom/Block.ts
+++ b/src/compile/render-dom/Block.ts
@@ -234,29 +234,7 @@ export default class Block {
 			this.builders.mount.addLine(`${this.autofocus}.focus();`);
 		}
 
-		if (this.event_listeners.length > 0) {
-			this.addVariable('#dispose');
-
-			if (this.event_listeners.length === 1) {
-				this.builders.hydrate.addLine(
-					`#dispose = ${this.event_listeners[0]};`
-				);
-
-				this.builders.destroy.addLine(
-					`#dispose();`
-				)
-			} else {
-				this.builders.hydrate.addBlock(deindent`
-					#dispose = [
-						${this.event_listeners.join(',\n')}
-					];
-				`);
-
-				this.builders.destroy.addLine(
-					`@run_all(#dispose);`
-				);
-			}
-		}
+		this.renderListeners();
 
 		const properties = new CodeBuilder();
 
@@ -403,6 +381,32 @@ export default class Block {
 		});
 	}
 
+	renderListeners(chunk: string = '') {
+		if (this.event_listeners.length > 0) {
+			this.addVariable(`#dispose${chunk}`);
+
+			if (this.event_listeners.length === 1) {
+				this.builders.hydrate.addLine(
+					`#dispose${chunk} = ${this.event_listeners[0]};`
+				);
+
+				this.builders.destroy.addLine(
+					`#dispose${chunk}();`
+				)
+			} else {
+				this.builders.hydrate.addBlock(deindent`
+					#dispose${chunk} = [
+						${this.event_listeners.join(',\n')}
+					];
+				`);
+
+				this.builders.destroy.addLine(
+					`@run_all(#dispose${chunk});`
+				);
+			}
+		}
+	}
+
 	toString() {
 		const localKey = this.key && this.getUniqueName('key');
 
diff --git a/src/compile/render-dom/wrappers/Slot.ts b/src/compile/render-dom/wrappers/Slot.ts
--- a/src/compile/render-dom/wrappers/Slot.ts
+++ b/src/compile/render-dom/wrappers/Slot.ts
@@ -72,7 +72,11 @@ export default class SlotWrapper extends Wrapper {
 		block.builders.update.pushCondition(`!${content_name}`);
 		block.builders.destroy.pushCondition(`!${content_name}`);
 
+		const listeners = block.event_listeners;
+		block.event_listeners = [];
 		this.fragment.render(block, parentNode, parentNodes);
+		block.renderListeners(`_${content_name}`);
+		block.event_listeners = listeners;
 
 		block.builders.create.popCondition();
 		block.builders.hydrate.popCondition();
diff --git a/src/utils/CodeBuilder.ts b/src/utils/CodeBuilder.ts
--- a/src/utils/CodeBuilder.ts
+++ b/src/utils/CodeBuilder.ts
@@ -1,168 +1,111 @@
 import repeat from './repeat';
 
-enum ChunkType {
-	Line,
-	Block
+const whitespace = /^\s+$/;
+
+interface Chunk {
+	parent?: BlockChunk;
+	type: 'root'|'line'|'condition';
+	children?: Chunk[];
+	line?: string;
+	block?: boolean;
+	condition?: string;
 }
 
-interface Condition {
-	condition: string;
-	used: boolean;
+interface BlockChunk extends Chunk {
+	type: 'root'|'condition';
+	children: Chunk[];
+	parent: BlockChunk;
 }
 
 export default class CodeBuilder {
-	result: string;
-	first: ChunkType;
-	last: ChunkType;
-	lastCondition: string;
-	conditionStack: Condition[];
-	indent: string;
+	root: BlockChunk = { type: 'root', children: [], parent: null };
+	last: Chunk;
+	current: BlockChunk;
 
 	constructor(str = '') {
-		this.result = str;
-
-		const initial = str
-			? /\n/.test(str) ? ChunkType.Block : ChunkType.Line
-			: null;
-		this.first = initial;
-		this.last = initial;
-
-		this.lastCondition = null;
-		this.conditionStack = [];
-		this.indent = '';
+		this.current = this.last = this.root;
+		this.addLine(str);
 	}
 
 	addConditional(condition: string, body: string) {
-		this.reifyConditions();
-
-		const indent = this.indent + (condition ? '\t' : '');
-		body = body.replace(/^/gm, indent);
-
-		if (condition === this.lastCondition) {
-			this.result += `\n${body}`;
+		if (this.last.type === 'condition' && this.last.condition === condition) {
+			if (body && !whitespace.test(body)) this.last.children.push({ type: 'line', line: body });
 		} else {
-			if (this.lastCondition) {
-				this.result += `\n${this.indent}}`;
-			}
-
-			const block = condition
-				? `if (${condition}) {\n${body}`
-				: body;
-
-			this.result += `${this.last === ChunkType.Block ? '\n\n' : '\n'}${this.indent}${block}`;
-			this.lastCondition = condition;
+			const next = this.last = { type: 'condition', condition, parent: this.current, children: [] };
+			this.current.children.push(next);
+			if (body && !whitespace.test(body)) next.children.push({ type: 'line', line: body });
 		}
-
-		this.last = ChunkType.Block;
 	}
 
 	addLine(line: string) {
-		this.reifyConditions();
-
-		if (this.lastCondition) {
-			this.result += `\n${this.indent}}`;
-			this.lastCondition = null;
-		}
-
-		if (this.last === ChunkType.Block) {
-			this.result += `\n\n${this.indent}${line}`;
-		} else if (this.last === ChunkType.Line) {
-			this.result += `\n${this.indent}${line}`;
-		} else {
-			this.result += line;
-		}
-
-		this.last = ChunkType.Line;
-		if (!this.first) this.first = ChunkType.Line;
+		if (line && !whitespace.test(line)) this.current.children.push(this.last = { type: 'line', line });
 	}
 
 	addLineAtStart(line: string) {
-		this.reifyConditions();
-
-		if (this.first === ChunkType.Block) {
-			this.result = `${line}\n\n${this.indent}${this.result}`;
-		} else if (this.first === ChunkType.Line) {
-			this.result = `${line}\n${this.indent}${this.result}`;
-		} else {
-			this.result += line;
-		}
-
-		this.first = ChunkType.Line;
-		if (!this.last) this.last = ChunkType.Line;
+		if (line && !whitespace.test(line)) this.root.children.unshift({ type: 'line', line });
 	}
 
 	addBlock(block: string) {
-		this.reifyConditions();
-
-		if (this.indent) block = block.replace(/^/gm, `${this.indent}`);
-
-		if (this.lastCondition) {
-			this.result += `\n${this.indent}}`;
-			this.lastCondition = null;
-		}
-
-		if (this.result) {
-			this.result += `\n\n${this.indent}${block}`;
-		} else {
-			this.result += block;
-		}
-
-		this.last = ChunkType.Block;
-		if (!this.first) this.first = ChunkType.Block;
+		if (block && !whitespace.test(block)) this.current.children.push(this.last = { type: 'line', line: block, block: true });
 	}
 
 	addBlockAtStart(block: string) {
-		this.reifyConditions();
-
-		if (this.result) {
-			this.result = `${block}\n\n${this.indent}${this.result}`;
-		} else {
-			this.result += block;
-		}
-
-		this.first = ChunkType.Block;
-		if (!this.last) this.last = ChunkType.Block;
+		if (block && !whitespace.test(block)) this.root.children.unshift({ type: 'line', line: block, block: true });
 	}
 
-	isEmpty() {
-		return this.result === '';
-	}
+	isEmpty() { return !findLine(this.root); }
 
 	pushCondition(condition: string) {
-		this.conditionStack.push({ condition, used: false });
+		if (this.last.type === 'condition' && this.last.condition === condition) {
+			this.current = this.last as BlockChunk;
+		} else {
+			const next = this.last = { type: 'condition', condition, parent: this.current, children: [] };
+			this.current.children.push(next);
+			this.current = next;
+		}
 	}
 
 	popCondition() {
-		const { used } = this.conditionStack.pop();
+		if (!this.current.parent) throw new Error(`Popping a condition that maybe wasn't pushed.`);
+		this.current = this.current.parent;
+	}
 
-		this.indent = repeat('\t', this.conditionStack.length);
-		if (used) this.addLine('}');
+	toString() {
+		return chunkToString(this.root);
 	}
+}
 
-	reifyConditions() {
-		for (let i = 0; i < this.conditionStack.length; i += 1) {
-			const condition = this.conditionStack[i];
-			if (!condition.used) {
-				const line = `if (${condition.condition}) {`;
-
-				if (this.last === ChunkType.Block) {
-					this.result += `\n\n${this.indent}${line}`;
-				} else if (this.last === ChunkType.Line) {
-					this.result += `\n${this.indent}${line}`;
-				} else {
-					this.result += line;
-				}
-
-				this.last = ChunkType.Line;
-				if (!this.first) this.first = ChunkType.Line;
-
-				this.indent = repeat('\t', this.conditionStack.length);
-				condition.used = true;
-			}
-		}
+function findLine(chunk: BlockChunk) {
+	for (const c of chunk.children) {
+		if (c.type === 'line' || findLine(c as BlockChunk)) return true;
 	}
+	return false;
+}
 
-	toString() {
-		return this.result.trim() + (this.lastCondition ? `\n}` : ``);
+function chunkToString(chunk: Chunk, level: number = 0, lastBlock?: boolean, first?: boolean): string {
+	if (chunk.type === 'line') {
+		return `${lastBlock || (!first && chunk.block) ? '\n' : ''}${chunk.line.replace(/^/gm, repeat('\t', level))}`;
+	} else if (chunk.type === 'condition') {
+		let t = false;
+		const lines = chunk.children.map((c, i) => {
+			const str = chunkToString(c, level + 1, t, i === 0);
+			t = c.type !== 'line' || c.block;
+			return str;
+		}).filter(l => !!l);
+
+		if (!lines.length) return '';
+
+		return `${lastBlock || (!first) ? '\n' : ''}${repeat('\t', level)}if (${chunk.condition}) {\n${lines.join('\n')}\n${repeat('\t', level)}}`;
+	} else if (chunk.type === 'root') {
+		let t = false;
+		const lines = chunk.children.map((c, i) => {
+			const str = chunkToString(c, 0, t, i === 0);
+			t = c.type !== 'line' || c.block;
+			return str;
+		}).filter(l => !!l);
+
+		if (!lines.length) return '';
+
+		return lines.join('\n');
 	}
 }


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
@@ -86,6 +86,7 @@ function create_fragment($$, ctx) {
 
 		d(detach) {
 			if_block.d(detach);
+			
 			if (detach) {
 				detachNode(if_block_anchor);
 			}
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
@@ -62,6 +62,7 @@ function create_fragment($$, ctx) {
 
 		d(detach) {
 			if (if_block) if_block.d(detach);
+			
 			if (detach) {
 				detachNode(if_block_anchor);
 			}
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
@@ -237,11 +237,13 @@ function create_fragment($$, ctx) {
 			if (if_block1) if_block1.d();
 			if (if_block2) if_block2.d();
 			if (if_block3) if_block3.d();
+
 			if (detach) {
 				detachNode(text7);
 			}
 
 			if (if_block4) if_block4.d(detach);
+			
 			if (detach) {
 				detachNode(if_block4_anchor);
 			}
diff --git a/test/runtime/samples/component-slot-used-with-default-event/Nested.html b/test/runtime/samples/component-slot-used-with-default-event/Nested.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-used-with-default-event/Nested.html
@@ -0,0 +1,7 @@
+<script>
+	function click() {}
+</script>
+
+<p>
+	<slot><button on:click>Should not appear</button></slot>
+</p>
\ No newline at end of file
diff --git a/test/runtime/samples/component-slot-used-with-default-event/_config.js b/test/runtime/samples/component-slot-used-with-default-event/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-used-with-default-event/_config.js
@@ -0,0 +1,3 @@
+export default {
+	html: '<p>Hello</p>', show: true
+};
diff --git a/test/runtime/samples/component-slot-used-with-default-event/main.html b/test/runtime/samples/component-slot-used-with-default-event/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-used-with-default-event/main.html
@@ -0,0 +1,7 @@
+<script>
+	import Nested from './Nested.html';
+</script>
+
+<Nested>
+	Hello
+</Nested>
\ No newline at end of file