Repository: sveltejs/svelte (JavaScript)
Date: 2020-12-31 02:15:22+00:00
Task type: Bug Fix

Issue:
Animation with binded groups
Hi there! I was trying to replicate a classic multi-select component where the selections you make go up top. Something like this:

![daa597aca528bc93503ac34a274c9d59](https://user-images.githubusercontent.com/36263538/102010794-9d3aa680-3d40-11eb-98ae-58643b09a546.gif)

However, as soon as I try to add a flip animation or even make the `#each` block keyed, it results in the following:

![df2f5cb25821ed47b414cbb9d7c38a8b](https://user-images.githubusercontent.com/36263538/102010852-facef300-3d40-11eb-869a-b2eb07ff981e.gif)

[Here is REPL](https://svelte.dev/repl/a0fffe1957684d63ac856cbeb7203ada?version=3.31.0) of the above. If you remove the `animate` and the key, you will see it works fine. It doesn't seem to be a problem with the animation itself since only having the key breaks it.

Since I'm new to Svelte, I'm unsure if this is the intended behavior, a bug, or even a feature request. That's why I put this on _Questions and help_.

Thanks! I've been loving Svelte so far ❤️ 


Issue discussion:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 * Fix location of automatically declared reactive variables ([#5749](https://github.com/sveltejs/svelte/issues/5749))
 * Warn when using `className` or `htmlFor` attributes ([#5777](https://github.com/sveltejs/svelte/issues/5777))
+* Fix checkbox `bind:group` in keyed `{#each}` where the array can be reordered ([#5779](https://github.com/sveltejs/svelte/issues/5779))
 * Fix checkbox `bind:group` in nested `{#each}` contexts ([#5811](https://github.com/sveltejs/svelte/issues/5811))
 * Add graphics roles as known ARIA roles ([#5822](https://github.com/sveltejs/svelte/pull/5822))
 
diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts
--- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts
+++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts
@@ -447,6 +447,8 @@ export default class EachBlockWrapper extends Wrapper {
 				: '@destroy_block';
 
 		if (this.dependencies.size) {
+			this.block.maintain_context = true;
+
 			this.updates.push(b`
 				const ${this.vars.each_block_value} = ${snippet};
 				${this.renderer.options.dev && b`@validate_each_argument(${this.vars.each_block_value});`}
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
@@ -851,7 +851,21 @@ export default class ElementWrapper extends Wrapper {
 			${outro && b`@add_transform(${this.var}, ${rect});`}
 		`);
 
-		const params = this.node.animation.expression ? this.node.animation.expression.manipulate(block) : x`{}`;
+		let params;
+		if (this.node.animation.expression) {
+			params = this.node.animation.expression.manipulate(block);
+
+			if (this.node.animation.expression.dynamic_dependencies().length) {
+				// if `params` is dynamic, calculate params ahead of time in the `.r()` method
+				const params_var = block.get_unique_name('params');
+				block.add_variable(params_var);
+
+				block.chunks.measure.push(b`${params_var} = ${params};`);
+				params = params_var;
+			}
+		} else {
+			params = x`{}`;
+		}
 
 		const name = this.renderer.reference(this.node.animation.name);
 


Test patch:
diff --git a/test/js/samples/each-block-keyed-animated/expected.js b/test/js/samples/each-block-keyed-animated/expected.js
--- a/test/js/samples/each-block-keyed-animated/expected.js
+++ b/test/js/samples/each-block-keyed-animated/expected.js
@@ -43,7 +43,8 @@ function create_each_block(key_1, ctx) {
 			insert(target, div, anchor);
 			append(div, t);
 		},
-		p(ctx, dirty) {
+		p(new_ctx, dirty) {
+			ctx = new_ctx;
 			if (dirty & /*things*/ 1 && t_value !== (t_value = /*thing*/ ctx[1].name + "")) set_data(t, t_value);
 		},
 		r() {
diff --git a/test/js/samples/each-block-keyed/expected.js b/test/js/samples/each-block-keyed/expected.js
--- a/test/js/samples/each-block-keyed/expected.js
+++ b/test/js/samples/each-block-keyed/expected.js
@@ -39,7 +39,8 @@ function create_each_block(key_1, ctx) {
 			insert(target, div, anchor);
 			append(div, t);
 		},
-		p(ctx, dirty) {
+		p(new_ctx, dirty) {
+			ctx = new_ctx;
 			if (dirty & /*things*/ 1 && t_value !== (t_value = /*thing*/ ctx[1].name + "")) set_data(t, t_value);
 		},
 		d(detaching) {
diff --git a/test/runtime/samples/animation-js-delay/_config.js b/test/runtime/samples/animation-js-delay/_config.js
--- a/test/runtime/samples/animation-js-delay/_config.js
+++ b/test/runtime/samples/animation-js-delay/_config.js
@@ -18,7 +18,7 @@ export default {
 	`,
 
 	test({ assert, component, target, window, raf }) {
-		let divs = document.querySelectorAll('div');
+		let divs = window.document.querySelectorAll('div');
 		divs.forEach(div => {
 			div.getBoundingClientRect = function() {
 				const index = [...this.parentNode.children].indexOf(this);
@@ -41,7 +41,7 @@ export default {
 			{ id: 1, name: 'a' }
 		];
 
-		divs = document.querySelectorAll('div');
+		divs = window.document.querySelectorAll('div');
 		assert.equal(divs[0].dy, 120);
 		assert.equal(divs[4].dy, -120);
 
@@ -56,5 +56,29 @@ export default {
 		raf.tick(150);
 		assert.equal(divs[0].dy, 0);
 		assert.equal(divs[4].dy, 0);
+
+		component.things = [
+			{ id: 1, name: 'a' },
+			{ id: 2, name: 'b' },
+			{ id: 3, name: 'c' },
+			{ id: 4, name: 'd' },
+			{ id: 5, name: 'e' }
+		];
+
+		divs = document.querySelectorAll('div');
+		assert.equal(divs[0].dy, 120);
+		assert.equal(divs[4].dy, -120);
+
+		raf.tick(200);
+		assert.equal(divs[0].dy, 108);
+		assert.equal(divs[4].dy, -60);
+
+		raf.tick(250);
+		assert.equal(divs[0].dy, 48);
+		assert.equal(divs[4].dy, 0);
+
+		raf.tick(300);
+		assert.equal(divs[0].dy, 0);
+		assert.equal(divs[4].dy, 0);
 	}
 };
diff --git a/test/runtime/samples/each-block-keyed-bind-group/_config.js b/test/runtime/samples/each-block-keyed-bind-group/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-bind-group/_config.js
@@ -0,0 +1,65 @@
+export default {
+	html: `
+		<label><input type="checkbox" value="Vanilla"> Vanilla</label>
+		<label><input type="checkbox" value="Strawberry"> Strawberry</label>
+		<label><input type="checkbox" value="Chocolate"> Chocolate</label>
+		<label><input type="checkbox" value="Lemon"> Lemon</label>
+		<label><input type="checkbox" value="Coconut"> Coconut</label>
+	`,
+
+	async test({ assert, target, window }) {
+		const [input1, input2, input3, input4, input5] = target.querySelectorAll('input');
+		const event = new window.Event('change');
+
+		input3.checked = true;
+		await input3.dispatchEvent(event);
+
+		assert.htmlEqual(target.innerHTML, `
+			<label><input type="checkbox" value="Chocolate"> Chocolate</label>
+			<label><input type="checkbox" value="Vanilla"> Vanilla</label>
+			<label><input type="checkbox" value="Strawberry"> Strawberry</label>
+			<label><input type="checkbox" value="Lemon"> Lemon</label>
+			<label><input type="checkbox" value="Coconut"> Coconut</label>
+		`);
+
+		assert.equal(input1.checked, false);
+		assert.equal(input2.checked, false);
+		assert.equal(input3.checked, true);
+		assert.equal(input4.checked, false);
+		assert.equal(input5.checked, false);
+
+		input4.checked = true;
+		await input4.dispatchEvent(event);
+
+		assert.htmlEqual(target.innerHTML, `
+			<label><input type="checkbox" value="Chocolate"> Chocolate</label>
+			<label><input type="checkbox" value="Lemon"> Lemon</label>
+			<label><input type="checkbox" value="Vanilla"> Vanilla</label>
+			<label><input type="checkbox" value="Strawberry"> Strawberry</label>
+			<label><input type="checkbox" value="Coconut"> Coconut</label>
+		`);
+
+		assert.equal(input1.checked, false);
+		assert.equal(input2.checked, false);
+		assert.equal(input3.checked, true);
+		assert.equal(input4.checked, true);
+		assert.equal(input5.checked, false);
+
+		input3.checked = false;
+		await input3.dispatchEvent(event);
+
+		assert.htmlEqual(target.innerHTML, `
+			<label><input type="checkbox" value="Lemon"> Lemon</label>
+			<label><input type="checkbox" value="Chocolate"> Chocolate</label>
+			<label><input type="checkbox" value="Vanilla"> Vanilla</label>
+			<label><input type="checkbox" value="Strawberry"> Strawberry</label>
+			<label><input type="checkbox" value="Coconut"> Coconut</label>
+		`);
+
+		assert.equal(input1.checked, false);
+		assert.equal(input2.checked, false);
+		assert.equal(input3.checked, false);
+		assert.equal(input4.checked, true);
+		assert.equal(input5.checked, false);
+	}
+};
diff --git a/test/runtime/samples/each-block-keyed-bind-group/main.svelte b/test/runtime/samples/each-block-keyed-bind-group/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-bind-group/main.svelte
@@ -0,0 +1,21 @@
+<script>
+	let flavours = [
+		'Vanilla',
+		'Strawberry',
+		'Chocolate',
+		'Lemon',
+		'Coconut'
+	];
+
+	let choices = [];
+
+	// Put choices first by sorting
+	$: flavours = flavours.sort((a, b) => choices.includes(b) - choices.includes(a));
+</script>
+
+{#each flavours as flavour (flavour)}
+	<label>
+		<input type=checkbox bind:group={choices} value={flavour}>
+		{flavour}
+	</label>
+{/each}