Repository: sveltejs/svelte (JavaScript)
Date: 2018-04-04 10:38:18+00:00
Task type: Bug Fix

Issue:
DOM order is not guaranteed when using keys
If you add a key to an each block, when that updates, the DOM order of the component is no longer guaranteed - maybe it's moving those updates to the end.

```
{{#each ones as one @text}}
  <div>{{one.text}}</div>
{{/each}}

{{#each twos as two @text}}
  <div>{{two.text}}</div>
{{/each}}

<script>
  export default {
    data()
      return {
	ones: [{ text: '1' }],
      	twos: [{ text: '2' }],
       };
    },
    oncreate() {
      this.set({ ones: [{ text: '11' }] });
    }
  }
</script>
```
If you run this, you'll see that the "ones" are placed after the "twos" once the component is created.

REPL: https://svelte.technology/repl?version=1.60.0&gist=e4d16c0723f25a51525fac0b8afd02d8



Issue discussion:

Solution patch:
diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts
--- a/src/generators/nodes/EachBlock.ts
+++ b/src/generators/nodes/EachBlock.ts
@@ -298,7 +298,7 @@ export default class EachBlock extends Node {
 		block.builders.update.addBlock(deindent`
 			var ${each_block_value} = ${snippet};
 
-			${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", function(#i) {
+			${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) {
 				return @assign(@assign({}, state), {
 					${this.contextProps.join(',\n')}
 				});
diff --git a/src/shared/keyed-each.js b/src/shared/keyed-each.js
--- a/src/shared/keyed-each.js
+++ b/src/shared/keyed-each.js
@@ -10,7 +10,7 @@ export function outroAndDestroyBlock(block, lookup) {
 	});
 }
 
-export function updateKeyedEach(old_blocks, component, changed, key_prop, dynamic, list, lookup, node, has_outro, create_each_block, intro_method, get_context) {
+export function updateKeyedEach(old_blocks, component, changed, key_prop, dynamic, list, lookup, node, has_outro, create_each_block, intro_method, next, get_context) {
 	var o = old_blocks.length;
 	var n = list.length;
 
@@ -39,16 +39,15 @@ export function updateKeyedEach(old_blocks, component, changed, key_prop, dynami
 		if (key in old_indexes) deltas[key] = Math.abs(i - old_indexes[key]);
 	}
 
-	var next = null;
-
 	var will_move = {};
 	var did_move = {};
 
 	var destroy = has_outro ? outroAndDestroyBlock : destroyBlock;
 
 	function insert(block) {
-		block[intro_method](node, next && next.first);
-		next = lookup[block.key] = block;
+		block[intro_method](node, next);
+		lookup[block.key] = block;
+		next = block.first;
 		n--;
 	}
 
@@ -60,7 +59,7 @@ export function updateKeyedEach(old_blocks, component, changed, key_prop, dynami
 
 		if (new_block === old_block) {
 			// do nothing
-			next = new_block;
+			next = new_block.first;
 			o--;
 			n--;
 		}


Test patch:
diff --git a/test/runtime/samples/each-block-keyed-siblings/_config.js b/test/runtime/samples/each-block-keyed-siblings/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-siblings/_config.js
@@ -0,0 +1,22 @@
+export default {
+	data: {
+		ones: [{ text: '1' }],
+		twos: [{ text: '2' }],
+	},
+
+	html: `
+		<div>1</div>
+		<div>2</div>
+	`,
+
+	test(assert, component, target) {
+		component.set({
+			ones: [{ text: '11' }]
+		});
+
+		assert.htmlEqual(target.innerHTML, `
+			<div>11</div>
+			<div>2</div>
+		`);
+	},
+};
diff --git a/test/runtime/samples/each-block-keyed-siblings/main.html b/test/runtime/samples/each-block-keyed-siblings/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-siblings/main.html
@@ -0,0 +1,7 @@
+{{#each ones as one @text}}
+	<div>{{one.text}}</div>
+{{/each}}
+
+{{#each twos as two @text}}
+	<div>{{two.text}}</div>
+{{/each}}
\ No newline at end of file