8.1 表单绑定:v-model 的各控件形态 本节摘要:v-model 在不同表单控件上展开为不同的属性与事件组合——文本框绑 value/input、复选框绑 checked/change、下拉绑 value/change。本节覆盖各控件绑定、值绑定技巧、三个常用修饰符,以及封装自定义表单组件时的 v-model 约定处理。 能力目标 阅读完本节,你应当能够: 正确绑定文本、多行文本、复选、单选、下拉五类控件; 处理复选框的真假值自定义与数组绑定; 用 lazy/number/trim 修饰符解决三类脏数据; 封装带 v-model 的自定义表单控件(两代写法)。
本节摘要:v-model 在不同表单控件上展开为不同的属性与事件组合——文本框绑 value/input、复选框绑 checked/change、下拉绑 value/change。本节覆盖各控件绑定、值绑定技巧、三个常用修饰符,以及封装自定义表单组件时的 v-model 约定处理。
阅读完本节,你应当能够:
一次看全,注意每种控件绑定的属性与事件其实不同:
<!-- 文本:value + input --> <input v-model="form.name" type="text"> <!-- 多行文本:value + input,注意插值表达式无效 --> <textarea v-model="form.remark"></textarea> <!-- 单个复选:checked + change,绑布尔值 --> <input v-model="form.agreed" type="checkbox"> <!-- 多个复选:同一数组,勾选推入取消移出 --> <input v-model="form.skills" type="checkbox" value="vue" id="s1"> <input v-model="form.skills" type="checkbox" value="react" id="s2"> <!-- 单选:checked + change,绑选中的 value --> <input v-model="form.level" type="radio" value="senior"> <!-- 下拉:value + change --> <select v-model="form.city"> <option disabled value="">请选择城市</option> <option>北京</option> <option>上海</option> </select>
下拉框有个必做细节:首项放 disabled 的占位项,否则初始状态下 Vue 选不中任何项、显示空白,用户也回不去"未选择"状态。
默认单个复选绑出 true/false,但业务常常要的是自定义值:
<!-- 勾选时 submitFlag 为 'yes',取消为 'no' --> <input v-model="form.submitFlag" type="checkbox" true-value="yes" false-value="no"> <!-- 下拉绑对象:选项来自数据而非写死 --> <select v-model="form.dept"> <option v-for="d in departments" :key="d.id" :value="d"> {{ d.name }} </option> </select>
下拉的 :value 绑对象是重要技巧——选中后 form.dept 直接拿到完整对象(含 id),省一步反查。初始渲染时若 form.dept 与某个选项引用不同但内容相同,可能选不中,用同一数据源即可避免。
<!-- lazy:失焦才同步,避免每个键触发一次更新与校验 --> <input v-model.lazy="form.title"> <!-- number:输入转数值。年龄字段不转的话拿到的是字符串 "18",提交后端类型错乱 --> <input v-model.number="form.age" type="number"> <!-- trim:去首尾空格,用户名的隐性格式化 --> <input v-model.trim="form.username">
v-model.number 值得展开一句:不写 number 修饰时,即便 input 标了 type="number",v-model 拿到的仍是字符串(input 事件回传的是字符串)。字符串数字混进算术运算与严格相等判断,是表单数据的第一大污染源。
假设要封装一个"带单位后缀的数字输入"。核心是接住 v-model 约定:
<!-- Vue 3 --> <script setup> defineProps(['modelValue']); defineEmits(['update:modelValue']); </script> <template> <span class="unit-input"> <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> <span class="unit">kg</span> </span> </template>
<!-- 调用处与原生控件无差别 --> <unit-input v-model="form.weight"></unit-input>
Vue 2 的对应写法是 props 声明 value、事件发 input:
Vue.component('unit-input', { props: ['value'], template: ` <span> <input type="text" :value="value" @input="$emit('input', $event.target.value)"> <span>kg</span> </span> ` });
封装的价值不止复用:单位后缀、格式化、防抖请求联想等逻辑收进组件,父级表单只面对干净的 v-model。多字段封装用 Vue 3 的多 v-model(v-model:weight、v-model:height),每个字段一对 prop 与事件。
在封装里常想把格式化逻辑放中间层:
// 危险写法:computed 里改了自己依赖的数据 computed: { weight: { get() { return this.form.weight; }, set(v) { this.form.weight = v + this.unit; } // 若 get 又解析 unit,改拉扯 } }
get 与 set 的数据形态必须稳定闭环:get 输出的形态,set 要能原样接收。数字输入框的经典实现——get 返回数值、set 里过滤非法字符后转数值存回——形态一致,循环安全;一旦 get/set 各自为政,就出现输入一个字符跳两个、光标飞到末尾的症状。
顺带梳理一遍本节五类控件绑定在记忆上的锚点:文本与多行文本是"值进值出"最直觉的形态;复选分单双两种形态——单个绑布尔、多个绑数组,切换的语义完全不同,配置时先想清楚业务上这是"是否同意"还是"多选标签";单选绑的是被选中项的值;下拉最灵活,选项可以来自数据、值可以绑对象,但别忘了占位项。五类控件在浏览器里触发的原生事件本来就不同,v-model 只是替你把这些差异翻译成了统一的书写形式,理解了这层翻译关系,遇到自定义控件接入 v-model 时就不会无从下手。
最后补一段容易被忽略的工程细节:表单数据在提交前后的形态管理。很多团队的习惯是让 v-model 直接绑定接口返回的原始对象,用户改到一半取消编辑时,只能靠逐字段回滚甚至重新请求来恢复——更稳妥的做法是编辑态绑一个本地副本,确认提交时才把它写回源数据,取消则直接丢弃副本。这个"副本编辑、确认落库"的模式,本质上是给用户输入加了一层事务,成本是几次对象拷贝,收益是取消、多标签编辑、校验失败重试这些场景全部自然成立。结合本章的 v-model 约定与下一章的提交时序,一个健壮的表单模块就齐了。
数据通道干净了,下一节叠加验证层——什么时候校、错了怎么展示、提交怎么防重。