Repository: sveltejs/svelte (JavaScript)
Date: 2017-06-11 20:54:46+00:00
Task type: Bug Fix

Issue:
Can't set type for input tag. 
I am tring to generate input tag:
```html
<input type="{{cond.field_type}}" placeholder="constant"
       value="{{cond.const}}" on:change="setCondConst(index, this)">
```
but svelte plugin output:
```
'svelte' plugin: 'type must be a static attribute
```


Issue discussion:
`type` needs to be static because in some cases fairly different code is generated depending on the input's type. Allowing `type` to vary would result in all that logic being pushed to the generated code. In addition to making the runtime code more complicated, it would also make the compiler code more complicated. The two versions of the code (doing different things based on the `type` at compile time, and doing different things based on the `type` at runtime) would have to be kept in sync. It seems to be a much larger surface for bugs, with not a lot of benefit. Rich may feel differently, but I doubt this is something we want to pursue.

If you need this sort of functionality, I would recommend having multiple `{{#if}}`...`{{else}}`...`{{/if}}` blocks around different instances of the `<input>` tag for all of the different `type`s you need to support.
I *think* it only needs to be static if there's a two-way binding, because the change handler is dependent on the input type. In this case, I reckon it's fine for it to be dynamic.

The error is being raised [here](https://github.com/sveltejs/svelte/blob/fe75570b910cc3e0cf6df55f8264dc50caafb2a4/src/generators/dom/visitors/Element/getStaticAttributeValue.ts#L11) because of [this block](https://github.com/sveltejs/svelte/blob/fe75570b910cc3e0cf6df55f8264dc50caafb2a4/src/generators/dom/visitors/Element/Attribute.ts#L24) — note the TODO comment (mea culpa). That check should probably be more specific and happen in the validation phase.

Solution patch:
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
@@ -22,8 +22,7 @@ export default function visitAttribute(
 	const isIndirectlyBoundValue =
 		name === 'value' &&
 		(node.name === 'option' || // TODO check it's actually bound
-			(node.name === 'input' &&
-				/^(checkbox|radio)$/.test(getStaticAttributeValue(node, 'type'))));
+			(node.name === 'input' && node.attributes.find((attribute: Node) => attribute.type === 'Binding' && /checked|group/.test(attribute.name))));
 
 	const propertyName = isIndirectlyBoundValue
 		? '__value'
@@ -133,8 +132,8 @@ export default function visitAttribute(
 		const statement = propertyName
 			? `${state.parentNode}.${propertyName} = ${value};`
 			: `${generator.helper(
-					method
-				)}( ${state.parentNode}, '${name}', ${value} );`;
+				method
+			)}( ${state.parentNode}, '${name}', ${value} );`;
 
 		block.builders.create.addLine(statement);
 
diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts
--- a/src/validate/html/validateElement.ts
+++ b/src/validate/html/validateElement.ts
@@ -25,6 +25,8 @@ export default function validateElement(validator: Validator, node: Node) {
 						attribute.start
 					);
 				}
+
+				checkTypeAttribute(validator, node);
 			} else if (name === 'checked') {
 				if (node.name !== 'input') {
 					validator.error(
@@ -33,7 +35,7 @@ export default function validateElement(validator: Validator, node: Node) {
 					);
 				}
 
-				if (getType(validator, node) !== 'checkbox') {
+				if (checkTypeAttribute(validator, node) !== 'checkbox') {
 					validator.error(
 						`'checked' binding can only be used with <input type="checkbox">`,
 						attribute.start
@@ -47,7 +49,7 @@ export default function validateElement(validator: Validator, node: Node) {
 					);
 				}
 
-				const type = getType(validator, node);
+				const type = checkTypeAttribute(validator, node);
 
 				if (type !== 'checkbox' && type !== 'radio') {
 					validator.error(
@@ -128,7 +130,7 @@ export default function validateElement(validator: Validator, node: Node) {
 	});
 }
 
-function getType(validator: Validator, node: Node) {
+function checkTypeAttribute(validator: Validator, node: Node) {
 	const attribute = node.attributes.find(
 		(attribute: Node) => attribute.name === 'type'
 	);
@@ -139,7 +141,7 @@ function getType(validator: Validator, node: Node) {
 	}
 
 	if (attribute.value.length > 1 || attribute.value[0].type !== 'Text') {
-		validator.error(`'type attribute cannot be dynamic`, attribute.start);
+		validator.error(`'type' attribute cannot be dynamic if input uses two-way binding`, attribute.start);
 	}
 
 	return attribute.value[0].data;


Test patch:
diff --git a/test/runtime/samples/attribute-dynamic-type/_config.js b/test/runtime/samples/attribute-dynamic-type/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/attribute-dynamic-type/_config.js
@@ -0,0 +1,19 @@
+export default {
+	'skip-ssr': true,
+
+	data: {
+		inputType: 'text',
+		inputValue: 42
+	},
+
+	html: `<input type="text">`,
+
+	test(assert, component, target) {
+		const input = target.querySelector('input');
+		assert.equal(input.type, 'text');
+		assert.equal(input.value, '42');
+
+		component.set({ inputType: 'number' });
+		assert.equal(input.type, 'number');
+	}
+};
diff --git a/test/runtime/samples/attribute-dynamic-type/main.html b/test/runtime/samples/attribute-dynamic-type/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/attribute-dynamic-type/main.html
@@ -0,0 +1 @@
+<input type='{{inputType}}' value='{{inputValue}}'>
\ No newline at end of file
diff --git a/test/validator/samples/binding-input-static-type/errors.json b/test/validator/samples/binding-input-static-type/errors.json
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/binding-input-static-type/errors.json
@@ -0,0 +1,8 @@
+[{
+	"message": "'type' attribute cannot be dynamic if input uses two-way binding",
+	"loc": {
+		"line": 1,
+		"column": 24
+	},
+	"pos": 24
+}]
\ No newline at end of file
diff --git a/test/validator/samples/binding-input-static-type/input.html b/test/validator/samples/binding-input-static-type/input.html
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/binding-input-static-type/input.html
@@ -0,0 +1 @@
+<input bind:value='foo' type='{{inputType}}'>
\ No newline at end of file