Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
Initial Data for Bound Required Input Fields is 'undefined'
I believe this is  a bug, but it may be a request for more documentation. Apologies if I'm posting this in the wrong spot.

Two-way binding an input field causes browsers to display the text `'undefined'`.

In the [Two-Way Binding](https://svelte.technology/guide#two-way-binding) section of the documentation, readers are provided [an example of form handling in Svelte in the REPL](https://svelte.technology/repl?version=1.57.3&data=eyJnaXN0IjpudWxsLCJjb21wb25lbnRzIjpbeyJuYW1lIjoiQXBwIiwic291cmNlIjoiPGZvcm0gb246c3VibWl0PSdoYW5kbGVTdWJtaXQoIGV2ZW50ICknPlxuXHQ8aW5wdXQgYmluZDp2YWx1ZT0ndGVzdCcgdHlwZT0ndGV4dCc%2BXG5cdDxidXR0b24gdHlwZT0nc3VibWl0Jz5TdG9yZTwvYnV0dG9uPlxuPC9mb3JtPlxuXG48c2NyaXB0PlxuZXhwb3J0IGRlZmF1bHQge1xuXHRtZXRob2RzOiB7XG5cdFx0aGFuZGxlU3VibWl0KGV2ZW50KSB7XG5cdFx0XHQvLyBwcmV2ZW50IHRoZSBwYWdlIGZyb20gcmVsb2FkaW5nXG5cdFx0XHRldmVudC5wcmV2ZW50RGVmYXVsdCgpO1xuXG5cdFx0XHR2YXIgdmFsdWUgPSB0aGlzLmdldCgndGVzdCcpO1xuXHRcdFx0Y29uc29sZS5sb2coJ3ZhbHVlJywgdmFsdWUpO1xuXHRcdH1cblx0fVxufTtcbjwvc2NyaXB0PlxuIn1dLCJkYXRhIjp7fX0%3D). In the example, the input field's initial value is `'undefined'`.

I sought to work around the problem by providing default data (in this case, an empty string). Unfortunately, this causes a different problem: Firefox sets input fields to the `:invalid` style state when JavaScript sets the value to `''` on required field, or if the string does not validate (such as with `type="email"`). You can see this [in this REPL (you will need to use Firefox to see the error)](https://svelte.technology/repl?version=1.57.2&gist=a66e8c1a0609ee57bd44202865103006).

I believe this is a bug: I expect an undefined variable in a two-way binding scenario not to write `'undefined'` to the field it is found to. Using default data is not desirable because of Firefox's behavior (leads to surprising UX). Workarounds involve losing HTML5 attributes (`required` and `type="email"`) or else avoiding the two-way binding, both of which require more work on the part of the developer using Svelte.


Discussion:
Ah, interesting. When this has come up previously we've decided that setting text fields to `"undefined"` is a) technically the correct behaviour and b) helpful insofar as it encourages people to initialize data (which is a good practice). But Firefox's behaviour changes the equation. I think I'd be on board with not setting `"undefined"` values.

Question is whether we should consider this a breaking change for semver purposes, or a bugfix?
Thanks for taking a look at this.

If this was a behavior that was previously agreed upon, then I suspect this counts as a breaking change, as much as I'd rather it count as a bugfix. 😒

I'm afraid I'm missing some context here: why would one wish to enforce initial data on a form? In particular, I'm building an authentication form (and assuming a new or unrecognized user). The extra code to set the fields to `''` feels unnecessary to me (as there is no intended change in browser behavior), but I am brand new to Svelte.

I really enjoying Svelte, FWIW. Thanks for putting this all together!
Dropped the ball on this (sorry) but adding an updated REPL link: https://svelte.dev/repl?version=3.0.0-beta.28&gist=450f0a76e46ca4b2eb38a00d1c6ca7bf

I think we'll be able to get away with calling this a bugfix (i.e. semver minor/patch) since it's hard to imagine someone relying on an `<input>` displaying `undefined`.

Resolving patch:
diff --git a/src/compiler/compile/render_dom/wrappers/Element/Binding.ts b/src/compiler/compile/render_dom/wrappers/Element/Binding.ts
--- a/src/compiler/compile/render_dom/wrappers/Element/Binding.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/Binding.ts
@@ -208,6 +208,10 @@ function get_dom_updater(
 		return `${element.var}.checked = ${condition};`;
 	}
 
+	if (binding.node.name === 'value') {
+		return `@set_input_value(${element.var}, ${binding.snippet});`
+	}
+
 	return `${element.var}.${binding.node.name} = ${binding.snippet};`;
 }
 
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
@@ -169,6 +169,12 @@ export function set_data(text, data) {
 	if (text.data !== data) text.data = data;
 }
 
+export function set_input_value(input, value) {
+	if (value != null || input.value) {
+		input.value = value;
+	}
+}
+
 export function set_input_type(input, type) {
 	try {
 		input.type = type;


Test patch:
diff --git a/test/js/samples/input-no-initial-value/expected.js b/test/js/samples/input-no-initial-value/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/input-no-initial-value/expected.js
@@ -0,0 +1,87 @@
+/* generated by Svelte vX.Y.Z */
+import {
+	SvelteComponent,
+	append,
+	attr,
+	detach,
+	element,
+	init,
+	insert,
+	listen,
+	noop,
+	run_all,
+	safe_not_equal,
+	set_input_value,
+	space
+} from "svelte/internal";
+
+function create_fragment(ctx) {
+	var form, input, t, button, dispose;
+
+	return {
+		c() {
+			form = element("form");
+			input = element("input");
+			t = space();
+			button = element("button");
+			button.textContent = "Store";
+			attr(input, "type", "text");
+			input.required = true;
+
+			dispose = [
+				listen(input, "input", ctx.input_input_handler),
+				listen(form, "submit", ctx.handleSubmit)
+			];
+		},
+
+		m(target, anchor) {
+			insert(target, form, anchor);
+			append(form, input);
+
+			set_input_value(input, ctx.test);
+
+			append(form, t);
+			append(form, button);
+		},
+
+		p(changed, ctx) {
+			if (changed.test && (input.value !== ctx.test)) set_input_value(input, ctx.test);
+		},
+
+		i: noop,
+		o: noop,
+
+		d(detaching) {
+			if (detaching) {
+				detach(form);
+			}
+
+			run_all(dispose);
+		}
+	};
+}
+
+function instance($$self, $$props, $$invalidate) {
+	let test = undefined;
+
+	function handleSubmit(event) {
+		event.preventDefault();
+		console.log('value', test);
+	}
+
+	function input_input_handler() {
+		test = this.value;
+		$$invalidate('test', test);
+	}
+
+	return { test, handleSubmit, input_input_handler };
+}
+
+class Component extends SvelteComponent {
+	constructor(options) {
+		super();
+		init(this, options, instance, create_fragment, safe_not_equal, []);
+	}
+}
+
+export default Component;
\ No newline at end of file
diff --git a/test/js/samples/input-no-initial-value/input.svelte b/test/js/samples/input-no-initial-value/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/js/samples/input-no-initial-value/input.svelte
@@ -0,0 +1,13 @@
+<script>
+	let test = undefined;
+
+	function handleSubmit(event) {
+		event.preventDefault();
+		console.log('value', test);
+	}
+</script>
+
+<form on:submit={handleSubmit}>
+	<input bind:value={test} type=text required>
+	<button>Store</button>
+</form>
\ No newline at end of file
diff --git a/test/js/samples/input-range/expected.js b/test/js/samples/input-range/expected.js
--- a/test/js/samples/input-range/expected.js
+++ b/test/js/samples/input-range/expected.js
@@ -10,6 +10,7 @@ import {
 	noop,
 	run_all,
 	safe_not_equal,
+	set_input_value,
 	to_number
 } from "svelte/internal";
 
@@ -30,11 +31,11 @@ function create_fragment(ctx) {
 		m(target, anchor) {
 			insert(target, input, anchor);
 
-			input.value = ctx.value;
+			set_input_value(input, ctx.value);
 		},
 
 		p(changed, ctx) {
-			if (changed.value) input.value = ctx.value;
+			if (changed.value) set_input_value(input, ctx.value);
 		},
 
 		i: noop,