Repository: sveltejs/svelte (JavaScript)
Date: 2020-04-18 19:31:23+00:00
Task type: Bug Fix

Issue:
Renders of a variable bound to an input element of type="number" will not update if the variable is changed within a reactive statement that is triggered by the input
**Describe the bug**
The title is a hell of a mouthful.
I'm still not certain I've actually pinpointed the odd behavior, but this is as close as I could get.
To elaborate on the title, if an `<input>` element with `type="number"` triggers a reactive statement via a bound variable, changing the bound variable in the reactive statement will not perform any updates to places which rely on that variable.

That means, in the following example the reactive statement will trigger when `myNum` changes (due to the `console.log`). However, if it was triggered by typing in the `<input>` element, renders using `myNum` will be unaffected. If other variables are changed, those will work fine, but ones with `myNum` will be ignored.

Triggering the reactive statement some other way (like having a button increment `myNum`) will result in `myNum` changing to 500.
```html
<script>
  let myNum = 0;
  $: {
    console.log(myNum);
    myNum = 500;
  }
</script>

<input type="number" bind:value={myNum} />
<button on:click={() => myNum++}>change num</button>
```
As an aside, @pngwn found a fix (not for the bug, but to get this REPL working). Using `tick` before `myNum = 500` allows an update cycle and it should work as it does with `type="text"`:
https://svelte.dev/repl/dca7070e064f4d9da9f49d4133056a76?version=3.20.1

**To Reproduce**
The previous example in a REPL:
https://svelte.dev/repl/2fb57570c5dd48a580619d13246a53e1?version=3.20.1
Change the type from `"number"` to `"text"` to see how its behavior changes.

For an actual use-case (and for how I ran into this), this REPL on restrictive/controlled inputs may help:
https://svelte.dev/repl/8019ec1168fe4fb99e2a2cd44545f70b?version=3.20.1

**Expected behavior**
like `type="text"`, `type="number"` should allow `myNum`'s change in value to affect renders.

**Information about your Svelte project:**
- Your browser and the version: (e.x. Chrome 52.1, Firefox 48.0, IE 10)

- Windows & OSX, REPL, Rollup, Chrome 80, Svelte v3.2

**Severity**
Medium. Creating restrictive user inputs is useful. With React it's the default way to make inputs. With svelte it's a bit more difficult and may require a bit of twisting. 



Issue discussion:
Possibly related to #2816 and #3470.

The difference in behavior between `number` and `text` is presumably related to the `input_updating` guard in the code generated for `number` but I haven't dug into it any more deeply than that.
```js
this.needs_lock = name === 'input' && type === 'number'; // TODO others?
```
[line reference](https://github.com/sveltejs/svelte/blob/aa3dcc06d6b0fcb079ccd993fa6e3455242a2a96/src/compiler/compile/render_dom/wrappers/Element/Binding.ts#L60)
it was introduced as a fix for #3426

Solution 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
@@ -57,7 +57,7 @@ export default class BindingWrapper {
 
 		this.is_readonly = this.node.is_readonly;
 
-		this.needs_lock = this.node.name === 'currentTime' || (parent.node.name === 'input' && parent.node.get_static_attribute_value('type') === 'number'); // TODO others?
+		this.needs_lock = this.node.name === 'currentTime';  // TODO others?
 	}
 
 	get_dependencies() {
@@ -93,11 +93,23 @@ export default class BindingWrapper {
 			update_conditions.push(block.renderer.dirty(dependency_array));
 		}
 
-		if (parent.node.name === 'input') {
-			const type = parent.node.get_static_attribute_value('type');
-
-			if (type === null || type === "" || type === "text" || type === "email" || type === "password") {
-				update_conditions.push(x`${parent.var}.${this.node.name} !== ${this.snippet}`);
+		if (parent.node.name === "input") {
+			const type = parent.node.get_static_attribute_value("type");
+
+			if (
+				type === null ||
+				type === "" ||
+				type === "text" ||
+				type === "email" ||
+				type === "password"
+			) {
+				update_conditions.push(
+					x`${parent.var}.${this.node.name} !== ${this.snippet}`
+				);
+			} else if (type === "number") {
+				update_conditions.push(
+					x`@to_number(${parent.var}.${this.node.name}) !== ${this.snippet}`
+				);
 			}
 		}
 


Test patch:
diff --git a/test/runtime/samples/binding-input-number-2/_config.js b/test/runtime/samples/binding-input-number-2/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-number-2/_config.js
@@ -0,0 +1,31 @@
+export default {
+	test({ assert, target, window, component }) {
+		const input = target.querySelector("input");
+		const inputEvent = new window.InputEvent("input");
+		assert.equal(component.value, 5);
+		assert.equal(input.value, "5");
+
+		input.value = "5.";
+		input.dispatchEvent(inputEvent);
+
+		// input type number has value === "" if ends with dot/comma
+		assert.equal(component.value, undefined);
+		assert.equal(input.value, "");
+
+		input.value = "5.5";
+		input.dispatchEvent(inputEvent);
+
+		assert.equal(component.value, 5.5);
+		assert.equal(input.value, "5.5");
+
+		input.value = "5.50";
+		input.dispatchEvent(inputEvent);
+
+		assert.equal(component.value, 5.5);
+		assert.equal(input.value, "5.50");
+
+		component.value = 1;
+		assert.equal(component.value, 1);
+		assert.equal(input.value, "1");
+	},
+};
diff --git a/test/runtime/samples/binding-input-number-2/main.svelte b/test/runtime/samples/binding-input-number-2/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-number-2/main.svelte
@@ -0,0 +1,5 @@
+<script>
+	export let value = 5;
+</script>
+
+<input type="number" bind:value />