跳到主要内容

Vue 表单绑定

表单输入绑定是前端开发中最常见的需求之一。Vue 的 v-model 指令提供了一种简洁的方式来实现表单元素与数据的双向绑定。本章将详细介绍表单绑定的各种用法和最佳实践。

理解 v-model

v-model 是 Vue 提供的用于实现双向数据绑定的指令。它本质上是语法糖,根据不同的表单元素自动绑定到不同的属性和事件:

表单元素绑定的属性监听的事件
text、textareavalueinput
checkbox、radiocheckedchange
selectvaluechange
重要提示

v-model 会忽略所有表单元素的 valuecheckedselected 属性的初始值。它总是将当前绑定的 JavaScript 状态作为数据源。你应该通过 JavaScript 响应式 API 来声明初始值。

基本用法

文本输入

<script setup>
import { ref } from 'vue'

const message = ref('')
</script>

<template>
<p>消息: {{ message }}</p>
<input v-model="message" placeholder="请输入内容" />
</template>
IME 输入法说明

对于需要使用 IME 的语言(中文、日文、韩文等),你会发现 v-model 在 IME 输入组合过程中不会更新。如果你想在此期间也更新数据,请使用 input 事件监听器和 value 绑定来替代 v-model

多行文本

<script setup>
import { ref } from 'vue'

const message = ref('')
</script>

<template>
<span>多行消息:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="添加多行内容"></textarea>
</template>
注意

<textarea> 中使用插值语法不会生效。请使用 v-model 来替代。

<!-- 错误 -->
<textarea>{{ text }}</textarea>

<!-- 正确 -->
<textarea v-model="text"></textarea>

复选框

单个复选框

单个复选框绑定到布尔值:

<script setup>
import { ref } from 'vue'

const checked = ref(false)
</script>

<template>
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked ? '已选中' : '未选中' }}</label>
</template>

多个复选框

多个复选框可以绑定到同一个数组或 Set 值:

<script setup>
import { ref } from 'vue'

const checkedNames = ref([])
</script>

<template>
<div>选中的名字: {{ checkedNames }}</div>

<input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
<label for="jack">Jack</label>

<input type="checkbox" id="john" value="John" v-model="checkedNames" />
<label for="john">John</label>

<input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<label for="mike">Mike</label>
</template>

在这个例子中,checkedNames 数组将始终包含当前选中框的值。

使用 Set 存储选中值

<script setup>
import { ref } from 'vue'

const checkedItems = ref(new Set())

function toggleItem(item) {
if (checkedItems.value.has(item)) {
checkedItems.value.delete(item)
} else {
checkedItems.value.add(item)
}
}
</script>

<template>
<!-- Vue 3.4+ 支持 Set 类型 -->
<input type="checkbox" value="a" v-model="checkedItems" />
<input type="checkbox" value="b" v-model="checkedItems" />
<p>选中: {{ Array.from(checkedItems) }}</p>
</template>

单选按钮

<script setup>
import { ref } from 'vue'

const picked = ref('')
</script>

<template>
<div>选中: {{ picked }}</div>

<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>

<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>
</template>

下拉选择框

单选下拉框

<script setup>
import { ref } from 'vue'

const selected = ref('')
</script>

<template>
<div>选中: {{ selected }}</div>

<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
</template>
iOS 兼容性提示

如果 v-model 的初始值不匹配任何选项,<select> 元素将渲染为"未选中"状态。在 iOS 上这会导致用户无法选择第一项,因为 iOS 不会触发 change 事件。因此建议提供一个空值的禁用选项。

多选下拉框

<script setup>
import { ref } from 'vue'

const selected = ref([])
</script>

<template>
<div>选中: {{ selected }}</div>

<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
</template>

动态渲染选项

<script setup>
import { ref } from 'vue'

const selected = ref('A')
const options = ref([
{ text: '选项一', value: 'A' },
{ text: '选项二', value: 'B' },
{ text: '选项三', value: 'C' }
])
</script>

<template>
<select v-model="selected">
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
<div>选中: {{ selected }}</div>
</template>

值绑定

对于单选按钮、复选框和选择框选项,v-model 绑定的值通常是静态字符串(复选框是布尔值):

