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 @@ + + + + + + + + +