Repository: sveltejs/svelte (JavaScript)
Date: 2017-08-17 23:04:34+00:00
Task type: Feature

Issue:
Potentially catch input type assignment for older browsers?
The new input types are great for progressive enhancement in HTML because if an old browser doesn't support them it just renders a `type="text"` input, which is perfect. Unfortunately, doing this doesn't work in svelte because (at least in IE9) the `input.type = 'search'` assignment will throw an error.

If it doesn't make things too filthy it might be nice to try to catch this assignment and fall back to text, but I totally understand if this is considered out of scope and not worth doing.

![inputtypesearch](https://user-images.githubusercontent.com/3939997/29337481-05ce2b96-81d7-11e7-94ca-a3230652d3c9.png)

As always, thank you for everything!


Issue discussion:
Gah, didn't realise that caused an error! I wonder if the best solution would be to have a `legacy` compile option that changed that code to this:

```js
try { input.type = 'search'; } catch (e) {}

// or
setInputType(input, 'search'); // where setInputType does the above
```
That would be perfect!
+1 for moving to `setInputType`, previous versions of V8 simply won't optimize functions with try/catch blocks.

Solution patch:
diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts
--- a/src/generators/dom/index.ts
+++ b/src/generators/dom/index.ts
@@ -20,6 +20,7 @@ export class DomGenerator extends Generator {
 	metaBindings: string[];
 
 	hydratable: boolean;
+	legacy: boolean;
 
 	hasIntroTransitions: boolean;
 	hasOutroTransitions: boolean;
@@ -40,6 +41,7 @@ export class DomGenerator extends Generator {
 		this.readonly = new Set();
 
 		this.hydratable = options.hydratable;
+		this.legacy = options.legacy;
 		this.needsEncapsulateHelper = false;
 
 		// initial values for e.g. window.innerWidth, if there's a <:Window> meta tag
diff --git a/src/generators/dom/visitors/Element/Attribute.ts b/src/generators/dom/visitors/Element/Attribute.ts
--- a/src/generators/dom/visitors/Element/Attribute.ts
+++ b/src/generators/dom/visitors/Element/Attribute.ts
@@ -44,6 +44,8 @@ export default function visitAttribute(
 		(attribute.value !== true && attribute.value.length > 1) ||
 		(attribute.value.length === 1 && attribute.value[0].type !== 'Text');
 
+	const isLegacyInputType = generator.legacy && name === 'type' && node.name === 'input';
+
 	if (isDynamic) {
 		let value;
 
@@ -108,7 +110,12 @@ export default function visitAttribute(
 		let updater;
 		const init = shouldCache ? `${last} = ${value}` : value;
 
-		if (isSelectValueAttribute) {
+		if (isLegacyInputType) {
+			block.builders.hydrate.addLine(
+				`@setInputType( ${state.parentNode}, ${init} );`
+			);
+			updater = `@setInputType( ${state.parentNode}, ${shouldCache ? last : value} );`;
+		} else if (isSelectValueAttribute) {
 			// annoying special case
 			const isMultipleSelect =
 				node.name === 'select' &&
@@ -178,9 +185,11 @@ export default function visitAttribute(
 				? `''`
 				: stringify(attribute.value[0].data);
 
-		const statement = propertyName
-			? `${state.parentNode}.${propertyName} = ${value};`
-			: `${method}( ${state.parentNode}, '${name}', ${value} );`;
+		const statement = (
+			isLegacyInputType ? `@setInputType( ${state.parentNode}, ${value} );` :
+			propertyName ? `${state.parentNode}.${propertyName} = ${value};` :
+			`${method}( ${state.parentNode}, '${name}', ${value} );`
+		);
 
 		block.builders.hydrate.addLine(statement);
 
diff --git a/src/interfaces.ts b/src/interfaces.ts
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -53,6 +53,7 @@ export interface CompileOptions {
 	shared?: boolean | string;
 	cascade?: boolean;
 	hydratable?: boolean;
+	legacy?: boolean;
 
 	onerror?: (error: Error) => void;
 	onwarn?: (warning: Warning) => void;
diff --git a/src/shared/dom.js b/src/shared/dom.js
--- a/src/shared/dom.js
+++ b/src/shared/dom.js
@@ -96,4 +96,10 @@ export function claimText (nodes, data) {
 	}
 
 	return createText(data);
+}
+
+export function setInputType(input, type) {
+	try {
+		input.type = type;
+	} catch (e) {}
 }
\ No newline at end of file


Test patch:
diff --git a/test/js/index.js b/test/js/index.js
--- a/test/js/index.js
+++ b/test/js/index.js
@@ -2,7 +2,7 @@ import assert from "assert";
 import * as fs from "fs";
 import * as path from "path";
 import { rollup } from "rollup";
-import { svelte } from "../helpers.js";
+import { loadConfig, svelte } from "../helpers.js";
 
 describe("js", () => {
 	fs.readdirSync("test/js/samples").forEach(dir => {
@@ -17,6 +17,8 @@ describe("js", () => {
 
 		(solo ? it.only : it)(dir, () => {
 			dir = path.resolve("test/js/samples", dir);
+			const config = loadConfig(`${dir}/_config.js`);
+
 			const input = fs
 				.readFileSync(`${dir}/input.html`, "utf-8")
 				.replace(/\s+$/, "");
@@ -24,20 +26,17 @@ describe("js", () => {
 			let actual;
 
 			try {
-				actual = svelte.compile(input, {
+				const options = Object.assign(config.options || {}, {
 					shared: true
-				}).code;
+				});
+
+				actual = svelte.compile(input, options).code;
 			} catch (err) {
 				console.log(err.frame);
 				throw err;
 			}
 
 			fs.writeFileSync(`${dir}/_actual.js`, actual);
-			const expected = fs.readFileSync(`${dir}/expected.js`, "utf-8");
-			const expectedBundle = fs.readFileSync(
-				`${dir}/expected-bundle.js`,
-				"utf-8"
-			);
 
 			return rollup({
 				entry: `${dir}/_actual.js`,
@@ -56,6 +55,12 @@ describe("js", () => {
 			}).then(({ code }) => {
 				fs.writeFileSync(`${dir}/_actual-bundle.js`, code);
 
+				const expected = fs.readFileSync(`${dir}/expected.js`, "utf-8");
+				const expectedBundle = fs.readFileSync(
+					`${dir}/expected-bundle.js`,
+					"utf-8"
+				);
+
 				assert.equal(
 					actual.trim().replace(/^\s+$/gm, ""),
 					expected.trim().replace(/^\s+$/gm, "")
diff --git a/test/js/samples/legacy-input-type/_config.js b/test/js/samples/legacy-input-type/_config.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/legacy-input-type/_config.js
@@ -0,0 +1,5 @@
+export default {
+	options: {
+		legacy: true
+	}
+};
\ No newline at end of file
diff --git a/test/js/samples/legacy-input-type/expected-bundle.js b/test/js/samples/legacy-input-type/expected-bundle.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/legacy-input-type/expected-bundle.js
@@ -0,0 +1,213 @@
+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 setInputType(input, type) {
+	try {
+		input.type = type;
+	} catch (e) {}
+}
+
+function destroy(detach) {
+	this.destroy = this.set = this.get = noop;
+	this.fire('destroy');
+
+	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()();
+}
+
+var proto = {
+	destroy: destroy,
+	get: get,
+	fire: fire,
+	observe: observe,
+	on: on,
+	set: set,
+	teardown: destroy,
+	_recompute: noop,
+	_set: _set
+};
+
+function create_main_fragment ( state, component ) {
+	var input;
+
+	return {
+		create: function () {
+			input = createElement( 'input' );
+			this.hydrate();
+		},
+
+		hydrate: function ( nodes ) {
+			setInputType( input, "search" );
+		},
+
+		mount: function ( target, anchor ) {
+			insertNode( input, target, anchor );
+		},
+
+		update: noop,
+
+		unmount: function () {
+			detachNode( input );
+		},
+
+		destroy: noop
+	};
+}
+
+function SvelteComponent ( options ) {
+	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, null );
+	}
+}
+
+assign( SvelteComponent.prototype, proto );
+
+export default SvelteComponent;
diff --git a/test/js/samples/legacy-input-type/expected.js b/test/js/samples/legacy-input-type/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/legacy-input-type/expected.js
@@ -0,0 +1,55 @@
+import { assign, createElement, detachNode, insertNode, noop, proto, setInputType } from "svelte/shared.js";
+
+function create_main_fragment ( state, component ) {
+	var input;
+
+	return {
+		create: function () {
+			input = createElement( 'input' );
+			this.hydrate();
+		},
+
+		hydrate: function ( nodes ) {
+			setInputType( input, "search" );
+		},
+
+		mount: function ( target, anchor ) {
+			insertNode( input, target, anchor );
+		},
+
+		update: noop,
+
+		unmount: function () {
+			detachNode( input );
+		},
+
+		destroy: noop
+	};
+}
+
+function SvelteComponent ( options ) {
+	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, null );
+	}
+}
+
+assign( SvelteComponent.prototype, proto );
+
+export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/legacy-input-type/input.html b/test/js/samples/legacy-input-type/input.html
new file mode 100644
--- /dev/null
+++ b/test/js/samples/legacy-input-type/input.html
@@ -0,0 +1 @@
+<input type='search'>
\ No newline at end of file