Repository: sveltejs/svelte (JavaScript)
Date: 2019-10-11 14:37:44+00:00
Task type: Bug Fix

Issue:
Spread operator doesn't work with "form" and "list" attributes
**Describe the bug**
I'll try set `list` and `form` with spread operator:
```js
let [type, form, list] = ['text', 'myID', 'listID'];
let props = {type, form, list};
```

It works in regular case:
```html
<input type={type} form={form} list={list}>
```

But fails with spread operator:
```html
<input {...props}>
```

**Logs**
Firefox: `setting getter-only property "form"`
Chrome: `Cannot assign to read only property 'form' of object '#<HTMLInputElement>'`

And if we remove "form" attribute:
Firefox: `setting getter-only property "list"`
Chrome: `Cannot assign to read only property 'list' of object '#<HTMLInputElement>'`

**To Reproduce**
[REPL](https://svelte.dev/repl/21720817a9464905934d7fbfe8216739?version=3.12.1)




Issue discussion:
This looks like another manifestation for an overarching problem which might not actually have its own issue (just a few related open issues) - namely, that we don't know whether each prop that's getting spread should be set as an attribute or as a property on the element. For non-spread props, we can look this stuff up at compile time, and just emit the correct code. I don't think we have a really good way to handle this for spread props (where this needs to be determined at runtime) without shipping a big list of properties vs attributes in the compiled code, which we'd really like to avoid doing.

The helper we're using right now for this is:

```typescript
export function set_attributes(node: Element & ElementCSSInlineStyle, attributes: { [x: string]: string }) {
	for (const key in attributes) {
		if (key === 'style') {
			node.style.cssText = attributes[key];
		} else if (key in node) {
			node[key] = attributes[key];
		} else {
			attr(node, key, attributes[key]);
		}
	}
}
```

We set an attribute if one is available on the DOM object, otherwise we use an attribute. This breaks when it's a read-only property. It also doesn't handle when the property has a different name than the attribute, apart from the one special case of `style`.

I recall it being suggested somewhere I can't find that one possibility would be to fall back to setting it as an attribute if `node[key] = attributes[key];` fails. This might be the best we can reasonably do. This definitely is an area that needs work.
A good idea from @pngwn: Check for `Object.getOwnPropertyDescriptor(node.__proto__, key).set` rather than `key in node`. These read-only properties don't actually have setters, which we can check for at runtime. Actually, we should probably do `Object.getOwnPropertyDescriptors(node.__proto__)` once at the beginning, and then do the lookups in that, which I imagine would be faster.

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
 * Fix tracking of dependencies of compound assignments in reactive statements ([#3634](https://github.com/sveltejs/svelte/issues/3634))
 * Flush changes in newly attached block when using `{#await}` ([#3660](https://github.com/sveltejs/svelte/issues/3660))
 * Throw exception immediately when calling `createEventDispatcher()` after component instantiation ([#3667](https://github.com/sveltejs/svelte/pull/3667))
+* Fix error resulting from trying to set a read-only property when spreading element attributes ([#3681](https://github.com/sveltejs/svelte/issues/3681))
 
 ## 3.12.1
 
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
@@ -90,10 +90,12 @@ export function attr(node: Element, attribute: string, value?: string) {
 }
 
 export function set_attributes(node: Element & ElementCSSInlineStyle, attributes: { [x: string]: string }) {
+	// @ts-ignore
+	const descriptors = Object.getOwnPropertyDescriptors(node.__proto__);
 	for (const key in attributes) {
 		if (key === 'style') {
 			node.style.cssText = attributes[key];
-		} else if (key in node) {
+		} else if (descriptors[key] && descriptors[key].set) {
 			node[key] = attributes[key];
 		} else {
 			attr(node, key, attributes[key]);


Test patch:
diff --git a/test/runtime/samples/spread-element-readonly/_config.js b/test/runtime/samples/spread-element-readonly/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/spread-element-readonly/_config.js
@@ -0,0 +1,9 @@
+export default {
+	skip_if_ssr: true, // DOM and SSR output is different, a separate SSR test exists
+	html: `<input form="qux" list="quu" />`,
+
+	test({ assert, target }) {
+		const div = target.querySelector('input');
+		assert.equal(div.value, 'bar');
+	}
+};
diff --git a/test/runtime/samples/spread-element-readonly/main.svelte b/test/runtime/samples/spread-element-readonly/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/spread-element-readonly/main.svelte
@@ -0,0 +1,9 @@
+<script>
+	let props = {
+		value: 'bar',
+		form: 'qux',
+		list: 'quu',
+	};
+</script>
+
+<input {...props} />
diff --git a/test/server-side-rendering/samples/spread-attributes/_expected.html b/test/server-side-rendering/samples/spread-attributes/_expected.html
new file mode 100644
--- /dev/null
+++ b/test/server-side-rendering/samples/spread-attributes/_expected.html
@@ -0,0 +1 @@
+<input value="bar" form="qux" list="quu" />
diff --git a/test/server-side-rendering/samples/spread-attributes/main.svelte b/test/server-side-rendering/samples/spread-attributes/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/server-side-rendering/samples/spread-attributes/main.svelte
@@ -0,0 +1,9 @@
+<script>
+	let props = {
+		value: 'bar',
+		form: 'qux',
+		list: 'quu',
+	};
+</script>
+
+<input {...props} />