Repository: sveltejs/svelte (JavaScript)
Date: 2017-11-18 19:18:52+00:00
Task type: Bug Fix

Issue:
Bound `<select>` not initialized to proper value when using `<option>`s with implicit values
Compare [REPL](https://svelte.technology/repl?version=1.41.3&gist=1b006e3d29beeb5beee0a03b62c31912) vs [REPL](https://svelte.technology/repl?version=1.41.3&gist=ad200757e892b8397fe0661ec71fd8c7).

Svelte should probably treat a `value`-less `<option>` as being the same as one where the `value` is set to the same as its content.
Bound `<select>` not initialized to proper value when using `<option>`s with implicit values
Compare [REPL](https://svelte.technology/repl?version=1.41.3&gist=1b006e3d29beeb5beee0a03b62c31912) vs [REPL](https://svelte.technology/repl?version=1.41.3&gist=ad200757e892b8397fe0661ec71fd8c7).

Svelte should probably treat a `value`-less `<option>` as being the same as one where the `value` is set to the same as its content.


Issue discussion:
Issue found by @petterek. While this is easily worked around, it does seem to be undesirable behavior on Svelte's part.
There's actually another subtle bug here: [REPL](https://svelte.technology/repl?version=1.41.3&gist=1e5365904ab01e555858f604981342df) vs [REPL](https://svelte.technology/repl?version=1.41.3&gist=15200aec490282c72b778edb699d4d1d). In the first one, when you change the value, it's setting `typeof foo` to `string`, because it's using `option.textContent` for the value.

I think the correct thing to do here is to reuse the element's children as its `value` attribute, if it doesn't have one. Working on a PR.
Issue found by @petterek. While this is easily worked around, it does seem to be undesirable behavior on Svelte's part.
There's actually another subtle bug here: [REPL](https://svelte.technology/repl?version=1.41.3&gist=1e5365904ab01e555858f604981342df) vs [REPL](https://svelte.technology/repl?version=1.41.3&gist=15200aec490282c72b778edb699d4d1d). In the first one, when you change the value, it's setting `typeof foo` to `string`, because it's using `option.textContent` for the value.

I think the correct thing to do here is to reuse the element's children as its `value` attribute, if it doesn't have one. Working on a PR.

Solution patch:
diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts
--- a/src/generators/dom/preprocess.ts
+++ b/src/generators/dom/preprocess.ts
@@ -358,6 +358,19 @@ const preprocessors = {
 			}
 		});
 
+		const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value');
+
+		// Treat these the same way:
+		//   <option>{{foo}}</option>
+		//   <option value='{{foo}}'>{{foo}}</option>
+		if (node.name === 'option' && !valueAttribute) {
+			node.attributes.push({
+				type: 'Attribute',
+				name: 'value',
+				value: node.children
+			});
+		}
+
 		// special case — in a case like this...
 		//
 		//   <select bind:value='foo'>
