diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap
index 33fb37a58f9..561004cfd07 100644
--- a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap
+++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap
@@ -1,5 +1,15 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+exports[`stringify static html > Select elements can preserve data types when stringified 1`] = `
+"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue
+
+const _hoisted_1 = /*#__PURE__*/_createStaticVNode("", 5)
+
+return function render(_ctx, _cache) {
+ return _hoisted_1
+}"
+`;
+
exports[`stringify static html > should bail on bindings that are hoisted but not stringifiable 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
index b0eb515c254..b005e0645bd 100644
--- a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
+++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
@@ -485,4 +485,17 @@ describe('stringify static html', () => {
expect(code).toMatch(`text1
`)
expect(code).toMatchSnapshot()
})
+
+ // #6568
+ test('Select elements can preserve data types when stringified', () => {
+ const { code } = compileWithStringify(`
+
+
+
+
+
+ `)
+
+ expect(code).toMatchSnapshot()
+ })
})
diff --git a/packages/compiler-dom/src/transforms/stringifyStatic.ts b/packages/compiler-dom/src/transforms/stringifyStatic.ts
index a5ff770f4e8..83ca9f7dc21 100644
--- a/packages/compiler-dom/src/transforms/stringifyStatic.ts
+++ b/packages/compiler-dom/src/transforms/stringifyStatic.ts
@@ -318,6 +318,12 @@ function stringifyElement(
res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml(
evaluated,
)}"`
+ if (typeof evaluated !== 'object' && node.tag === 'option') {
+ res += ` v-stringify-type='${typeof evaluated}'`
+ }
+ }
+ if (evaluated === null && node.tag === 'option') {
+ res += ` v-stringify-type='${'null'}'`
}
} else if (p.name === 'html') {
// #5439 v-html with constant value
diff --git a/packages/runtime-dom/__tests__/directives/vModel.spec.ts b/packages/runtime-dom/__tests__/directives/vModel.spec.ts
index 05ee59d9010..7ee76679ab5 100644
--- a/packages/runtime-dom/__tests__/directives/vModel.spec.ts
+++ b/packages/runtime-dom/__tests__/directives/vModel.spec.ts
@@ -1,5 +1,6 @@
import {
type VNode,
+ createStaticVNode,
defineComponent,
h,
nextTick,
@@ -1227,4 +1228,61 @@ describe('vModel', () => {
await nextTick()
expect(data.value).toEqual('使用拼音输入')
})
+
+ it(`After the select tag is stringified,
+ v-model can get the correct type`, async () => {
+ const hoist = createStaticVNode(
+ "",
+ 5,
+ )
+ const component = defineComponent({
+ data() {
+ return { value: '' }
+ },
+ render() {
+ return [
+ withVModel(
+ h(
+ 'select',
+ {
+ value: null,
+ 'onUpdate:modelValue': setValue.bind(this),
+ },
+ [hoist],
+ ),
+ this.value,
+ ),
+ ]
+ },
+ })
+ render(h(component), root)
+
+ await nextTick()
+ const input = root.querySelector('select')
+ const optionList = root.querySelectorAll('option')
+ const data = root._vnode.component.data
+
+ optionList[0].selected = true
+ triggerEvent('change', input)
+ await nextTick()
+ expect(data.value).toBe('string')
+
+ optionList[0].selected = false
+ optionList[1].selected = true
+ triggerEvent('change', input)
+ await nextTick()
+ expect(data.value).toBe(false)
+
+ optionList[1].selected = false
+ optionList[2].selected = true
+ triggerEvent('change', input)
+ await nextTick()
+ expect(data.value).toBe(1)
+
+ optionList[2].selected = false
+ optionList[4].selected = true
+ triggerEvent('change', input)
+ await nextTick()
+ expect(data.value).toBe(null)
+ })
})
diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts
index 7b387096b68..f719af2fbbc 100644
--- a/packages/runtime-dom/src/directives/vModel.ts
+++ b/packages/runtime-dom/src/directives/vModel.ts
@@ -246,7 +246,18 @@ function setSelected(el: HTMLSelectElement, value: any) {
// retrieve raw value set via :value bindings
function getValue(el: HTMLOptionElement | HTMLInputElement) {
- return '_value' in el ? (el as any)._value : el.value
+ let value = '_value' in el ? (el as any)._value : el.value
+ let stringifyType = el.getAttribute('v-stringify-type')
+ if (stringifyType === 'number') {
+ return Number(value)
+ }
+ if (stringifyType === 'boolean') {
+ return value !== 'false'
+ }
+ if (stringifyType === 'null') {
+ return null
+ }
+ return value
}
// retrieve raw value for true-value and false-value set via :true-value or :false-value bindings