Appearance
表单输入绑定
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
template
<input
:value="text"
@input="event => text = event.target.value">v-model 指令帮我们简化了这一步骤:
template
<input v-model="text">
```guide/components/v-model
另外,`v-model` 还可以用于各种不同的输入元素,`<textarea>`、`<select>` 元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:
- 文本类型的 `<input>` 和 `<textarea>` 元素会绑定 `value` property 并侦听 `input` 事件;
- `<input type="checkbox">` 和 `<input type="radio">` 会绑定 `checked` property 并侦听 `change` 事件;
- `<select>` 会绑定 `value` property 并侦听 `change` 事件。
::: tip 注意
`v-model` 会忽略任何表单元素上初始的 `value`、`checked` 或 `selected` attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用响应式系统的 API来声明该初始值。
:::
## 基本用法
### 文本
```vue
<script setup>
import { ref } from 'vue'
const message = ref('')
</script>
<template>
<p>Message is: {{ message }}</p>
<input v-model="message" placeholder="edit me" />
</template>Message is:
注意
对于需要使用 IME 的语言 (中文,日文和韩文等),你会发现 v-model 不会在 IME 输入还在拼字阶段时触发更新。如果你的确想在拼字阶段也触发更新,请直接使用自己的 input 事件监听器和 value 绑定而不要使用 v-model。
多行文本
vue
<script setup>
import { ref } from 'vue'
const message = ref('')
</script>
<template>
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea>
</template>注意在 <textarea> 中是不支持插值表达式的。请使用 v-model 来替代:
template
<!-- 错误 -->
<textarea>{{ text }}</textarea>
<!-- 正确 -->
<textarea v-model="text"></textarea>复选框
单一的复选框,绑定布尔类型值:
vue
<script setup>
import { ref } from 'vue'
const checked = ref(true)
</script>
<template>
<input id="checkbox" type="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
</template>我们也可以将多个复选框绑定到同一个数组或集合的值:
vue
<script setup>
import { ref } from 'vue'
const checkedNames = ref([])
</script>
<template>
<div>Checked names: {{ checkedNames }}</div>
<input id="jack" type="checkbox" value="Jack" v-model="checkedNames" />
<label for="jack">Jack</label>
<input id="john" type="checkbox" value="John" v-model="checkedNames" />
<label for="john">John</label>
<input id="mike" type="checkbox" value="Mike" v-model="checkedNames" />
<label for="mike">Mike</label>
</template>在这个例子中,checkedNames 数组将始终包含所有当前被选中的框的值。
单选按钮
vue
<script setup>
import { ref } from 'vue'
const picked = ref('One')
</script>
<template>
<div>Picked: {{ picked }}</div>
<input id="one" type="radio" value="One" v-model="picked" />
<label for="one">One</label>
<input id="two" type="radio" value="Two" v-model="picked" />
<label for="two">Two</label>
</template>选择器
单个选择器的示例如下:
vue
<script setup>
import { ref } from 'vue'
const selected = ref('')
</script>
<template>
<div>Selected: {{ selected }}</div>
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
</template>注意
如果 v-model 表达式的初始值不匹配任何一个选择项,<select> 元素会渲染成一个"未选择"的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件。因此,我们建议提供一个空值的禁用选项,如上面的例子所示。
多选 (值绑定到一个数组):
vue
<script setup>
import { ref } from 'vue'
const selected = ref([])
</script>
<template>
<div>Selected: {{ selected }}</div>
<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
</template>选择器的选项可以使用 v-for 动态渲染:
vue
<script setup>
import { ref } from 'vue'
const selected = ref('A')
const options = ref([
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
])
</script>
<template>
<select v-model="selected">
<option v-for="option in options" :value="option.value">
{{ option.text }}
</option>
</select>
<div>Selected: {{ selected }}</div>
</template>值绑定
对于单选按钮,复选框和选择器选项,v-model 绑定的值通常是静态的字符串 (或者对复选框是布尔值):
template
<!-- `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 还使我们可以将选项值绑定为非字符串的数据类型。
复选框
template
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no" />true-value 和 false-value 是 Vue 特有的 attributes,仅支持和 v-model 配套使用。这里 toggle 属性的值会在选中时被设为 'yes',取消选择时设为 'no'。你同样可以通过 v-bind 将其绑定为其他动态值:
template
<input
type="checkbox"
v-model="toggle"
:true-value="dynamicTrueValue"
:false-value="dynamicFalseValue" />提示
true-value 和 false-value attributes 不会影响 value attribute,因为浏览器在表单提交时,并不会包含未选择的复选框。为了保证这两个值 (例如:"yes"和"no") 的其中之一被表单提交,请使用单选按钮作为替代。
单选按钮
template
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second。
选择器选项
template
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
</select>v-model 同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected 会被设为该对象字面量值 { number: 123 }。
修饰符
.lazy
默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据:
template
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />.number
如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:
template
<input v-model.number="age" />如果该值无法被 parseFloat() 处理,那么将返回原始值。
number 修饰符会在输入框有 type="number" 时自动启用。
.trim
如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:
template
<input v-model.trim="msg" />组件上的 v-model
如果你还不熟悉 Vue 的组件,可以先跳过这一小节。
HTML 的内置表单输入类型并不总能满足所有需求。幸运的是,Vue 的组件系统允许你创建具有自定义行为的可复用输入组件。这些输入组件甚至可以和 v-model 一起使用!
实际应用示例
用户注册表单
vue
<script setup>
import { ref, reactive } from 'vue'
const form = reactive({
username: '',
email: '',
password: '',
confirmPassword: '',
gender: '',
interests: [],
country: '',
agreeTerms: false
})
const countries = ['中国', '美国', '英国', '日本', '韩国']
const interestOptions = ['编程', '音乐', '运动', '阅读', '旅行']
const submitForm = () => {
if (form.password !== form.confirmPassword) {
alert('密码不匹配!')
return
}
if (!form.agreeTerms) {
alert('请同意服务条款!')
return
}
console.log('表单数据:', form)
alert('注册成功!')
}
</script>
<template>
<form @submit.prevent="submitForm" class="registration-form">
<h2>用户注册</h2>
<!-- 用户名 -->
<div class="form-group">
<label for="username">用户名:</label>
<input
id="username"
v-model.trim="form.username"
type="text"
required
placeholder="请输入用户名"
/>
</div>
<!-- 邮箱 -->
<div class="form-group">
<label for="email">邮箱:</label>
<input
id="email"
v-model.trim="form.email"
type="email"
required
placeholder="请输入邮箱"
/>
</div>
<!-- 密码 -->
<div class="form-group">
<label for="password">密码:</label>
<input
id="password"
v-model="form.password"
type="password"
required
placeholder="请输入密码"
/>
</div>
<!-- 确认密码 -->
<div class="form-group">
<label for="confirmPassword">确认密码:</label>
<input
id="confirmPassword"
v-model="form.confirmPassword"
type="password"
required
placeholder="请再次输入密码"
/>
</div>
<!-- 性别 -->
<div class="form-group">
<label>性别:</label>
<div class="radio-group">
<input id="male" type="radio" value="male" v-model="form.gender" />
<label for="male">男</label>
<input id="female" type="radio" value="female" v-model="form.gender" />
<label for="female">女</label>
<input id="other" type="radio" value="other" v-model="form.gender" />
<label for="other">其他</label>
</div>
</div>
<!-- 兴趣爱好 -->
<div class="form-group">
<label>兴趣爱好:</label>
<div class="checkbox-group">
<div v-for="interest in interestOptions" :key="interest">
<input
:id="interest"
type="checkbox"
:value="interest"
v-model="form.interests"
/>
<label :for="interest">{{ interest }}</label>
</div>
</div>
</div>
<!-- 国家 -->
<div class="form-group">
<label for="country">国家:</label>
<select id="country" v-model="form.country" required>
<option value="">请选择国家</option>
<option v-for="country in countries" :key="country" :value="country">
{{ country }}
</option>
</select>
</div>
<!-- 同意条款 -->
<div class="form-group">
<input id="terms" type="checkbox" v-model="form.agreeTerms" />
<label for="terms">我同意服务条款和隐私政策</label>
</div>
<!-- 提交按钮 -->
<button type="submit" :disabled="!form.agreeTerms">注册</button>
<!-- 表单数据预览 -->
<div class="form-preview">
<h3>表单数据预览:</h3>
<pre>{{ JSON.stringify(form, null, 2) }}</pre>
</div>
</form>
</template>
<style scoped>
.registration-form {
max-width: 500px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"],
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.radio-group,
.checkbox-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.checkbox-group {
flex-direction: column;
}
.checkbox-group > div {
display: flex;
align-items: center;
gap: 5px;
}
button {
width: 100%;
padding: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.form-preview {
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
}
.form-preview pre {
font-size: 12px;
overflow-x: auto;
}
</style>动态表单验证
vue
<script setup>
import { ref, computed, watch } from 'vue'
const email = ref('')
const password = ref('')
const confirmPassword = ref('')
// 邮箱验证
const emailError = computed(() => {
if (!email.value) return ''
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email.value) ? '' : '请输入有效的邮箱地址'
})
// 密码验证
const passwordError = computed(() => {
if (!password.value) return ''
if (password.value.length < 6) return '密码至少需要6个字符'
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password.value)) {
return '密码必须包含大小写字母和数字'
}
return ''
})
// 确认密码验证
const confirmPasswordError = computed(() => {
if (!confirmPassword.value) return ''
return password.value === confirmPassword.value ? '' : '两次输入的密码不一致'
})
// 表单是否有效
const isFormValid = computed(() => {
return email.value &&
password.value &&
confirmPassword.value &&
!emailError.value &&
!passwordError.value &&
!confirmPasswordError.value
})
// 监听密码变化,清空确认密码
watch(password, () => {
if (confirmPassword.value) {
confirmPassword.value = ''
}
})
</script>
<template>
<form class="validation-form">
<h2>表单验证示例</h2>
<div class="form-group">
<label for="email">邮箱:</label>
<input
id="email"
v-model="email"
type="email"
:class="{ error: emailError }"
placeholder="请输入邮箱"
/>
<span v-if="emailError" class="error-message">{{ emailError }}</span>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input
id="password"
v-model="password"
type="password"
:class="{ error: passwordError }"
placeholder="请输入密码"
/>
<span v-if="passwordError" class="error-message">{{ passwordError }}</span>
</div>
<div class="form-group">
<label for="confirmPassword">确认密码:</label>
<input
id="confirmPassword"
v-model="confirmPassword"
type="password"
:class="{ error: confirmPasswordError }"
placeholder="请再次输入密码"
/>
<span v-if="confirmPasswordError" class="error-message">{{ confirmPasswordError }}</span>
</div>
<button type="submit" :disabled="!isFormValid">
{{ isFormValid ? '提交' : '请完善表单信息' }}
</button>
</form>
</template>
<style scoped>
.validation-form {
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #007bff;
}
.form-group input.error {
border-color: #dc3545;
}
.error-message {
display: block;
color: #dc3545;
font-size: 12px;
margin-top: 5px;
}
button {
width: 100%;
padding: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
button:not(:disabled):hover {
background-color: #0056b3;
}
</style>选项式 API
如果你使用的是选项式 API,可以这样使用表单绑定:
vue
<script>
export default {
data() {
return {
message: '',
checked: false,
picked: '',
selected: '',
multiSelected: []
}
},
methods: {
onSubmit() {
console.log({
message: this.message,
checked: this.checked,
picked: this.picked,
selected: this.selected,
multiSelected: this.multiSelected
})
}
}
}
</script>
<template>
<form @submit.prevent="onSubmit">
<!-- 文本输入 -->
<input v-model="message" placeholder="输入消息" />
<!-- 复选框 -->
<input type="checkbox" v-model="checked" />
<!-- 单选按钮 -->
<input type="radio" value="A" v-model="picked" />
<input type="radio" value="B" v-model="picked" />
<!-- 选择器 -->
<select v-model="selected">
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<!-- 多选 -->
<select v-model="multiSelected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<button type="submit">提交</button>
</form>
</template>性能优化
使用 v-model.lazy
对于不需要实时更新的表单字段,使用 .lazy 修饰符可以减少不必要的更新:
template
<!-- 只在失去焦点时更新 -->
<input v-model.lazy="searchQuery" placeholder="搜索..." />防抖处理
对于搜索框等需要频繁更新的输入,可以使用防抖:
vue
<script setup>
import { ref, watch } from 'vue'
import { debounce } from 'lodash-es'
const searchQuery = ref('')
const searchResults = ref([])
const performSearch = debounce(async (query) => {
if (query) {
// 执行搜索逻辑
searchResults.value = await searchAPI(query)
} else {
searchResults.value = []
}
}, 300)
watch(searchQuery, performSearch)
</script>大型表单优化
对于包含大量字段的表单,考虑使用 shallowRef 或分组管理:
vue
<script setup>
import { shallowRef } from 'vue'
// 使用 shallowRef 减少深层响应式开销
const formData = shallowRef({
personalInfo: {
name: '',
email: '',
phone: ''
},
address: {
street: '',
city: '',
zipCode: ''
}
})
const updateField = (section, field, value) => {
formData.value = {
...formData.value,
[section]: {
...formData.value[section],
[field]: value
}
}
}
</script>最佳实践
使用合适的修饰符:
.trim用于去除空格.number用于数字输入.lazy用于减少更新频率
表单验证:
- 使用计算属性进行实时验证
- 提供清晰的错误提示
- 在提交前进行最终验证
用户体验:
- 提供占位符文本
- 使用适当的输入类型
- 添加加载状态和禁用状态
性能考虑:
- 对于复杂表单使用防抖
- 避免在
v-model中使用复杂计算 - 合理使用
.lazy修饰符