Skip to content

Commit 506196b

Browse files
authored
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
1 parent db0b802 commit 506196b

File tree

7 files changed

+107
-21
lines changed

7 files changed

+107
-21
lines changed

.changeset/good-buses-reply.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: allow dynamic `type` attribute with `bind:value`

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,10 @@ const validation = {
435435
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'type')
436436
);
437437
if (type && !is_text_attribute(type)) {
438-
error(type, 'invalid-type-attribute');
438+
if (node.name !== 'value' || type.value === true) {
439+
error(type, 'invalid-type-attribute');
440+
}
441+
return; // bind:value can handle dynamic `type` attributes
439442
}
440443

441444
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {

packages/svelte/src/internal/client/render.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,12 @@ export function selected(dom) {
10181018
*/
10191019
export function bind_value(dom, get_value, update) {
10201020
dom.addEventListener('input', () => {
1021+
if (DEV && dom.type === 'checkbox') {
1022+
throw new Error(
1023+
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
1024+
);
1025+
}
1026+
10211027
/** @type {any} */
10221028
let value = dom.value;
10231029
if (is_numberlike_input(dom)) {
@@ -1027,6 +1033,12 @@ export function bind_value(dom, get_value, update) {
10271033
});
10281034

10291035
render_effect(() => {
1036+
if (DEV && dom.type === 'checkbox') {
1037+
throw new Error(
1038+
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
1039+
);
1040+
}
1041+
10301042
const value = get_value();
10311043
// @ts-ignore
10321044
dom.__value = value;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { tick } from 'svelte';
2+
import { test, ok } from '../../test';
3+
4+
export default test({
5+
html: `
6+
<input type=text>
7+
<input type=text>
8+
<p>x / y</p>
9+
10+
<button>change to text</button>
11+
<button>change to number</button>
12+
<button>change to range</button>
13+
`,
14+
ssrHtml: `
15+
<input type=text value=x>
16+
<input type=text value=y>
17+
<p>x / y</p>
18+
19+
<button>change to text</button>
20+
<button>change to number</button>
21+
<button>change to range</button>
22+
`,
23+
async test({ assert, target }) {
24+
const [in1, in2] = target.querySelectorAll('input');
25+
const [btn1, btn2, btn3] = target.querySelectorAll('button');
26+
const p = target.querySelector('p');
27+
ok(p);
28+
29+
in1.value = '0';
30+
in2.value = '1';
31+
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
32+
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
33+
await tick();
34+
btn2?.click();
35+
await tick();
36+
assert.htmlEqual(p.innerHTML, '0 / 1');
37+
38+
in1.stepUp();
39+
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
40+
in2.stepUp();
41+
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
42+
await tick();
43+
assert.htmlEqual(p.innerHTML, '1 / 2');
44+
45+
btn1?.click();
46+
await tick();
47+
try {
48+
in1.stepUp();
49+
assert.fail();
50+
} catch (e) {
51+
// expected
52+
}
53+
54+
btn3?.click();
55+
await tick();
56+
in1.stepUp();
57+
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
58+
in2.stepUp();
59+
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
60+
await tick();
61+
assert.htmlEqual(p.innerHTML, '2 / 3');
62+
63+
btn1?.click();
64+
await tick();
65+
in1.value = 'a';
66+
in2.value = 'b';
67+
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
68+
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
69+
await tick();
70+
assert.htmlEqual(p.innerHTML, 'a / b');
71+
}
72+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script>
2+
let dynamic = $state('x');
3+
let spread = $state('y');
4+
let inputType = $state('text');
5+
let props = $derived({type: inputType});
6+
</script>
7+
8+
<input bind:value={dynamic} type={inputType}>
9+
<input bind:value={spread} {...props}>
10+
<p>{dynamic} / {spread}</p>
11+
12+
<button onclick={() => inputType = 'text'}>change to text</button>
13+
<button onclick={() => inputType = 'number'}>change to number</button>
14+
<button onclick={() => inputType = 'range'}>change to range</button>

packages/svelte/tests/validator/samples/binding-input-type-dynamic/errors.json

Lines changed: 0 additions & 14 deletions
This file was deleted.

packages/svelte/tests/validator/samples/binding-input-type-dynamic/input.svelte

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)