<!-- 选中时 `picked` 是字符串 "a" -->
<input type="radio" v-model="picked" value="a" />

<!-- `toggle` 为 true 或 false -->
<input type="checkbox" v-model="toggle" />

<!-- 选中第一个选项时 `selected` 是字符串 "abc" -->
<select v-model="selected">
<option value="abc">ABC</option>
</select>

绑定动态值

有时我们可能需要将值绑定到当前活动实例的动态属性上,可以使用 v-bind 来实现。此外,使用 v-bind 可以将输入值绑定为非字符串值。

复选框的值绑定

<script setup>
import { ref } from 'vue'

const toggle = ref('yes')
const dynamicTrueValue = ref('on')
const dynamicFalseValue = ref('off')
</script>

<template>
<!-- 静态值:选中时为 'yes',未选中时为 'no' -->
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no"
/>
<p>值: {{ toggle }}</p>

<!-- 动态值 -->
<input
type="checkbox"
v-model="toggle"
:true-value="dynamicTrueValue"
:false-value="dynamicFalseValue"
/>
</template>

true-valuefalse-value 是 Vue 特有的属性,仅适用于 v-model。当复选框被选中时,toggle 会被设置为 'yes';未选中时被设置为 'no'

提示

true-valuefalse-value 属性不会影响输入框的 value 属性,因为浏览器在表单提交时不包含未选中的复选框。如果需要确保提交两个值中的一个(例如 "yes" 或 "no"),请改用单选按钮。

单选按钮的值绑定

<script setup>
import { ref } from 'vue'

const pick = ref(null)
const first = ref({ name: 'first' })
const second = ref({ name: 'second' })
</script>

<template>
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />

<p>选中: {{ pick?.name }}</p>
</template>

当第一个单选按钮被选中时,pick 会被设置为 first 对象的值;选中第二个时,被设置为 second 对象的值。

选择框选项的值绑定

<script setup>
import { ref } from 'vue'

const selected = ref(null)
</script>

<template>
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
<option :value="{ number: 456 }">456</option>
</select>

<div>选中: {{ selected?.number }}</div>
</template>

v-model 也支持非字符串的值绑定!在上面的例子中,当选项被选中时,selected 会被设置为对象字面量值 { number: 123 }

修饰符

.lazy

默认情况下,v-model 在每次 input 事件后同步输入框的值(除了上述 IME 组合期间)。你可以添加 lazy 修饰符,使其在 change 事件后同步:

<script setup>
import { ref } from 'vue'

const msg = ref('')
</script>

<template>
<!-- 在 "change" 事件后同步,而非 "input" -->
<input v-model.lazy="msg" />
<p>消息: {{ msg }}</p>
</template>

.number

如果你想将用户输入自动转换为数字类型,可以添加 number 修饰符:

<script setup>
import { ref } from 'vue'

const age = ref('')
</script>

<template>
<input v-model.number="age" type="number" />
<p>类型: {{ typeof age }}, 值: {{ age }}</p>
</template>

如果输入值无法被 parseFloat() 解析,则返回原始值。当输入类型是 type="number" 时,number 修饰符会自动应用。

.trim

如果要自动去除用户输入的首尾空白字符,可以添加 trim 修饰符:

<script setup>
import { ref } from 'vue'

const msg = ref('')
</script>

<template>
<input v-model.trim="msg" />
<p>消息: '{{ msg }}'</p>
</template>

组合使用修饰符

可以同时使用多个修饰符:

<input v-model.lazy.trim="msg" />
<input v-model.number.trim="age" />

组件上的 v-model

HTML 内置的输入类型并不总是能满足需求。幸运的是,Vue 组件允许你构建具有完全自定义行为的可复用输入框,这些输入框甚至可以与 v-model 一起使用。

基本用法

<!-- CustomInput.vue -->
<script setup>
const model = defineModel()

function updateValue(e) {
model.value = e.target.value
}
</script>

<template>
<input :value="model" @input="updateValue" />
</template>
<!-- 使用组件 -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const message = ref('')
</script>

<template>
<CustomInput v-model="message" />
<p>{{ message }}</p>
</template>

