Repository: sveltejs/svelte (JavaScript)
Date: 2019-01-27 04:43:14+00:00
Task type: Bug Fix

Issue:
Transitions going wrong with keyed each block
In [this example](https://v3.svelte.technology/repl?version=3.0.0-alpha19&gist=60468d4a462945ce61f77be060deec05), I don't think transitions should be happening at all when you hit 'shuffle' — no elements are being removed from the DOM and reinserted, they're just moving about.

It's also weird that if you hit the button a few times, fewer transitions happen each time.
Transitions going wrong with keyed each block
In [this example](https://v3.svelte.technology/repl?version=3.0.0-alpha19&gist=60468d4a462945ce61f77be060deec05), I don't think transitions should be happening at all when you hit 'shuffle' — no elements are being removed from the DOM and reinserted, they're just moving about.

It's also weird that if you hit the button a few times, fewer transitions happen each time.


Issue discussion:
Interesting point is that if we use `index` as a `key`, transitions never happen: 

https://v3.svelte.technology/repl?version=3.0.0-alpha19&gist=9b515856fdc42890c673d87de7c4ae52

If not use a `key` at all, the transition happens only once for each item.

Interesting point is that if we use `index` as a `key`, transitions never happen: 

https://v3.svelte.technology/repl?version=3.0.0-alpha19&gist=9b515856fdc42890c673d87de7c4ae52

If not use a `key` at all, the transition happens only once for each item.


Solution 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
@@ -223,6 +223,7 @@ export default class Block {
 
 			if (!this.builders.intro.isEmpty()) {
 				this.builders.intro.addLine(`#current = true;`);
+				this.builders.mount.addLine(`#current = true;`);
 			}
 
 			if (!this.builders.outro.isEmpty()) {
diff --git a/src/compile/render-dom/wrappers/EachBlock.ts b/src/compile/render-dom/wrappers/EachBlock.ts
--- a/src/compile/render-dom/wrappers/EachBlock.ts
+++ b/src/compile/render-dom/wrappers/EachBlock.ts
@@ -429,18 +429,19 @@ export default class EachBlockWrapper extends Wrapper {
 				? deindent`
 					if (${iterations}[#i]) {
 						${iterations}[#i].p(changed, child_ctx);
+						${has_transitions && `${iterations}[#i].i(1);`}
 					} else {
 						${iterations}[#i] = ${create_each_block}(child_ctx);
 						${iterations}[#i].c();
+						${has_transitions && `${iterations}[#i].i(1);`}
 						${iterations}[#i].m(${updateMountNode}, ${anchor});
 					}
-					${has_transitions && `${iterations}[#i].i(1);`}
 				`
 				: deindent`
 					${iterations}[#i] = ${create_each_block}(child_ctx);
 					${iterations}[#i].c();
-					${iterations}[#i].m(${updateMountNode}, ${anchor});
 					${has_transitions && `${iterations}[#i].i(1);`}
+					${iterations}[#i].m(${updateMountNode}, ${anchor});
 				`;
 
 			const start = this.block.hasUpdateMethod ? '0' : `${iterations}.length`;
diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts
--- a/src/compile/render-dom/wrappers/Element/index.ts
+++ b/src/compile/render-dom/wrappers/Element/index.ts
@@ -671,12 +671,13 @@ export default class ElementWrapper extends Wrapper {
 				if (outro) {
 					intro_block = deindent`
 						@add_render_callback(() => {
+							if (${outroName}) ${outroName}.end(1);
 							if (!${introName}) ${introName} = @create_in_transition(${this.var}, ${fn}, ${snippet});
 							${introName}.start();
 						});
 					`;
 
-					block.builders.outro.addLine(`if (${introName}) ${introName}.invalidate()`);
+					block.builders.outro.addLine(`if (${introName}) ${introName}.invalidate();`);
 				} else {
 					intro_block = deindent`
 						if (!${introName}) {
@@ -707,9 +708,11 @@ export default class ElementWrapper extends Wrapper {
 
 				const fn = component.qualify(outro.name);
 
-				block.builders.intro.addBlock(deindent`
-					if (${outroName}) ${outroName}.end(1);
-				`);
+				if (!intro) {
+					block.builders.intro.addBlock(deindent`
+						if (${outroName}) ${outroName}.end(1);
+					`);
+				}
 
 				// TODO hide elements that have outro'd (unless they belong to a still-outroing
 				// group) prior to their removal from the DOM
diff --git a/src/compile/render-dom/wrappers/IfBlock.ts b/src/compile/render-dom/wrappers/IfBlock.ts
--- a/src/compile/render-dom/wrappers/IfBlock.ts
+++ b/src/compile/render-dom/wrappers/IfBlock.ts
@@ -235,8 +235,8 @@ export default class IfBlockWrapper extends Wrapper {
 			${name} = ${current_block_type_and}${current_block_type}(ctx);
 			if (${name}) {
 				${name}.c();
-				${name}.m(${updateMountNode}, ${anchor});
 				${has_transitions && `${name}.i(1);`}
+				${name}.m(${updateMountNode}, ${anchor});
 			}
 		`;
 
@@ -334,8 +334,8 @@ export default class IfBlockWrapper extends Wrapper {
 				${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](ctx);
 				${name}.c();
 			}
-			${name}.m(${updateMountNode}, ${anchor});
 			${has_transitions && `${name}.i(1);`}
+			${name}.m(${updateMountNode}, ${anchor});
 		`;
 
 		const changeBlock = hasElse
@@ -407,20 +407,23 @@ export default class IfBlockWrapper extends Wrapper {
 			? deindent`
 				if (${name}) {
 					${name}.p(changed, ctx);
+					${has_transitions && `${name}.i(1);`}
 				} else {
 					${name} = ${branch.block.name}(ctx);
 					${name}.c();
+					${has_transitions && `${name}.i(1);`}
 					${name}.m(${updateMountNode}, ${anchor});
 				}
-				${has_transitions && `${name}.i(1);`}
 			`
 			: deindent`
 				if (!${name}) {
 					${name} = ${branch.block.name}(ctx);
 					${name}.c();
+					${has_transitions && `${name}.i(1);`}
 					${name}.m(${updateMountNode}, ${anchor});
+				${has_transitions && `} else {
+					${name}.i(1);`}
 				}
-				${has_transitions && `${name}.i(1);`}
 			`;
 
 		// no `p()` here — we don't want to update outroing nodes,
diff --git a/src/compile/render-dom/wrappers/InlineComponent/index.ts b/src/compile/render-dom/wrappers/InlineComponent/index.ts
--- a/src/compile/render-dom/wrappers/InlineComponent/index.ts
+++ b/src/compile/render-dom/wrappers/InlineComponent/index.ts
@@ -408,8 +408,8 @@ export default class InlineComponentWrapper extends Wrapper {
 						${munged_handlers}
 
 						${name}.$$.fragment.c();
-						@mount_component(${name}, ${updateMountNode}, ${anchor});
 						${name}.$$.fragment.i(1);
+						@mount_component(${name}, ${updateMountNode}, ${anchor});
 					} else {
 						${name} = null;
 					}
diff --git a/src/internal/Component.js b/src/internal/Component.js
--- a/src/internal/Component.js
+++ b/src/internal/Component.js
@@ -110,8 +110,8 @@ export function init(component, options, instance, create_fragment, not_equal) {
 			$$.fragment.c();
 		}
 
-		mount_component(component, options.target, options.anchor);
 		if (options.intro && component.$$.fragment.i) component.$$.fragment.i();
+		mount_component(component, options.target, options.anchor);
 		flush();
 	}
 
diff --git a/src/internal/await-block.js b/src/internal/await-block.js
--- a/src/internal/await-block.js
+++ b/src/internal/await-block.js
@@ -31,8 +31,8 @@ export function handlePromise(promise, info) {
 			}
 
 			block.c();
-			block.m(info.mount(), info.anchor);
 			if (block.i) block.i(1);
+			block.m(info.mount(), info.anchor);
 
 			flush();
 		}
diff --git a/src/internal/keyed-each.js b/src/internal/keyed-each.js
--- a/src/internal/keyed-each.js
+++ b/src/internal/keyed-each.js
@@ -52,8 +52,8 @@ export function updateKeyedEach(old_blocks, changed, get_key, dynamic, ctx, list
 	var did_move = {};
 
 	function insert(block) {
-		block.m(node, next);
 		if (block.i) block.i(1);
+		block.m(node, next);
 		lookup[block.key] = block;
 		next = block.first;
 		n--;


Test patch:
diff --git a/test/js/samples/component-static-array/expected.js b/test/js/samples/component-static-array/expected.js
--- a/test/js/samples/component-static-array/expected.js
+++ b/test/js/samples/component-static-array/expected.js
@@ -13,6 +13,7 @@ function create_fragment(ctx) {
 
 		m(target, anchor) {
 			mount_component(nested, target, anchor);
+			current = true;
 		},
 
 		p: noop,
diff --git a/test/js/samples/component-static-immutable/expected.js b/test/js/samples/component-static-immutable/expected.js
--- a/test/js/samples/component-static-immutable/expected.js
+++ b/test/js/samples/component-static-immutable/expected.js
@@ -13,6 +13,7 @@ function create_fragment(ctx) {
 
 		m(target, anchor) {
 			mount_component(nested, target, anchor);
+			current = true;
 		},
 
 		p: noop,
diff --git a/test/js/samples/component-static-immutable2/expected.js b/test/js/samples/component-static-immutable2/expected.js
--- a/test/js/samples/component-static-immutable2/expected.js
+++ b/test/js/samples/component-static-immutable2/expected.js
@@ -13,6 +13,7 @@ function create_fragment(ctx) {
 
 		m(target, anchor) {
 			mount_component(nested, target, anchor);
+			current = true;
 		},
 
 		p: noop,
diff --git a/test/js/samples/component-static/expected.js b/test/js/samples/component-static/expected.js
--- a/test/js/samples/component-static/expected.js
+++ b/test/js/samples/component-static/expected.js
@@ -13,6 +13,7 @@ function create_fragment(ctx) {
 
 		m(target, anchor) {
 			mount_component(nested, target, anchor);
+			current = true;
 		},
 
 		p: noop,
diff --git a/test/js/samples/dynamic-import/expected.js b/test/js/samples/dynamic-import/expected.js
--- a/test/js/samples/dynamic-import/expected.js
+++ b/test/js/samples/dynamic-import/expected.js
@@ -14,6 +14,7 @@ function create_fragment(ctx) {
 
 		m(target, anchor) {
 			mount_component(lazyload, target, anchor);
+			current = true;
 		},
 
 		p: noop,
diff --git a/test/js/samples/non-imported-component/expected.js b/test/js/samples/non-imported-component/expected.js
--- a/test/js/samples/non-imported-component/expected.js
+++ b/test/js/samples/non-imported-component/expected.js
@@ -20,6 +20,7 @@ function create_fragment(ctx) {
 			mount_component(imported, target, anchor);
 			insert(target, text, anchor);
 			mount_component(nonimported, target, anchor);
+			current = true;
 		},
 
 		p: noop,
diff --git a/test/js/samples/transition-local/expected.js b/test/js/samples/transition-local/expected.js
--- a/test/js/samples/transition-local/expected.js
+++ b/test/js/samples/transition-local/expected.js
@@ -23,9 +23,11 @@ function create_if_block(ctx) {
 				if (!if_block) {
 					if_block = create_if_block_1(ctx);
 					if_block.c();
+					if_block.i(1);
 					if_block.m(if_block_anchor.parentNode, if_block_anchor);
+				} else {
+									if_block.i(1);
 				}
-				if_block.i(1);
 			} else if (if_block) {
 				if_block.d(1);
 				if_block = null;
diff --git a/test/runtime/samples/transition-js-each-keyed-unchanged/_config.js b/test/runtime/samples/transition-js-each-keyed-unchanged/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-each-keyed-unchanged/_config.js
@@ -0,0 +1,25 @@
+export default {
+	props: {
+		numbers: [1, 2, 3, 4, 5]
+	},
+
+	test({ assert, component, target, raf }) {
+		const divs1 = target.querySelectorAll('div');
+		assert.equal(divs1[0].foo, undefined);
+
+		component.numbers = [1, 2, 5, 4, 3];
+		const divs2 = target.querySelectorAll('div');
+
+		assert.equal(divs1[0], divs2[0]);
+		assert.equal(divs1[1], divs2[1]);
+		assert.equal(divs1[2], divs2[4]);
+		assert.equal(divs1[3], divs2[3]);
+		assert.equal(divs1[4], divs2[2]);
+
+		assert.equal(divs1[0].foo, undefined);
+		assert.equal(divs1[1].foo, undefined);
+		assert.equal(divs1[2].foo, undefined);
+		assert.equal(divs1[3].foo, undefined);
+		assert.equal(divs1[4].foo, undefined);
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/transition-js-each-keyed-unchanged/main.html b/test/runtime/samples/transition-js-each-keyed-unchanged/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-each-keyed-unchanged/main.html
@@ -0,0 +1,16 @@
+<script>
+	export let numbers;
+
+	function foo(node, params) {
+		return {
+			duration: 100,
+			tick: t => {
+				node.foo = t;
+			}
+		};
+	}
+</script>
+
+{#each numbers as num, i (num)}
+	<div transition:foo>{num}</div>
+{/each}
\ No newline at end of file
diff --git a/test/runtime/samples/transition-js-each-unchanged/_config.js b/test/runtime/samples/transition-js-each-unchanged/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-each-unchanged/_config.js
@@ -0,0 +1,25 @@
+export default {
+	props: {
+		numbers: [1, 2, 3, 4, 5]
+	},
+
+	test({ assert, component, target, raf }) {
+		const divs1 = target.querySelectorAll('div');
+		assert.equal(divs1[0].foo, undefined);
+
+		component.numbers = [1, 2, 5, 4, 3];
+		const divs2 = target.querySelectorAll('div');
+
+		assert.equal(divs1[0], divs2[0]);
+		assert.equal(divs1[1], divs2[1]);
+		assert.equal(divs1[2], divs2[2]);
+		assert.equal(divs1[3], divs2[3]);
+		assert.equal(divs1[4], divs2[4]);
+
+		assert.equal(divs1[0].foo, undefined);
+		assert.equal(divs1[1].foo, undefined);
+		assert.equal(divs1[2].foo, undefined);
+		assert.equal(divs1[3].foo, undefined);
+		assert.equal(divs1[4].foo, undefined);
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/transition-js-each-unchanged/main.html b/test/runtime/samples/transition-js-each-unchanged/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-each-unchanged/main.html
@@ -0,0 +1,16 @@
+<script>
+	export let numbers;
+
+	function foo(node, params) {
+		return {
+			duration: 100,
+			tick: t => {
+				node.foo = t;
+			}
+		};
+	}
+</script>
+
+{#each numbers as num, i}
+	<div transition:foo>{num}</div>
+{/each}
\ No newline at end of file