Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
3.0.0-alpha18: select value binding against a store does not update all subscriptions
Hi,

I'm trying to set a store value through a value binding of select input. After changing the select, the value get's updated correctly in the same component where the select is, but other components that may be subscribed to the same writable are not getting update.

In contrast, if I exercise the writable.set() API to programmatically set the value of the writable store, the value updates correctly in all components that are subscribed.

I have made a repro in the REPL. 
https://v3.svelte.technology/repl?version=3.0.0-alpha18&gist=17ec1020c2a1750931f4415883bbbe32

Cheers!

Germán

App.html
```
<script>  
  import { currentLang } from './stores.js';
</script>

Selected lang: {$currentLang}
<select bind:value={$currentLang}>
	{#each langs as lang (lang.value) }
		<option value={lang.value}>{lang.text}</option>
	{/each}	
</select>

```

Nav.html
```
<script>  
  import { currentLang } from './stores.js';
</script>

<nav style="background-color: #eee">	
	Current lang <strong>{$currentLang}</strong>	
</nav>

```



Discussion:
I think I'm running into the same issue, but I'm seeing it with a smaller repro:

```html
<script>
    import { writable, derive } from "svelte/store";
    
    const a = writable(1);
    const b = writable(2);
    const c = derive([ a, b ], ([ a, b ]) => a + b);
</script>

a: <input bind:value={$a}/>
<hr />
{$a} + {$b} = {$c}
```

The value in the DOM for `$a` gets updated, but `$c` never seems to re-trigger.
Editing the input field is directly updating the `$a` variable which is used to hold the current store value - but without actually updating the store.

```javascript
	function input_input_handler() {
		$a = this.value;
		$$invalidate('$a', $a);
	}
```

So `{$a}` in the DOM updates because that value has been updated, but `{$c}` isn't updated because the `a` _store_ is unchanged.