Vue 3.4+ 使用 defineModel

<!-- CustomInput.vue -->
<script setup>
const model = defineModel()
</script>

<template>
<input v-model="model" />
</template>

多个 v-model 绑定

<!-- UserName.vue -->
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
<input v-model="firstName" placeholder="名" />
<input v-model="lastName" placeholder="姓" />
</template>
<!-- 使用 -->
<script setup>
import { ref } from 'vue'
import UserName from './UserName.vue'

const first = ref('')
const last = ref('')
</script>

<template>
<UserName v-model:first-name="first" v-model:last-name="last" />
</template>

实际应用示例

示例:登录表单

<script setup>
import { ref, computed } from 'vue'

const form = ref({
username: '',
password: '',
remember: false
})

const errors = computed(() => {
const errs = {}
if (!form.value.username) errs.username = '请输入用户名'
if (!form.value.password) errs.password = '请输入密码'
return errs
})

const isValid = computed(() => Object.keys(errors.value).length === 0)

function handleSubmit() {
if (!isValid.value) return
console.log('登录:', form.value)
}
</script>

<template>
<form class="login-form" @submit.prevent="handleSubmit">
<h2>登录</h2>

<div class="form-group">
<label>用户名</label>
<input v-model.trim="form.username" placeholder="请输入用户名" />
<span v-if="errors.username" class="error">{{ errors.username }}</span>
</div>

<div class="form-group">
<label>密码</label>
<input v-model="form.password" type="password" placeholder="请输入密码" />
<span v-if="errors.password" class="error">{{ errors.password }}</span>
</div>

<div class="form-group">
<label>
<input type="checkbox" v-model="form.remember" />
记住我
</label>
</div>

<button type="submit" :disabled="!isValid">登录</button>
</form>
</template>

<style scoped>
.login-form {
max-width: 300px;
margin: 0 auto;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 8px;
}
.error {
color: #f5222d;
font-size: 12px;
}
button {
width: 100%;
padding: 10px;
background: #42b883;
color: white;
border: none;
}
button:disabled {
background: #ccc;
}
</style>

示例:搜索表单

<script setup>
import { ref, watch } from 'vue'

const searchQuery = ref('')
const selectedCategory = ref('')
const priceRange = ref([0, 1000])
const sortBy = ref('relevance')

// 搜索参数
const searchParams = ref({})

// 监听搜索条件变化
watch([searchQuery, selectedCategory, priceRange, sortBy], () => {
searchParams.value = {
query: searchQuery.value,
category: selectedCategory.value,
minPrice: priceRange.value[0],
maxPrice: priceRange.value[1],
sortBy: sortBy.value
}
}, { immediate: true, deep: true })

function resetFilters() {
searchQuery.value = ''
selectedCategory.value = ''
priceRange.value = [0, 1000]
sortBy.value = 'relevance'
}
</script>

<template>
<div class="search-form">
<h2>搜索</h2>

<!-- 搜索框 -->
<div class="form-group">
<input
v-model.lazy="searchQuery"
placeholder="搜索商品..."
class="search-input"
/>
</div>

<!-- 分类选择 -->
<div class="form-group">
<label>分类</label>
<select v-model="selectedCategory">
<option value="">全部分类</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
<option value="books">图书</option>
</select>
</div>

<!-- 价格范围 -->
<div class="form-group">
<label>价格范围</label>
<div class="price-inputs">
<input type="number" v-model.number="priceRange[0]" min="0" />
<span>-</span>
<input type="number" v-model.number="priceRange[1]" :min="priceRange[0]" />
</div>
</div>

<!-- 排序 -->
<div class="form-group">
<label>排序方式</label>
<select v-model="sortBy">
<option value="relevance">相关度</option>
<option value="price-asc">价格从低到高</option>
<option value="price-desc">价格从高到低</option>
<option value="newest">最新上架</option>
</select>
</div>

<button @click="resetFilters">重置筛选</button>

<!-- 当前搜索条件 -->
<pre class="params">{{ searchParams }}</pre>
</div>
</template>

