Repository: sveltejs/svelte (JavaScript)
Date: 2021-04-06 23:46:38+00:00
Task type: Bug Fix

Issue:
First option in select dropdown appears selected when no option matches the bound value
**Describe the bug**
When a select dropdown value is bound to a component variable, and this variable doesn't match any of the options in the dropdown, Svelte shows the first option as selected rather than blank. This causes bugs/unexpected behavior when the form is submitted (the submitted value doesn't match what the user sees).

**To Reproduce**
https://svelte.dev/repl/a7fdeec83c504c00b9706edc679330c6?version=3.35.0

**Expected behavior**
No option should be selected when none of the options match the bound value.

Uncomment the two `setTimeout` calls in the above REPL to see the expected behavior.

**Information about your Svelte project:**

- Browser: Chromium 89, Firefox 87
- Operating system: Windows 10
- Svelte version: 3.35.0

**Severity**
I'm currently using Angular and would like to use Svelte instead. For my app it is important that select dropdowns be blank when none of the options match the bound value.


Issue discussion:
>  No option should be selected when none of the options match the bound value.

> For my app it is important that select dropdowns be blank when none of the options match the bound value.

Does this even work in plain HTML? That's why every `<select>` ever has an option with an empty value that says "Please select a value...". I don't think HTML has a concept of not having an option selected. How would that be rendered?

Example:

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select

![Selection_841](https://user-images.githubusercontent.com/679144/112596669-69295400-8e0c-11eb-909a-456231b9e473.png)

Edit: here's another point to think about: How would a user _undo_ their selection (go back to the no-selected state) without having an explicit empty `<option>`? So even if a `<select>` would by default not show any of the options it would be painful ux if a user could never go back to the state if the selection is optional.
@Prinzhorn Thank you for the reply and thoughtful questions.

For single select elements in plain HTML, I think you're right that there isn't a concept of not having an option selected. However, in plain HTML there also isn't a concept of binding a select value to an object, `null`, or other non-string value as can be done in Svelte.

With a plain HTML form where the page is reloaded on submission, the submitted value always matches that of the selected option. But this expectation is broken in Svelte if an option is shown as selected when it doesn't match the bound value.

> How would a user undo their selection (go back to the no-selected state) without having an explicit empty `<option>`? So even if a `<select>` would by default not show any of the options it would be painful ux if a user could never go back to the state if the selection is optional.

For optional dropdowns, you are correct that an explicit empty option is needed. For such forms it works fine to add a `<option value={null}></option>` element and set the variable bound to the select value back to `null` after the form is submitted. However, this doesn't work for required dropdowns, where the `required` attribute should prevent the form from being submitted if the user hasn't selected an option. Because an option with a value of `{null}` ends up with the string "null" in its value property, the browser won't prevent users from submitting the form with that option selected.

It seems like the only way I can get close to the expected behavior for required dropdowns is to add `<option value hidden></option>` to every required select element, and then set the bound value to an empty string initially and after the form is submitted. I can't find a way to make it work with `null` as the bound value.

Adding an extra option element and binding to a blank string seems a bit like a hacky workaround, though. My expectation is that when using a two-way binding Svelte would automatically ensure that an option is only selected if it matches the bound select value.
I think I misunderstood the original issue and the discussion was missing the point. In HTML there is no `selectedIndex = -1`, you cannot have a "no-selection" state with plain HTML. But you can with JavaScript.

So to rephrase your feature request: you want `selectedIndex = -1` if none of the `option[value]` matches the `bind:value`. I still think that's odd. Because the state is still inconsistent: your `value` contains something but the select is empty (user doesn't know your state is not empty though)

Here's what I would do, which matches how basically every select I've ever encountered worked:

https://svelte.dev/repl/1cdd3422d8ef4a6eb12ddf0b25be6e82?version=3.35.0

Edit: I personally would be very cautions with binding objects and stick to your `id` field. If through some functionality later you update one of the objects to a different one (with the same id) you end up in an inconsistent state. If you have an `id` I personally would always use it and have a `selectedId`. But that depends entirely on your use-case, I just play safe and I hate subtle bugs.

Edit2: Also regarding the `required` attribute: if you don't play by the rules of HTML (e.g. binding objects) than don't expect HTML features to work. If you go that route you need a custom validation logic.

Solution patch:
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
@@ -221,6 +221,8 @@ export function select_option(select, value) {
 			return;
 		}
 	}
+
+	select.selectedIndex = -1; // no option should be selected
 }
 
 export function select_options(select, value) {


Test patch:
diff --git a/test/runtime/samples/binding-select-initial-value-undefined/_config.js b/test/runtime/samples/binding-select-initial-value-undefined/_config.js
--- a/test/runtime/samples/binding-select-initial-value-undefined/_config.js
+++ b/test/runtime/samples/binding-select-initial-value-undefined/_config.js
@@ -14,6 +14,7 @@ export default {
 	`,
 
 	test({ assert, component, target }) {
+		assert.equal(component.selected, 'a');
 		const select = target.querySelector('select');
 		const options = [...target.querySelectorAll('option')];
 
diff --git a/test/runtime/samples/binding-select-unmatched/_config.js b/test/runtime/samples/binding-select-unmatched/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-select-unmatched/_config.js
@@ -0,0 +1,60 @@
+export default {
+	html: `
+		<p>selected: null</p>
+
+		<select>
+			<option value='a'>a</option>
+			<option value='b'>b</option>
+			<option value='c'>c</option>
+		</select>
+
+		<p>selected: null</p>
+	`,
+
+	async test({ assert, component, target }) {
+		const select = target.querySelector('select');
+		const options = [...target.querySelectorAll('option')];
+
+		assert.equal(component.selected, null);
+
+		// no option should be selected since none of the options matches the bound value
+		assert.equal(select.value, '');
+		assert.equal(select.selectedIndex, -1);
+		assert.ok(!options[0].selected);
+
+		component.selected = 'a'; // first option should now be selected
+		assert.equal(select.value, 'a');
+		assert.ok(options[0].selected);
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>selected: a</p>
+
+			<select>
+				<option value='a'>a</option>
+				<option value='b'>b</option>
+				<option value='c'>c</option>
+			</select>
+
+			<p>selected: a</p>
+		`);
+
+		component.selected = 'd'; // doesn't match an option
+
+		// now no option should be selected again
+		assert.equal(select.value, '');
+		assert.equal(select.selectedIndex, -1);
+		assert.ok(!options[0].selected);
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>selected: d</p>
+
+			<select>
+				<option value='a'>a</option>
+				<option value='b'>b</option>
+				<option value='c'>c</option>
+			</select>
+
+			<p>selected: d</p>
+		`);
+	}
+};
diff --git a/test/runtime/samples/binding-select-unmatched/main.svelte b/test/runtime/samples/binding-select-unmatched/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-select-unmatched/main.svelte
@@ -0,0 +1,14 @@
+<script>
+	// set as null so no option will be selected by default
+	export let selected = null;
+</script>
+
+<p>selected: {selected}</p>
+
+<select bind:value={selected}>
+	<option>a</option>
+	<option>b</option>
+	<option>c</option>
+</select>
+
+<p>selected: {selected}</p>
\ No newline at end of file