@@ -369,12 +382,9 @@ const preprocessors = {
 		// so that if `foo.qux` changes, we know that we need to
 		// mark `bar` and `baz` as dirty too
 		if (node.name === 'select') {
-			const value = node.attributes.find(
-				(attribute: Node) => attribute.name === 'value'
-			);
-			if (value) {
+			if (valueAttribute) {
 				// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
-				const dependencies = block.findDependencies(value.value);
+				const dependencies = block.findDependencies(valueAttribute.value);
 				state.selectBindingDependencies = dependencies;
 				dependencies.forEach((prop: string) => {
 					generator.indirectDependencies.set(prop, new Set());
diff --git a/src/generators/dom/visitors/Element/Element.ts b/src/generators/dom/visitors/Element/Element.ts
--- a/src/generators/dom/visitors/Element/Element.ts
+++ b/src/generators/dom/visitors/Element/Element.ts
@@ -190,19 +190,6 @@ export default function visitElement(
 		visitAttributesAndAddProps();
 	}
 
-	// special case – bound <option> without a value attribute
-	if (
-		node.name === 'option' &&
-		!node.attributes.find(
-			(attribute: Node) =>
-				attribute.type === 'Attribute' && attribute.name === 'value'
-		)
-	) {
-		// TODO check it's bound
-		const statement = `${name}.__value = ${name}.textContent;`;
-		node.initialUpdate = node.lateUpdate = statement;
-	}
-
 	if (!childState.namespace && node.canUseInnerHTML && node.children.length > 0) {
 		if (node.children.length === 1 && node.children[0].type === 'Text') {
 			block.builders.create.addLine(
@@ -219,10 +206,6 @@ export default function visitElement(
 		});
 	}
 
-	if (node.lateUpdate) {
-		block.builders.update.addLine(node.lateUpdate);
-	}
-
 	if (node.name === 'select') {
 		visitAttributesAndAddProps();
 	}
diff --git a/src/generators/server-side-rendering/preprocess.ts b/src/generators/server-side-rendering/preprocess.ts
--- a/src/generators/server-side-rendering/preprocess.ts
+++ b/src/generators/server-side-rendering/preprocess.ts
@@ -69,6 +69,19 @@ const preprocessors = {
 			if (slot && isChildOfComponent(node, generator)) {
 				node.slotted = true;
 			}
+
+			// Treat these the same way:
+			//   <option>{{foo}}</option>
+			//   <option value='{{foo}}'>{{foo}}</option>
+			const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value');
+
+			if (node.name === 'option' && !valueAttribute) {
+				node.attributes.push({
+					type: 'Attribute',
+					name: 'value',
+					value: node.children
+				});
+			}
 		}
 
 		if (node.children.length) {


Test patch:
diff --git a/test/runtime/samples/binding-select-implicit-option-value/_config.js b/test/runtime/samples/binding-select-implicit-option-value/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-select-implicit-option-value/_config.js
@@ -0,0 +1,40 @@
+export default {
+	data: {
+		values: [1, 2, 3],
+		foo: 2
+	},
+
+	html: `
+		<select>
+			<option value='1'>1</option>
+			<option value='2'>2</option>
+			<option value='3'>3</option>
+		</select>
+
+		<p>foo: 2</p>
+	`,
+
+	test(assert, component, target, window) {
+		const select = target.querySelector('select');
+		const options = [...target.querySelectorAll('option')];
+
+		assert.ok(options[1].selected);
+		assert.equal(component.get('foo'), 2);
+
+		const change = new window.Event('change');
+
+		options[2].selected = true;
+		select.dispatchEvent(change);
+
+		assert.equal(component.get('foo'), 3);
+		assert.htmlEqual( target.innerHTML, `
+			<select>
+				<option value='1'>1</option>
+				<option value='2'>2</option>
+				<option value='3'>3</option>
+			</select>
+
+			<p>foo: 3</p>
+		` );
+	}
+};
diff --git a/test/runtime/samples/binding-select-implicit-option-value/main.html b/test/runtime/samples/binding-select-implicit-option-value/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-select-implicit-option-value/main.html
@@ -0,0 +1,7 @@
+<select bind:value='foo'>
+	{{#each values as v}}
+		<option>{{v}}</option>
+	{{/each}}
+</select>
+
+<p>foo: {{foo}}</p>
\ No newline at end of file
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
@@ -5,9 +5,9 @@ export default {
 		<p>selected: a</p>
 
 		<select>
-			<option>a</option>
-			<option>b</option>
-			<option>c</option>
+			<option value='a'>a</option>
+			<option value='b'>b</option>
+			<option value='c'>c</option>
 		</select>
 
 		<p>selected: a</p>
diff --git a/test/runtime/samples/binding-select-initial-value/_config.js b/test/runtime/samples/binding-select-initial-value/_config.js
--- a/test/runtime/samples/binding-select-initial-value/_config.js
+++ b/test/runtime/samples/binding-select-initial-value/_config.js
@@ -3,9 +3,9 @@ export default {
 		<p>selected: b</p>
 
 		<select>
-			<option>a</option>
-			<option>b</option>
-			<option>c</option>
+			<option value='a'>a</option>
+			<option value='b'>b</option>
+			<option value='c'>c</option>
 		</select>
 
 		<p>selected: b</p>
diff --git a/test/runtime/samples/binding-select-late/_config.js b/test/runtime/samples/binding-select-late/_config.js
--- a/test/runtime/samples/binding-select-late/_config.js
+++ b/test/runtime/samples/binding-select-late/_config.js
@@ -22,9 +22,9 @@ export default {
 
 		assert.htmlEqual( target.innerHTML, `
 			<select>
-				<option>one</option>
-				<option>two</option>
-				<option>three</option>
+				<option value='one'>one</option>
+				<option value='two'>two</option>
+				<option value='three'>three</option>
 			</select>
 			<p>selected: two</p>
 		` );
diff --git a/test/runtime/samples/binding-select/_config.js b/test/runtime/samples/binding-select/_config.js
--- a/test/runtime/samples/binding-select/_config.js
+++ b/test/runtime/samples/binding-select/_config.js
@@ -3,9 +3,9 @@ export default {
 		<p>selected: one</p>
 
 		<select>
-			<option>one</option>
-			<option>two</option>
-			<option>three</option>
+			<option value='one'>one</option>
+			<option value='two'>two</option>
+			<option value='three'>three</option>
 		</select>
 
 		<p>selected: one</p>
@@ -32,9 +32,9 @@ export default {
 			<p>selected: two</p>
 
 			<select>
-				<option>one</option>
-				<option>two</option>
-				<option>three</option>
+				<option value='one'>one</option>
+				<option value='two'>two</option>
+				<option value='three'>three</option>
 			</select>
 
 			<p>selected: two</p>