Repository: sveltejs/svelte (JavaScript)
Date: 2019-08-04 13:49:22+00:00
Task type: Bug Fix

Issue:
unexpected behavior using unwrapped html expression as component output
https://svelte.dev/repl/98745830909e4c88979965fe00a8ba3a?version=3.2.1

### Repro Steps:
1. change the text in any field to modify the alphabetical order
2. change the focus

Repros in Chrome, FF, Safari. Svelte 3.2.1

Doesn't repro if the html expression is wrapped in an html tag by itself


Issue discussion:
Hi @CreaturesInUnitards! Very interesting. [It happens if the HTML string is in the topmost component as well](https://svelte.dev/repl/21425fc5339a4f37b1f4404f9d536430?version=3.2.1). If you as an experiment remove the each block key and change the ordering, the HTML isn't duplicated.
Here is a minimal example that shows the same problem: https://svelte.dev/repl/8d71f3064391458aae9f917c3b693888?version=3. The problem seems to be related to the keyed each, because non-keyed each makes the problem go away.
Have been trying to figure out what causes this. Think that the code for the indexed each will not call the `d()` that is made in its `create_each_block`, because no keys are removed. However the `m()` code does an `insertAdjacentHTML` call. So it seems to duplicate HTML on every mount call. The trouble is that it is impossible to create the HTML node beforehand without a parent element.

One way to solve this might be to call the detach logic when the node is mounted. However this logic must be called before the `<noscript>` markers are moved. Adding detach logic on the insert in the `RawMustacheTagWrapper` failed because of this...
Maybe it is a pragmatic idea to make it a syntax error to have an `{@html ...}` outside of an element. This gives the user less control about the actual html structure. However it makes it easier/more efficient to handle without the need for `<noscript>` markers. For instance React also uses a property on an element: https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml. Vue does have mustache markers but also a v-html directive on an element. Literal html should also be discouraged because of potential security issues :)
> However the `m()` code does an `insertAdjacentHTML` call. So it seems to duplicate HTML on every mount call. The trouble is that it is impossible to create the HTML node beforehand without a parent element.

I haven't touched Svelte code so far … but in Vanilla JS, you could use a [DocumentFragment](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment) for it, I guess.
Adding arbitrary html to a `documentFragment` using something like `innerHTML` seems not possible. So you would still need a nesting element. Seems that both Vue and React only allow arbitrary html on an element using a directive (`v-html` and `dangerouslySetInnerHTML`). So it might not be strange to enforce such a limitation...

Solution patch:
diff --git a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts
--- a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts
+++ b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts
@@ -47,10 +47,10 @@ export default class RawMustacheTagWrapper extends Tag {
 				content => `${html_tag}.p(${content});`
 			);
 
-			const anchor = in_head ? 'null' : needs_anchor ? html_anchor : this.next ? this.next.var : 'null';
+			const update_anchor = in_head ? 'null' : needs_anchor ? html_anchor : this.next ? this.next.var : 'null';
 
-			block.builders.hydrate.add_line(`${html_tag} = new @HtmlTag(${init}, ${anchor});`);
-			block.builders.mount.add_line(`${html_tag}.m(${parent_node || '#target'}, anchor);`);
+			block.builders.hydrate.add_line(`${html_tag} = new @HtmlTag(${init}, ${update_anchor});`);
+			block.builders.mount.add_line(`${html_tag}.m(${parent_node || '#target'}${parent_node ? '' : ', anchor'});`);
 
 			if (needs_anchor) {
 				block.add_element(html_anchor, '@empty()', '@empty()', parent_node);
diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts
--- a/src/runtime/internal/dom.ts
+++ b/src/runtime/internal/dom.ts
@@ -252,7 +252,7 @@ export class HtmlTag {
 		this.u(html);
 	}
 
-	m(target: HTMLElement, anchor: HTMLElement) {
+	m(target: HTMLElement, anchor: HTMLElement = null) {
 		for (let i = 0; i < this.n.length; i += 1) {
 			insert(target, this.n[i], anchor);
 		}


Test patch:
diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js
--- a/test/js/samples/each-block-changed-check/expected.js
+++ b/test/js/samples/each-block-changed-check/expected.js
@@ -55,7 +55,7 @@ function create_each_block(ctx) {
 			append(span, t4);
 			append(span, t5);
 			append(div, t6);
-			html_tag.m(div, anchor);
+			html_tag.m(div);
 		},
 
 		p(changed, ctx) {
diff --git a/test/runtime/samples/each-block-keyed-html-b/_config.js b/test/runtime/samples/each-block-keyed-html-b/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-html-b/_config.js
@@ -0,0 +1,14 @@
+export default {
+	html: `
+		<div><span>hello</span> John</div>
+		<div><span>hello</span> Jill</div>
+	`,
+
+	test({ assert, component, target }) {
+		component.names = component.names.reverse();
+		assert.htmlEqual(target.innerHTML, `
+			<div><span>hello</span> Jill</div>
+			<div><span>hello</span> John</div>
+		`);
+	}
+};
diff --git a/test/runtime/samples/each-block-keyed-html-b/main.svelte b/test/runtime/samples/each-block-keyed-html-b/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-html-b/main.svelte
@@ -0,0 +1,11 @@
+<script>
+	export let names = ['John', 'Jill'];
+</script>
+
+{#each names as name (name)}
+	<div>
+		<span>hello</span>
+		{@html name}
+	</div>
+{/each}
+