diff --git a/.changeset/good-buses-reply.md b/.changeset/good-buses-reply.md new file mode 100644 index 0000000000..e760523f96 --- /dev/null +++ b/.changeset/good-buses-reply.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: allow dynamic `type` attribute with `bind:value` diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 27f61bf634..67d45226f3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -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') { diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 97cb748bae..3d8d855e86 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -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; diff --git a/packages/svelte/tests/runtime-runes/samples/bind-value-input-type-dynamic/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-value-input-type-dynamic/_config.js new file mode 100644 index 0000000000..e422704dcd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-value-input-type-dynamic/_config.js @@ -0,0 +1,72 @@ +import { tick } from 'svelte'; +import { test, ok } from '../../test'; + +export default test({ + html: ` + + +
x / y
+ + + + + `, + ssrHtml: ` + + +x / y
+ + + + + `, + 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'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-value-input-type-dynamic/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-value-input-type-dynamic/main.svelte new file mode 100644 index 0000000000..82b33feb5f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-value-input-type-dynamic/main.svelte @@ -0,0 +1,14 @@ + + + + +{dynamic} / {spread}
+ + + + diff --git a/packages/svelte/tests/validator/samples/binding-input-type-dynamic/errors.json b/packages/svelte/tests/validator/samples/binding-input-type-dynamic/errors.json deleted file mode 100644 index 4292da87fe..0000000000 --- a/packages/svelte/tests/validator/samples/binding-input-type-dynamic/errors.json +++ /dev/null @@ -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 - } - } -] diff --git a/packages/svelte/tests/validator/samples/binding-input-type-dynamic/input.svelte b/packages/svelte/tests/validator/samples/binding-input-type-dynamic/input.svelte deleted file mode 100644 index e1c69e97d6..0000000000 --- a/packages/svelte/tests/validator/samples/binding-input-type-dynamic/input.svelte +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file