It should hopefully be pretty simple to instead have the event handler run `a.set(this.value);` (and then I think we probably don't even need to run `$$invalidate()`). But this does sort of bring up a philosophical point: Up til now, everything in Svelte that consumes stores only assumes they have a `.subscribe()` method. We need to assume something more if we want to be able to bind an input to it. In this one instance do we also assume that the store you use has a `.set()` method?
It *feels* like the wrong move - it wouldn't work with `spring`, for example. But it is inconvenient at the moment. Not sure.

At the very least there should be an error 
This can currently be worked around by manually implementing the two halves of the two-way binding. In the first example `<select value={$currentLang} on:change='{() => currentLang.set(this.value)}'>` and in the second example `<input value={$a} on:input='{() => a.set(this.value)}'/>`. This may become the official way to do this, if we decide to disallow two-way binding with stores at compile time.
I guess store values are already special in the sense that they are $ prefixed in the templates vs regular values. I don't think teaching an extra pattern here will hurt too much.

Anyway, totally enjoying using svelte v3 in a poc app. Thanks! 👍


Ah, that explanation for why it doesn't work makes sense but it feels like a weird line in the sand to draw/missed opportunity though.

If template code can read from store variables like any other variable, why shouldn't they be able to write to them as well?
FWIW I think I would prefer to have a dev mode check for `set` and warning if it's missing compiled in, and just let non-dev fail when trying to `set`. This is one of those places that it would be awesome to have typechecked, but that's not really feasible right now. I don't think trying to set a spring or tween would happen much in practice, but glancing over the code, it doesn't _look_ like it would be a fatal mistake.

I would be a little more surprised that bind didn't work at all with stores, especially since it's basically just shorthand for the attr setting and event handler anyways.

Resolving patch:
diff --git a/src/compile/render-dom/wrappers/Element/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding.ts
--- a/src/compile/render-dom/wrappers/Element/Binding.ts
+++ b/src/compile/render-dom/wrappers/Element/Binding.ts
@@ -221,6 +221,17 @@ function getEventHandler(
 	snippet: string
 ) {
 	const value = getValueFromDom(renderer, binding.parent, binding);
+	const store = binding.object[0] === '$' ? binding.object.slice(1) : null;
+
+	if (store && binding.node.expression.node.type === 'MemberExpression') {
+		// TODO is there a way around this? Mutating an object doesn't work,
+		// because stores check for referential equality (i.e. immutable data).
+		// But we can't safely or easily clone objects. So for now, we bail
+		renderer.component.error(binding.node.expression.node.property, {
+			code: 'invalid-store-binding',
+			message: 'Cannot bind to a nested property of a store'
+		});
+	}
 
 	if (binding.node.isContextual) {
 		let tail = '';
@@ -233,15 +244,21 @@ function getEventHandler(
 
 		return {
 			usesContext: true,
-			mutation: `${snippet}${tail} = ${value};`,
+			mutation: store
+				? `${store}.set(${value});`
+				: `${snippet}${tail} = ${value};`,
 			contextual_dependencies: new Set([object, property])
 		};
 	}
 
+	const mutation = store
+		? `${store}.set(${value});`
+		: `${snippet} = ${value};`;
+
 	if (binding.node.expression.node.type === 'MemberExpression') {
 		return {
 			usesContext: binding.node.expression.usesContext,
-			mutation: `${snippet} = ${value};`,
+			mutation,
 			contextual_dependencies: binding.node.expression.contextual_dependencies,
 			snippet
 		};
@@ -249,7 +266,7 @@ function getEventHandler(
 
 	return {
 		usesContext: false,
-		mutation: `${snippet} = ${value};`,
+		mutation,
 		contextual_dependencies: new Set()
 	};
 }
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
@@ -456,7 +456,7 @@ export default class ElementWrapper extends Wrapper {
 			this.renderer.component.partly_hoisted.push(deindent`
 				function ${handler}(${contextual_dependencies.size > 0 ? `{ ${[...contextual_dependencies].join(', ')} }` : ``}) {
 					${group.bindings.map(b => b.handler.mutation)}
-					${Array.from(dependencies).map(dep => `$$invalidate('${dep}', ${dep});`)}
+					${Array.from(dependencies).filter(dep => dep[0] !== '$').map(dep => `$$invalidate('${dep}', ${dep});`)}
 				}
 			`);
 


Test patch:
diff --git a/test/runtime/samples/binding-store-deep/_config.js b/test/runtime/samples/binding-store-deep/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-store-deep/_config.js
@@ -0,0 +1,5 @@
+export default {
+	error(assert, err) {
+		assert.equal(err.message, `Cannot bind to a nested property of a store`);
+	}
+};
diff --git a/test/runtime/samples/binding-store-deep/main.html b/test/runtime/samples/binding-store-deep/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-store-deep/main.html
@@ -0,0 +1,8 @@
+<script>
+	import { writable } from 'svelte/store';
+
+	export const user = writable({ name: 'world' });
+</script>
+
+<input bind:value={$user.name}>
+<p>hello {$user.name}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/binding-store/_config.js b/test/runtime/samples/binding-store/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-store/_config.js
@@ -0,0 +1,41 @@
+export default {
+	html: `
+		<input>
+		<p>hello world</p>
+	`,
+
+	ssrHtml: `
+		<input value="world">
+		<p>hello world</p>
+	`,
+
+	async test({ assert, component, target, window }) {
+		const input = target.querySelector('input');
+		assert.equal(input.value, 'world');
+
+		const event = new window.Event('input');
+
+		const names = [];
+		const unsubscribe = component.name.subscribe(name => {
+			names.push(name);
+		});
+
+		input.value = 'everybody';
+		await input.dispatchEvent(event);
+
+		assert.htmlEqual(target.innerHTML, `
+			<input>
+			<p>hello everybody</p>
+		`);
+
+		await component.name.set('goodbye');
+		assert.equal(input.value, 'goodbye');
+		assert.htmlEqual(target.innerHTML, `
+			<input>
+			<p>hello goodbye</p>
+		`);
+
+		assert.deepEqual(names, ['world', 'everybody', 'goodbye']);
+		unsubscribe();
+	},
+};
diff --git a/test/runtime/samples/binding-store/main.html b/test/runtime/samples/binding-store/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-store/main.html
@@ -0,0 +1,8 @@
+<script>
+	import { writable } from 'svelte/store';
+
+	export const name = writable('world');
+</script>
+
+<input bind:value={$name}>
+<p>hello {$name}</p>
\ No newline at end of file