<style scoped>
.search-form {
max-width: 400px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input, select {
width: 100%;
padding: 8px;
}
.search-input {
font-size: 16px;
}
.price-inputs {
display: flex;
align-items: center;
gap: 10px;
}
.price-inputs input {
flex: 1;
}
button {
width: 100%;
padding: 10px;
background: #42b883;
color: white;
border: none;
margin-top: 10px;
}
.params {
background: #f5f5f5;
padding: 10px;
margin-top: 20px;
font-size: 12px;
}
</style>

示例:设置面板

<script setup>
import { ref } from 'vue'

const settings = ref({
notifications: {
email: true,
push: false,
sms: false
},
privacy: {
profileVisible: true,
showEmail: false,
showPhone: false
},
preferences: {
theme: 'light',
language: 'zh-CN',
timezone: 'Asia/Shanghai'
}
})

const themes = [
{ value: 'light', label: '浅色' },
{ value: 'dark', label: '深色' },
{ value: 'auto', label: '跟随系统' }
]

const languages = [
{ value: 'zh-CN', label: '简体中文' },
{ value: 'zh-TW', label: '繁體中文' },
{ value: 'en', label: 'English' }
]

function saveSettings() {
console.log('保存设置:', settings.value)
}
</script>

<template>
<div class="settings-panel">
<h2>设置</h2>

<!-- 通知设置 -->
<section>
<h3>通知</h3>
<label>
<input type="checkbox" v-model="settings.notifications.email" />
邮件通知
</label>
<label>
<input type="checkbox" v-model="settings.notifications.push" />
推送通知
</label>
<label>
<input type="checkbox" v-model="settings.notifications.sms" />
短信通知
</label>
</section>

<!-- 隐私设置 -->
<section>
<h3>隐私</h3>
<label>
<input type="checkbox" v-model="settings.privacy.profileVisible" />
公开个人资料
</label>
<label>
<input type="checkbox" v-model="settings.privacy.showEmail" />
显示邮箱地址
</label>
<label>
<input type="checkbox" v-model="settings.privacy.showPhone" />
显示电话号码
</label>
</section>

<!-- 偏好设置 -->
<section>
<h3>偏好</h3>
<div class="form-row">
<label>主题</label>
<select v-model="settings.preferences.theme">
<option v-for="theme in themes" :key="theme.value" :value="theme.value">
{{ theme.label }}
</option>
</select>
</div>
<div class="form-row">
<label>语言</label>
<select v-model="settings.preferences.language">
<option v-for="lang in languages" :key="lang.value" :value="lang.value">
{{ lang.label }}
</option>
</select>
</div>
</section>

<button @click="saveSettings">保存设置</button>
</div>
</template>

<style scoped>
.settings-panel {
max-width: 500px;
}
section {
margin-bottom: 30px;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
}
h3 {
margin-bottom: 15px;
}
label {
display: block;
margin: 10px 0;
}
.form-row {
display: flex;
align-items: center;
margin: 10px 0;
}
.form-row label {
flex: 0 0 80px;
margin: 0;
}
.form-row select {
flex: 1;
padding: 8px;
}
button {
width: 100%;
padding: 12px;
background: #42b883;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
}
</style>

小结

本章我们详细学习了 Vue 表单绑定的完整内容:

  1. v-model 原理:根据不同表单元素绑定不同属性和事件
  2. 文本输入:单行文本和多行文本
  3. 复选框:单个复选框和多选复选框
  4. 单选按钮:单选按钮组
  5. 选择框:单选、多选和动态选项
  6. 值绑定:绑定动态值和非字符串值
  7. 修饰符.lazy.number.trim
  8. 组件上的 v-model:自定义表单组件

最佳实践

  • 始终在 JavaScript 端声明初始值
  • 使用 .lazy 优化性能敏感场景
  • 使用 .number 处理数字输入
  • 使用 .trim 处理文本输入
  • 对于复杂表单,考虑使用计算属性进行验证

练习

  1. 创建一个带有实时验证的注册表单
  2. 实现一个带有搜索、过滤和排序功能的搜索表单
  3. 创建一个用户设置面板,包含多个表单元素

准备好进入下一章,学习组件基础的内容了吗?