Repository: sveltejs/svelte (JavaScript)
Date: 2019-09-08 08:27:12+00:00
Task type: Bug Fix

Issue:
Store binding inside each is broken
**Describe the bug**

This doesn't work:

```svelte
{#each $items as item, index}
  <input bind:value={item.text}>
{/each}
```

**To Reproduce**

https://svelte.dev/repl/358a3620fd6b4f159a90268a6b2035a4?version=3.9.1

**Expected behavior**

Editing the text input should update the store

**Severity**

Seems bad

**Additional context**

https://stackoverflow.com/questions/57626273/svelte-component-not-re-rendering-with-new-values-if-text-input-is-inside-an-eac



Issue discussion:
As a workaround, this works:
```
<input bind:value={$items[index].text}>
```

Might help track down how to correct it.

Solution patch:
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
@@ -10,7 +10,7 @@ export interface BlockOptions {
 	renderer?: Renderer;
 	comment?: string;
 	key?: string;
-	bindings?: Map<string, { object: string; property: string; snippet: string }>;
+	bindings?: Map<string, { object: string; property: string; snippet: string; store: string; tail: string }>;
 	dependencies?: Set<string>;
 }
 
@@ -27,7 +27,7 @@ export default class Block {
 
 	dependencies: Set<string>;
 
-	bindings: Map<string, { object: string; property: string; snippet: string }>;
+	bindings: Map<string, { object: string; property: string; snippet: string; store: string; tail: string }>;
 
 	builders: {
 		init: CodeBuilder;
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
@@ -121,11 +121,19 @@ export default class EachBlockWrapper extends Wrapper {
 			view_length: fixed_length === null ? `${iterations}.[✂${c}-${c+4}✂]` : fixed_length
 		};
 
+		const store = 
+			node.expression.node.type === 'Identifier' && 
+			node.expression.node.name[0] === '$'
+				? node.expression.node.name.slice(1)
+				: null;
+
 		node.contexts.forEach(prop => {
 			this.block.bindings.set(prop.key.name, {
 				object: this.vars.each_block_value,
 				property: this.index_name,
-				snippet: attach_head(`${this.vars.each_block_value}[${this.index_name}]`, prop.tail)
+				snippet: attach_head(`${this.vars.each_block_value}[${this.index_name}]`, prop.tail),
+				store,
+				tail: attach_head(`[${this.index_name}]`, prop.tail)
 			});
 		});
 
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
@@ -249,7 +249,7 @@ function get_event_handler(
 		snippet?: string;
 	} {
 	const value = get_value_from_dom(renderer, binding.parent, binding);
-	const store = binding.object[0] === '$' ? binding.object.slice(1) : null;
+	let store = binding.object[0] === '$' ? binding.object.slice(1) : null;
 
 	let tail = '';
 	if (binding.node.expression.node.type === 'MemberExpression') {
@@ -258,7 +258,13 @@ function get_event_handler(
 	}
 
 	if (binding.node.is_contextual) {
-		const { object, property, snippet } = block.bindings.get(name);
+		const binding = block.bindings.get(name);
+		const { object, property, snippet } = binding;
+
+		if (binding.store) {
+			store = binding.store;
+			tail = `${binding.tail}${tail}`;
+		}
 
 		return {
 			uses_context: true,


Test patch:
diff --git a/test/runtime/samples/store-each-binding-destructuring/_config.js b/test/runtime/samples/store-each-binding-destructuring/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-each-binding-destructuring/_config.js
@@ -0,0 +1,14 @@
+export default {
+	async test({ assert, target, window }) {
+		const input = target.querySelector('input');
+
+		const event = new window.Event('input');
+		input.value = 'changed';
+		await input.dispatchEvent(event);
+
+		assert.htmlEqual(target.innerHTML, `
+			<input>
+			<p>changed</p>
+		`);
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/store-each-binding-destructuring/main.svelte b/test/runtime/samples/store-each-binding-destructuring/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-each-binding-destructuring/main.svelte
@@ -0,0 +1,13 @@
+<script>
+	import { writable } from 'svelte/store';
+
+	const items = writable([
+		 { id: 0, text: 'initial' }
+	]);
+</script>
+
+{#each $items as { text }}
+	<input bind:value={text}>
+{/each}
+
+<p>{$items[0].text}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/store-each-binding/_config.js b/test/runtime/samples/store-each-binding/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-each-binding/_config.js
@@ -0,0 +1,14 @@
+export default {
+	async test({ assert, target, window }) {
+		const input = target.querySelector('input');
+
+		const event = new window.Event('input');
+		input.value = 'changed';
+		await input.dispatchEvent(event);
+
+		assert.htmlEqual(target.innerHTML, `
+			<input>
+			<p>changed</p>
+		`);
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/store-each-binding/main.svelte b/test/runtime/samples/store-each-binding/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-each-binding/main.svelte
@@ -0,0 +1,13 @@
+<script>
+	import { writable } from 'svelte/store';
+
+	const items = writable([
+		 { id: 0, text: 'initial' }
+	]);
+</script>
+
+{#each $items as item}
+	<input bind:value={item.text}>
+{/each}
+
+<p>{$items[0].text}</p>
\ No newline at end of file