feat: allow dynamic `type` attribute with `bind:value` (#10608)

Since these all share the same underlying small runtime code, there's no reason anymore to not allow it.
closes #10256
closes #3921
pull/10614/head
Simon H 2 years ago committed by GitHub
parent db0b802fc2
commit 506196b72b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: allow dynamic `type` attribute with `bind:value`

@ -435,7 +435,10 @@ const validation = {
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'type')
);
if (type && !is_text_attribute(type)) {
error(type, 'invalid-type-attribute');
if (node.name !== 'value' || type.value === true) {
error(type, 'invalid-type-attribute');
}
return; // bind:value can handle dynamic `type` attributes
}
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {

@ -1018,6 +1018,12 @@ export function selected(dom) {
*/
export function bind_value(dom, get_value, update) {
dom.addEventListener('input', () => {
if (DEV && dom.type === 'checkbox') {
throw new Error(
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
);
}
/** @type {any} */
let value = dom.value;
if (is_numberlike_input(dom)) {
@ -1027,6 +1033,12 @@ export function bind_value(dom, get_value, update) {
});
render_effect(() => {
if (DEV && dom.type === 'checkbox') {
throw new Error(
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
);
}
const value = get_value();
// @ts-ignore
dom.__value = value;

@ -0,0 +1,72 @@
import { tick } from 'svelte';
import { test, ok } from '../../test';
export default test({
html: `
<input type=text>
<input type=text>
<p>x / y</p>
<button>change to text</button>
<button>change to number</button>
<button>change to range</button>
`,
ssrHtml: `
<input type=text value=x>
<input type=text value=y>
<p>x / y</p>
<button>change to text</button>
<button>change to number</button>
<button>change to range</button>
`,
async test({ assert, target }) {
const [in1, in2] = target.querySelectorAll('input');
const [btn1, btn2, btn3] = target.querySelectorAll('button');
const p = target.querySelector('p');
ok(p);
in1.value = '0';
in2.value = '1';
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
btn2?.click();
await tick();
assert.htmlEqual(p.innerHTML, '0 / 1');
in1.stepUp();
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.stepUp();
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(p.innerHTML, '1 / 2');
btn1?.click();
await tick();
try {
in1.stepUp();
assert.fail();
} catch (e) {
// expected
}
btn3?.click();
await tick();
in1.stepUp();
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.stepUp();
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(p.innerHTML, '2 / 3');
btn1?.click();
await tick();
in1.value = 'a';
in2.value = 'b';
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(p.innerHTML, 'a / b');
}
});

@ -0,0 +1,14 @@
<script>
let dynamic = $state('x');
let spread = $state('y');
let inputType = $state('text');
let props = $derived({type: inputType});
</script>
<input bind:value={dynamic} type={inputType}>
<input bind:value={spread} {...props}>
<p>{dynamic} / {spread}</p>
<button onclick={() => inputType = 'text'}>change to text</button>
<button onclick={() => inputType = 'number'}>change to number</button>
<button onclick={() => inputType = 'range'}>change to range</button>

@ -1,14 +0,0 @@
[
{
"code": "invalid-type-attribute",
"message": "'type' attribute must be a static text value if input uses two-way binding",
"start": {
"line": 6,
"column": 24
},
"end": {
"line": 6,
"column": 40
}
}
]

@ -1,6 +0,0 @@
<script>
let foo;
let inputType;
</script>
<input bind:value={foo} type={inputType}>
Loading…
Cancel
Save