diff --git a/CHANGELOG.md b/CHANGELOG.md
index c94c24dd65..74b16cc16f 100644
--- 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
index ecddaaebaa..85f252f57e 100644
--- 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
index 8894308fdf..9291f329b6 100644
--- 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
index 5a165136ce..9ab9a4395f 100644
--- 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]);
diff --git a/test/runtime/samples/binding-indirect-spread/_config.js b/test/runtime/samples/binding-indirect-spread/_config.js
new file mode 100644
index 0000000000..1f24fcdbcb
--- /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
index 0000000000..43129b08b7
--- /dev/null
+++ b/test/runtime/samples/binding-indirect-spread/main.svelte
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+