Repository: sveltejs/svelte (JavaScript)
Date: 2017-09-03 16:46:51+00:00
Task type: Refactoring

Issue:
Redundant input blowback guard?
First of a couple of random thoughts I just had:

If you have a template like [this](https://svelte.technology/repl?version=1.22.4&gist=814af8163ea6d4d7be709b7e55040d84)...

```html
<input bind:value>
```

...Svelte generates code like this:

```js
function input_input_handler () {
  input_updating = true;
  component._set({ value: input.value });
  input_updating = false;
}

// later...
if ( !input_updating ) {
  input.value = state.value;
}
```

IIRC the purpose of the `input_updating` check is to prevent the cursor jumping about if, say, you edit text at the beginning of the field (I think setting the value causes the cursor to jump to the end).

In inputs without cursors (range, color, etc etc) maybe that's unnecessary?


Issue discussion:

Solution patch:
diff --git a/src/generators/dom/visitors/Element/Binding.ts b/src/generators/dom/visitors/Element/Binding.ts
--- a/src/generators/dom/visitors/Element/Binding.ts
+++ b/src/generators/dom/visitors/Element/Binding.ts
@@ -51,10 +51,13 @@ export default function visitBinding(
 
 	let setter = getSetter(block, name, snippet, state.parentNode, attribute, dependencies, value);
 	let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`;
+
+	const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(type); // TODO others?
 	const lock = `#${state.parentNode}_updating`;
-	let updateCondition = `!${lock}`;
+	let updateConditions = needsLock ? [`!${lock}`] : [];
+	let readOnly = false;
 
-	block.addVariable(lock, 'false');
+	if (needsLock) block.addVariable(lock, 'false');
 
 	// <select> special case
 	if (node.name === 'select') {
@@ -125,24 +128,24 @@ export default function visitBinding(
 				${setter}
 			`;
 
-			updateCondition += ` && !isNaN(${snippet})`;
+			updateConditions.push(`!isNaN(${snippet})`);
 		} else if (attribute.name === 'duration') {
-			updateCondition = null;
+			readOnly = true;
 		} else if (attribute.name === 'paused') {
 			// this is necessary to prevent the audio restarting by itself
 			const last = block.getUniqueName(`${state.parentNode}_paused_value`);
 			block.addVariable(last, 'true');
 
-			updateCondition = `${last} !== (${last} = ${snippet})`;
+			updateConditions = [`${last} !== (${last} = ${snippet})`];
 			updateElement = `${state.parentNode}[${last} ? "pause" : "play"]();`;
 		}
 	}
 
 	block.builders.init.addBlock(deindent`
 		function ${handler}() {
-			${lock} = true;
+			${needsLock && `${lock} = true;`}
 			${setter}
-			${lock} = false;
+			${needsLock && `${lock} = false;`}
 		}
 	`);
 
@@ -172,13 +175,18 @@ export default function visitBinding(
 		node.initialUpdateNeedsStateObject = !block.contexts.has(name);
 	}
 
-	if (updateCondition !== null) {
-		// audio/video duration is read-only, it never updates
-		block.builders.update.addBlock(deindent`
-			if (${updateCondition}) {
+	if (!readOnly) { // audio/video duration is read-only, it never updates
+		if (updateConditions.length) {
+			block.builders.update.addBlock(deindent`
+				if (${updateConditions.join(' && ')}) {
+					${updateElement}
+				}
+			`);
+		} else {
+			block.builders.update.addBlock(deindent`
 				${updateElement}
-			}
-		`);
+			`);
+		}
 	}
 
 	if (attribute.name === 'paused') {


Test patch:
diff --git a/test/js/samples/input-without-blowback-guard/expected-bundle.js b/test/js/samples/input-without-blowback-guard/expected-bundle.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/input-without-blowback-guard/expected-bundle.js
@@ -0,0 +1,237 @@
+function noop() {}
+
+function assign(target) {
+	var k,
+		source,
+		i = 1,
+		len = arguments.length;
+	for (; i < len; i++) {
+		source = arguments[i];
+		for (k in source) target[k] = source[k];
+	}
+
+	return target;
+}
+
+function insertNode(node, target, anchor) {
+	target.insertBefore(node, anchor);
+}
+
+function detachNode(node) {
+	node.parentNode.removeChild(node);
+}
+
+function createElement(name) {
+	return document.createElement(name);
+}
+
+function addListener(node, event, handler) {
+	node.addEventListener(event, handler, false);
+}
+
+function removeListener(node, event, handler) {
+	node.removeEventListener(event, handler, false);
+}
+
+function destroy(detach) {
+	this.destroy = noop;
+	this.fire('destroy');
+	this.set = this.get = noop;
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = this._state = null;
+}
+
+function differs(a, b) {
+	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
+}
+
+function dispatchObservers(component, group, changed, newState, oldState) {
+	for (var key in group) {
+		if (!changed[key]) continue;
+
+		var newValue = newState[key];
+		var oldValue = oldState[key];
+
+		var callbacks = group[key];
+		if (!callbacks) continue;
+
+		for (var i = 0; i < callbacks.length; i += 1) {
+			var callback = callbacks[i];
+			if (callback.__calling) continue;
+
+			callback.__calling = true;
+			callback.call(component, newValue, oldValue);
+			callback.__calling = false;
+		}
+	}
+}
+
+function get(key) {
+	return key ? this._state[key] : this._state;
+}
+
+function fire(eventName, data) {
+	var handlers =
+		eventName in this._handlers && this._handlers[eventName].slice();
+	if (!handlers) return;
+
+	for (var i = 0; i < handlers.length; i += 1) {
+		handlers[i].call(this, data);
+	}
+}
+
+function observe(key, callback, options) {
+	var group = options && options.defer
+		? this._observers.post
+		: this._observers.pre;
+
+	(group[key] || (group[key] = [])).push(callback);
+
+	if (!options || options.init !== false) {
+		callback.__calling = true;
+		callback.call(this, this._state[key]);
+		callback.__calling = false;
+	}
+
+	return {
+		cancel: function() {
+			var index = group[key].indexOf(callback);
+			if (~index) group[key].splice(index, 1);
+		}
+	};
+}
+
+function on(eventName, handler) {
+	if (eventName === 'teardown') return this.on('destroy', handler);
+
+	var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
+	handlers.push(handler);
+
+	return {
+		cancel: function() {
+			var index = handlers.indexOf(handler);
+			if (~index) handlers.splice(index, 1);
+		}
+	};
+}
+
+function set(newState) {
+	this._set(assign({}, newState));
+	if (this._root._lock) return;
+	this._root._lock = true;
+	callAll(this._root._beforecreate);
+	callAll(this._root._oncreate);
+	callAll(this._root._aftercreate);
+	this._root._lock = false;
+}
+
+function _set(newState) {
+	var oldState = this._state,
+		changed = {},
+		dirty = false;
+
+	for (var key in newState) {
+		if (differs(newState[key], oldState[key])) changed[key] = dirty = true;
+	}
+	if (!dirty) return;
+
+	this._state = assign({}, oldState, newState);
+	this._recompute(changed, this._state, oldState, false);
+	if (this._bind) this._bind(changed, this._state);
+	dispatchObservers(this, this._observers.pre, changed, this._state, oldState);
+	this._fragment.update(changed, this._state);
+	dispatchObservers(this, this._observers.post, changed, this._state, oldState);
+}
+
+function callAll(fns) {
+	while (fns && fns.length) fns.pop()();
+}
+
+function _mount(target, anchor) {
+	this._fragment.mount(target, anchor);
+}
+
+function _unmount() {
+	this._fragment.unmount();
+}
+
+var proto = {
+	destroy: destroy,
+	get: get,
+	fire: fire,
+	observe: observe,
+	on: on,
+	set: set,
+	teardown: destroy,
+	_recompute: noop,
+	_set: _set,
+	_mount: _mount,
+	_unmount: _unmount
+};
+
+function create_main_fragment(state, component) {
+	var input;
+
+	function input_change_handler() {
+		component.set({ foo: input.checked });
+	}
+
+	return {
+		create: function() {
+			input = createElement( 'input' );
+			this.hydrate();
+		},
+
+		hydrate: function(nodes) {
+			input.type = "checkbox";
+			addListener(input, "change", input_change_handler);
+		},
+
+		mount: function(target, anchor) {
+			insertNode( input, target, anchor );
+
+			input.checked = state.foo;
+		},
+
+		update: function(changed, state) {
+			input.checked = state.foo;
+		},
+
+		unmount: function() {
+			detachNode( input );
+		},
+
+		destroy: function() {
+			removeListener(input, "change", input_change_handler);
+		}
+	};
+}
+
+function SvelteComponent(options) {
+	this.options = options;
+	this._state = options.data || {};
+
+	this._observers = {
+		pre: Object.create(null),
+		post: Object.create(null)
+	};
+
+	this._handlers = Object.create(null);
+
+	this._root = options._root || this;
+	this._yield = options._yield;
+	this._bind = options._bind;
+
+	this._fragment = create_main_fragment(this._state, this);
+
+	if (options.target) {
+		this._fragment.create();
+		this._fragment.mount(options.target, options.anchor || null);
+	}
+}
+
+assign(SvelteComponent.prototype, proto );
+
+export default SvelteComponent;
diff --git a/test/js/samples/input-without-blowback-guard/expected.js b/test/js/samples/input-without-blowback-guard/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/input-without-blowback-guard/expected.js
@@ -0,0 +1,66 @@
+import { addListener, assign, createElement, detachNode, insertNode, proto, removeListener } from "svelte/shared.js";
+
+function create_main_fragment(state, component) {
+	var input;
+
+	function input_change_handler() {
+		component.set({ foo: input.checked });
+	}
+
+	return {
+		create: function() {
+			input = createElement( 'input' );
+			this.hydrate();
+		},
+
+		hydrate: function(nodes) {
+			input.type = "checkbox";
+			addListener(input, "change", input_change_handler);
+		},
+
+		mount: function(target, anchor) {
+			insertNode( input, target, anchor );
+
+			input.checked = state.foo;
+		},
+
+		update: function(changed, state) {
+			input.checked = state.foo;
+		},
+
+		unmount: function() {
+			detachNode( input );
+		},
+
+		destroy: function() {
+			removeListener(input, "change", input_change_handler);
+		}
+	};
+}
+
+function SvelteComponent(options) {
+	this.options = options;
+	this._state = options.data || {};
+
+	this._observers = {
+		pre: Object.create(null),
+		post: Object.create(null)
+	};
+
+	this._handlers = Object.create(null);
+
+	this._root = options._root || this;
+	this._yield = options._yield;
+	this._bind = options._bind;
+
+	this._fragment = create_main_fragment(this._state, this);
+
+	if (options.target) {
+		this._fragment.create();
+		this._fragment.mount(options.target, options.anchor || null);
+	}
+}
+
+assign(SvelteComponent.prototype, proto );
+
+export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/input-without-blowback-guard/input.html b/test/js/samples/input-without-blowback-guard/input.html
new file mode 100644
--- /dev/null
+++ b/test/js/samples/input-without-blowback-guard/input.html
@@ -0,0 +1 @@
+<input type='checkbox' bind:checked='foo'>
\ No newline at end of file