Repository: sveltejs/svelte (JavaScript)
Date: 2020-12-29 08:13:35+00:00
Task type: Bug Fix

Issue:
bind:group on nested loop not working as expected
## bind:group on input checkbox not working as expected inside nested loop

**Describe the bug**
Currently I create dynamic checkbox based on nested array. I want to make role management with just check the action (C,R,U,or D) based on module. This is the minimal example of the data
```js
[
  {
     key: "p1",
     checked: [],
     sub: [
      {
         key: "p1s1",
	checked: []
     }]
  },
 {
    key: "p2",
    checked: [],
    sub: [
     {
	key: "p1s1",
	checked: []
     }]
 }
]
```

p1 will be parent module, and p1s1 is sub module for p1. Then i loop that array to create input checkbox. For parent checkbox the behaviour is ok as expected. But for the nested one is not working as expected. I think the index of bind:group of nested checkbox is collapse with the other. 
For example:
p1s1 has checked value ["C"], when I check "R" on p2s1 (same sub index but different parent), the value of p2s1 checked is ["C","R"].

**To Reproduce**
this is the REPL link for the demo https://svelte.dev/repl/8d03a86642e449108c2dd22de7aa8cd3?version=3.31.0 
1. check p1s1 C
2. check p2s1 R
you will see, p2s1 C is checked (if you check p1s1 D first, you will see p2s1 D checked)

**Expected behavior**
only p2s1 R is checked

**Stacktraces**
If you have a stack trace to include, we recommend putting inside a `<details>` block for the sake of the thread's readability:

<details>
  <summary>Stack trace</summary>

  Stack trace goes here...
</details>

**Information about your Svelte project:**
To make your life easier, just run `npx envinfo --system --npmPackages svelte,rollup,webpack --binaries --browsers` and paste the output here.

System:
    OS: macOS 11.0.1
    CPU: (4) x64 Intel(R) Core(TM) i5-5300U CPU @ 2.30GHz
    Memory: 212.89 MB / 8.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 14.4.0 - ~/.nvm/versions/node/v14.4.0/bin/node
    Yarn: 1.22.5 - ~/.yarn/bin/yarn
    npm: 6.14.5 - ~/.nvm/versions/node/v14.4.0/bin/npm
    Watchman: 4.9.0 - /usr/local/bin/watchman
  Browsers:
    Chrome: 87.0.4280.88
    Firefox: 83.0
    Safari: 14.0.1
  npmPackages:
    rollup: ^2.3.4 => 2.33.1 
    svelte: ^3.31.0 => 3.31.0 

**Severity**
Not urgent, but is annoying

**Current Work around**
when i change bind:group on nested loop from `s.checked` to `d.sub[i].checked` this issue is gone


Issue discussion:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+* 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))
 
 ## 3.31.0
diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts
--- a/src/compiler/compile/render_dom/Block.ts
+++ b/src/compiler/compile/render_dom/Block.ts
@@ -39,6 +39,7 @@ export default class Block {
 	dependencies: Set<string> = new Set();
 
 	bindings: Map<string, Bindings>;
+	binding_group_initialised: Set<string> = new Set();
 
 	chunks: {
 		declarations: Array<Node | Node[]>;
diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts
--- a/src/compiler/compile/render_dom/Renderer.ts
+++ b/src/compiler/compile/render_dom/Renderer.ts
@@ -32,7 +32,7 @@ export default class Renderer {
 	blocks: Array<Block | Node | Node[]> = [];
 	readonly: Set<string> = new Set();
 	meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
-	binding_groups: Map<string, { binding_group: (to_reference?: boolean) => Node; is_context: boolean; contexts: string[]; index: number }> = new Map();
+	binding_groups: Map<string, { binding_group: (to_reference?: boolean) => Node; is_context: boolean; contexts: string[]; index: number; keypath: string }> = new Map();
 
 	block: Block;
 	fragment: FragmentWrapper;
diff --git a/src/compiler/compile/render_dom/wrappers/Element/Binding.ts b/src/compiler/compile/render_dom/wrappers/Element/Binding.ts
--- a/src/compiler/compile/render_dom/wrappers/Element/Binding.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/Binding.ts
@@ -116,11 +116,11 @@ export default class BindingWrapper {
 		switch (this.node.name) {
 			case 'group':
 			{
-				const { binding_group, is_context, contexts, index } = get_binding_group(parent.renderer, this.node, block);
+				const { binding_group, is_context, contexts, index, keypath } = get_binding_group(parent.renderer, this.node, block);
 
 				block.renderer.add_to_context('$$binding_groups');
 
-				if (is_context) {
+				if (is_context && !block.binding_group_initialised.has(keypath)) {
 					if (contexts.length > 1) {
 						let binding_group = x`${block.renderer.reference('$$binding_groups')}[${index}]`;
 						for (const name of contexts.slice(0, -1)) {
@@ -133,6 +133,7 @@ export default class BindingWrapper {
 					block.chunks.init.push(
 						b`${binding_group(true)} = [];`
 					);
+					block.binding_group_initialised.add(keypath);
 				}
 
 				block.chunks.hydrate.push(
@@ -257,8 +258,22 @@ function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
 	let keypath = parts.join('.');
 
 	const contexts = [];
-
+	const contextual_dependencies = new Set<string>();
+	const { template_scope } = value.expression;
+	const add_contextual_dependency = (dep: string) => {
+		contextual_dependencies.add(dep);
+		const owner = template_scope.get_owner(dep);
+		if (owner.type === 'EachBlock') {
+			for (const dep of owner.expression.contextual_dependencies) {
+				add_contextual_dependency(dep);
+			}
+		}
+	};
 	for (const dep of value.expression.contextual_dependencies) {
+		add_contextual_dependency(dep);
+	}
+
+	for (const dep of contextual_dependencies) {
 		const context = block.bindings.get(dep);
 		let key;
 		let name;
@@ -302,7 +317,8 @@ function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
 			},
 			is_context: contexts.length > 0,
 			contexts,
-			index
+			index,
+			keypath
 		});
 	}
 
diff --git a/src/compiler/compile/render_dom/wrappers/shared/mark_each_block_bindings.ts b/src/compiler/compile/render_dom/wrappers/shared/mark_each_block_bindings.ts
--- a/src/compiler/compile/render_dom/wrappers/shared/mark_each_block_bindings.ts
+++ b/src/compiler/compile/render_dom/wrappers/shared/mark_each_block_bindings.ts
@@ -17,10 +17,18 @@ export default function mark_each_block_bindings(
 	});
 
 	if (binding.name === 'group') {
+		const add_index_binding = (name: string) => {
+			const each_block = parent.node.scope.get_owner(name);
+			if (each_block.type === 'EachBlock') {
+				each_block.has_index_binding = true;
+				for (const dep of each_block.expression.contextual_dependencies) {
+					add_index_binding(dep);
+				}
+			}
+		};
 		// for `<input bind:group={} >`, we make sure that all the each blocks creates context with `index`
 		for (const name of binding.expression.contextual_dependencies) {
-			const each_block = parent.node.scope.get_owner(name);
-			(each_block as EachBlock).has_index_binding = true;
+			add_index_binding(name);
 		}
 	}
 }


Test patch:
diff --git a/test/runtime/samples/binding-input-group-each-7/_config.js b/test/runtime/samples/binding-input-group-each-7/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-group-each-7/_config.js
@@ -0,0 +1,53 @@
+export default {
+	html: `
+		<input type="checkbox" value="a" data-index="x-1">
+		<input type="checkbox" value="b" data-index="x-1">
+		<input type="checkbox" value="c" data-index="x-1">
+		<input type="checkbox" value="a" data-index="x-2">
+		<input type="checkbox" value="b" data-index="x-2">
+		<input type="checkbox" value="c" data-index="x-2">
+		<input type="checkbox" value="a" data-index="y-1">
+		<input type="checkbox" value="b" data-index="y-1">
+		<input type="checkbox" value="c" data-index="y-1">
+		<input type="checkbox" value="a" data-index="y-2">
+		<input type="checkbox" value="b" data-index="y-2">
+		<input type="checkbox" value="c" data-index="y-2">
+		<input type="checkbox" value="a" data-index="z-1">
+		<input type="checkbox" value="b" data-index="z-1">
+		<input type="checkbox" value="c" data-index="z-1">
+		<input type="checkbox" value="a" data-index="z-2">
+		<input type="checkbox" value="b" data-index="z-2">
+		<input type="checkbox" value="c" data-index="z-2">
+	`,
+
+	async test({ assert, component, target, window }) {
+		const inputs = target.querySelectorAll('input');
+		const checked = new Set();
+		const checkInbox = async (i) => {
+			checked.add(i);
+			inputs[i].checked = true;
+			await inputs[i].dispatchEvent(event);
+		};
+
+		for (let i = 0; i < 18; i++) {
+			assert.equal(inputs[i].checked, checked.has(i));
+		}
+
+		const event = new window.Event('change');
+
+		await checkInbox(2);
+		for (let i = 0; i < 18; i++) {
+			assert.equal(inputs[i].checked, checked.has(i));
+		}
+
+		await checkInbox(12);
+		for (let i = 0; i < 18; i++) {
+			assert.equal(inputs[i].checked, checked.has(i));
+		}
+
+		await checkInbox(8);
+		for (let i = 0; i < 18; i++) {
+			assert.equal(inputs[i].checked, checked.has(i));
+		}
+	}
+};
diff --git a/test/runtime/samples/binding-input-group-each-7/main.svelte b/test/runtime/samples/binding-input-group-each-7/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-group-each-7/main.svelte
@@ -0,0 +1,15 @@
+<script>
+	const list = [
+		{ id: 'x', data: [{ id: 1, data: [] }, { id: 2, data: [] }] },
+		{ id: 'y', data: [{ id: 1, data: [] }, { id: 2, data: [] }] },
+		{ id: 'z', data: [{ id: 1, data: [] }, { id: 2, data: [] }] }
+	];
+</script>
+
+{#each list as { id, data }}
+	{#each data as item}
+		<input type="checkbox" bind:group={item.data} value="a" data-index="{id}-{item.id}" />
+		<input type="checkbox" bind:group={item.data} value="b" data-index="{id}-{item.id}" />
+		<input type="checkbox" bind:group={item.data} value="c" data-index="{id}-{item.id}" />
+	{/each}
+{/each}