Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
Radio/checkbox input with bind:group and spread props makes variable undefined
**Describe the bug**
When using an input (`type="radio"` or `type="checkbox"`) with `bind:group` in combination with spread properties, it sets the bound variable to undefined when clicked.

**To Reproduce**
https://svelte.dev/repl/eded09217d054d5cb518712f08833437?version=3.12.1

**Severity**
Semi-minor inconvenience. Having to specify each prop now instead of using a spread. And not being able to share props easily.



Discussion:
I think this may be a (slightly more convoluted) manifestation of the same issue as #3764. The presence of the spread is preventing the attributes from being set the way they normally would - which in this case includes the special `__value` property that Svelte then tries to refer to later.
#3764 has been fixed, but, as I noted in a comment there, this is actually a slightly different issue. In situations like this with indirectly bound values, we need to always set the custom `__value` property on the node. Easiest would probably be to expose the `is_indirectly_bound_value` value as a method on `AttributeWrapper`, and to refer to that in `ElementWrapper#add_spread_attributes()` and emit that code as a special case. I'm not sure where relative to the other updates this should go. It might be that, for correctness, we need to do it inline with the one big `set_attributes()` call, and that that needs to have a check that always sets `__value` as a property, even though it's not present on the prototype.

cc @mrkishi 

Resolving patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Svelte changelog
 
+## Unreleased
+
+* Fix indirect bindings involving elements with spreads ([#3680](https://github.com/sveltejs/svelte/issues/3680))
+
 ## 3.18.2
 
 * Fix binding to module-level variables ([#4086](https://github.com/sveltejs/svelte/issues/4086))
diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
--- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
@@ -39,20 +39,25 @@ export default class AttributeWrapper {
 		}
 	}
 
-	render(block: Block) {
+	is_indirectly_bound_value() {
 		const element = this.parent;
 		const name = fix_attribute_casing(this.node.name);
-
-		const metadata = this.get_metadata();
-
-		const is_indirectly_bound_value =
-			name === 'value' &&
+		return name === 'value' &&
 			(element.node.name === 'option' || // TODO check it's actually bound
 				(element.node.name === 'input' &&
-					element.node.bindings.find(
+					element.node.bindings.some(
 						(binding) =>
 							/checked|group/.test(binding.name)
 					)));
+	}
+
+	render(block: Block) {
+		const element = this.parent;
+		const name = fix_attribute_casing(this.node.name);
+
+		const metadata = this.get_metadata();
+
+		const is_indirectly_bound_value = this.is_indirectly_bound_value();
 
 		const property_name = is_indirectly_bound_value
 			? '__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
@@ -679,10 +679,10 @@ export default class ElementWrapper extends Wrapper {
 					updates.push(condition ? x`${condition} && ${snippet}` : snippet);
 				} else {
 					const metadata = attr.get_metadata();
-					const snippet = x`{ ${
-						(metadata && metadata.property_name) ||
-						fix_attribute_casing(attr.node.name)
-					}: ${attr.get_value(block)} }`;
+					const name = attr.is_indirectly_bound_value()
+						? '__value'
+						: (metadata && metadata.property_name) || fix_attribute_casing(attr.node.name);
+					const snippet = x`{ ${name}: ${attr.get_value(block)} }`;
 					initial_props.push(snippet);
 
 					updates.push(condition ? x`${condition} && ${snippet}` : snippet);
diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts
--- a/src/runtime/internal/dom.ts
+++ b/src/runtime/internal/dom.ts
@@ -98,7 +98,7 @@ export function set_attributes(node: Element & ElementCSSInlineStyle, attributes
 			node.removeAttribute(key);
 		} else if (key === 'style') {
 			node.style.cssText = attributes[key];
-		} else if (descriptors[key] && descriptors[key].set) {
+		} else if (key === '__value' || descriptors[key] && descriptors[key].set) {
 			node[key] = attributes[key];
 		} else {
 			attr(node, key, attributes[key]);


Test patch:
diff --git a/test/runtime/samples/binding-indirect-spread/_config.js b/test/runtime/samples/binding-indirect-spread/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-indirect-spread/_config.js
@@ -0,0 +1,44 @@
+export default {
+	skip_if_ssr: true,
+	async test({ assert, component, target, window }) {
+		const event = new window.MouseEvent('click');
+
+		const [radio1, radio2, radio3] = target.querySelectorAll('input[type=radio]');
+
+		assert.ok(!radio1.checked);
+		assert.ok(radio2.checked);
+		assert.ok(!radio3.checked);
+
+		component.radio = 'radio1';
+
+		assert.ok(radio1.checked);
+		assert.ok(!radio2.checked);
+		assert.ok(!radio3.checked);
+
+		await radio3.dispatchEvent(event);
+
+		assert.equal(component.radio, 'radio3');
+		assert.ok(!radio1.checked);
+		assert.ok(!radio2.checked);
+		assert.ok(radio3.checked);
+
+		const [check1, check2, check3] = target.querySelectorAll('input[type=checkbox]');
+
+		assert.ok(!check1.checked);
+		assert.ok(check2.checked);
+		assert.ok(!check3.checked);
+
+		component.check = ['check1', 'check2'];
+
+		assert.ok(check1.checked);
+		assert.ok(check2.checked);
+		assert.ok(!check3.checked);
+
+		await check3.dispatchEvent(event);
+
+		assert.deepEqual(component.check, ['check1', 'check2', 'check3']);
+		assert.ok(check1.checked);
+		assert.ok(check2.checked);
+		assert.ok(check3.checked);
+	}
+};
diff --git a/test/runtime/samples/binding-indirect-spread/main.svelte b/test/runtime/samples/binding-indirect-spread/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-indirect-spread/main.svelte
@@ -0,0 +1,12 @@
+<script>
+	export let radio = 'radio2';
+	export let check = ['check2'];
+</script>
+
+<input type='radio' bind:group={radio} value='radio1' {...{}}>
+<input type='radio' bind:group={radio} value='radio2' {...{}}>
+<input type='radio' bind:group={radio} value='radio3' {...{}}>
+
+<input type='checkbox' bind:group={check} value='check1' {...{}}>
+<input type='checkbox' bind:group={check} value='check2' {...{}}>
+<input type='checkbox' bind:group={check} value='check3' {...{}}>