Repository: sveltejs/svelte (JavaScript)
Date: 2019-11-09 21:20:20+00:00
Task type: Bug Fix

Issue:
Void element 'children' get hydrated
**Describe the bug**
Svelte generates code that hydrates the children of `<img>` elements and other void elements, even though there can't be any by definition.

**To Reproduce**
https://svelte.dev/repl/f5c696a56a234934a9556be6a696e5db?version=3.12.1

**Expected behavior**
```diff
	return {
		c() {
			img = element("img");
			this.h()
		},

		l(nodes) {
			img = claim_element(nodes, "IMG", { src: true }, false);
-			var img_nodes = children(img);
-
-			img_nodes.forEach(detach);
			this.h();
		},

		h() {
			attr(img, "src", "donuts.jpg");
		},
```

**Severity**
Mostly harmless, but easily fixed


Issue discussion:

Solution patch:
diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts
--- a/src/compiler/compile/render_dom/wrappers/Element/index.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts
@@ -121,6 +121,7 @@ export default class ElementWrapper extends Wrapper {
 	select_binding_dependencies?: Set<string>;
 
 	var: any;
+	void: boolean;
 
 	constructor(
 		renderer: Renderer,
@@ -136,6 +137,8 @@ export default class ElementWrapper extends Wrapper {
 			name: node.name.replace(/[^a-zA-Z0-9_$]/g, '_')
 		};
 
+		this.void = is_void(node.name);
+
 		this.class_dependencies = [];
 
 		this.attributes = this.node.attributes.map(attribute => {
@@ -258,6 +261,7 @@ export default class ElementWrapper extends Wrapper {
 
 		const node = this.var;
 		const nodes = parent_nodes && block.get_unique_name(`${this.var.name}_nodes`); // if we're in unclaimable territory, i.e. <head>, parent_nodes is null
+		const children = x`@children(${this.node.name === 'template' ? x`${node}.content` : node})`;
 
 		block.add_variable(node);
 		const render_statement = this.get_render_statement();
@@ -269,8 +273,13 @@ export default class ElementWrapper extends Wrapper {
 			if (parent_nodes) {
 				block.chunks.claim.push(b`
 					${node} = ${this.get_claim_statement(parent_nodes)};
-					var ${nodes} = @children(${this.node.name === 'template' ? x`${node}.content` : node});
 				`);
+
+				if (!this.void && this.node.children.length > 0) {
+					block.chunks.claim.push(b`
+						var ${nodes} = ${children};
+					`);
+				}
 			} else {
 				block.chunks.claim.push(
 					b`${node} = ${render_statement};`
@@ -351,9 +360,9 @@ export default class ElementWrapper extends Wrapper {
 		this.add_classes(block);
 		this.add_manual_style_scoping(block);
 
-		if (nodes && this.renderer.options.hydratable) {
+		if (nodes && this.renderer.options.hydratable && !this.void) {
 			block.chunks.claim.push(
-				b`${nodes}.forEach(@detach);`
+				b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);`
 			);
 		}
 
@@ -915,7 +924,7 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | TagWrapper>, blo
 
 			state.quasi.value.raw += '>';
 
-			if (!is_void(wrapper.node.name)) {
+			if (!(wrapper as ElementWrapper).void) {
 				to_html((wrapper as ElementWrapper).fragment.nodes as Array<ElementWrapper | TextWrapper>, block, literal, state);
 
 				state.quasi.value.raw += `</${wrapper.node.name}>`;


Test patch:
diff --git a/test/js/samples/hydrated-void-element/_config.js b/test/js/samples/hydrated-void-element/_config.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/hydrated-void-element/_config.js
@@ -0,0 +1,5 @@
+export default {
+	options: {
+		hydratable: true
+	}
+};
\ No newline at end of file
diff --git a/test/js/samples/hydrated-void-element/expected.js b/test/js/samples/hydrated-void-element/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/hydrated-void-element/expected.js
@@ -0,0 +1,63 @@
+/* generated by Svelte vX.Y.Z */
+import {
+	SvelteComponent,
+	attr,
+	children,
+	claim_element,
+	claim_space,
+	detach,
+	element,
+	init,
+	insert,
+	noop,
+	safe_not_equal,
+	space
+} from "svelte/internal";
+
+function create_fragment(ctx) {
+	let img;
+	let t;
+	let div;
+
+	return {
+		c() {
+			img = element("img");
+			t = space();
+			div = element("div");
+			this.h();
+		},
+		l(nodes) {
+			img = claim_element(nodes, "IMG", { src: true, alt: true });
+			t = claim_space(nodes);
+			div = claim_element(nodes, "DIV", {});
+			children(div).forEach(detach);
+			this.h();
+		},
+		h() {
+			attr(img, "src", "donuts.jpg");
+			attr(img, "alt", "donuts");
+		},
+		m(target, anchor) {
+			insert(target, img, anchor);
+			insert(target, t, anchor);
+			insert(target, div, anchor);
+		},
+		p: noop,
+		i: noop,
+		o: noop,
+		d(detaching) {
+			if (detaching) detach(img);
+			if (detaching) detach(t);
+			if (detaching) detach(div);
+		}
+	};
+}
+
+class Component extends SvelteComponent {
+	constructor(options) {
+		super();
+		init(this, options, null, create_fragment, safe_not_equal, {});
+	}
+}
+
+export default Component;
\ No newline at end of file
diff --git a/test/js/samples/hydrated-void-element/input.svelte b/test/js/samples/hydrated-void-element/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/js/samples/hydrated-void-element/input.svelte
@@ -0,0 +1,2 @@
+<img src="donuts.jpg" alt="donuts">
+<div></div>
\ No newline at end of file