前言
在Vue项目开发中,针对复杂表单,特别是根据不同条件渲染不同表单时,我们多半会使用 v-if 进行条件渲染,但是经常会遇到表单的校验问题,下面以 iView (view-design 是 iView 的第四个版本,官方改名)代码举个例子:
// package.json 依赖版本
{
"dependencies": {
"vue": "^2.6.14",
"view-design": "^4.7.0"
}
}
// .vue 文件
<template>
<div>
<Form :model="form">
<FormItem label="选择渲染不同组件" prop="componentType">
<RadioGroup v-model="form.componentType">
<Radio label="1">组件1</Radio>
<Radio label="2">组件2</Radio>
</RadioGroup>
</FormItem>
<template v-if="form.componentType === '1'">
<FormItem label="我是组件1" prop="inputValue1">
<Input
v-model="form.inputValue1"
placeholder="我是组件1"
></Input>
</FormItem>
</template>
<template v-if="form.componentType === '2'">
<FormItem label="我是组件2" prop="inputValue2">
<Input
v-model="form.inputValue2"
placeholder="我是组件2"
></Input>
</FormItem>
</template>
</Form>
</div>
</template>
<script>
export default {
data() {
return {
form: {
componentType: "",
inputValue1: "111111",
inputValue2: "222222",
}
}
}
}
</script>
示例效果:
问题集合
一、添加校验属性 required
假设当选择组件1时,inputValue1 必填,那么就在组件1上增加 required 属性,代码如下:
/// 其他代码
<template v-if="form.componentType === '1'">
// 注意下面这一行,增加了属性 required
<FormItem label="我是组件1" prop="inputValue1" required>
<Input
v-model="form.inputValue1"
placeholder="我是组件1"
></Input>
</FormItem>
</template>
/// 其他代码
示例效果:
先选组件1
1、如果先选择了组件1,再选择组件2,表现异常,inputValue2 的值被赋值为 inputValue1 的值
打开 Vue DevTools 面板可以查看到数据已经被改变了
2、如果先选择组件2,再选择组件1,则表现正常
二、原因
我们知道 vue 本身的 diff 算法是复用机制,第一次渲染了 FormItem ,后续切换内部实际上复用了上一次的,只是属性的正常替换,如果不想复用,需要添加不同的 key 属性,官方文档。
组件本身复用好理解,但从上面的表现来看,先后的选择引起的复用,却是不同的表现,那么差异点只是在组件1多了 required 属性,那么我们进入 view-design 的源码查看,可以看到 watch 中对 required 属性实现了监听。
简单来说就是 required 属性 从 true 到 false,会执行一个回调事件 resetField,该事件的主要作用,是重置该表单项的校验,并给绑定的 prop 属性重新赋值 initialValue
然后 initialValue 是在组件 mounted 中被赋值为 fieldValue,fieldValue 是组件对应的实时的值
那么结合上面的例子,流程就是:
// 先选择组件1,第一次渲染 FormItem
{
prop: "inputValue1",
fieldValue: "111111",
initialValue: "111111",
required: true,
}
// 切换到组件2,复用 FormItem
// 因为生命周期 mounted 只执行一次,所以 initialValue 赋值后就不再改变
{
prop: "inputValue2",
fieldValue: "222222",
initialValue: "111111",
required: false,
}
// 此时触发了 required 的 watch 监听,执行了 resetField 函数,
// 将 initialValue 赋值给了对应的 prop 属性字段,结果就是
{
prop: "inputValue2",
fieldValue: "111111",
initialValue: "111111",
required: false,
}
// 表现异常,出现 bug
// ------------------分割线------------------
// 先选择组件2,第一次渲染 FormItem
{
prop: "inputValue2",
fieldValue: "222222",
initialValue: "222222",
required: false,
}
// 切换到组件1,复用 FormItem
// 因为生命周期 mounted 只执行一次,所以 initialValue 赋值后就不再改变
// 此时触发了 required 的 watch 监听,但判断为 false,不会执行了 resetField 函数
{
prop: "inputValue1",
fieldValue: "111111",
initialValue: "222222",
required: true,
}
// 再切换到组件2,继续复用 FormItem
// 此时触发了 required 的 watch 监听,执行了 resetField 函数,
// 将 initialValue 赋值给了对应的 prop 属性字段,结果就是
{
prop: "inputValue2",
fieldValue: "222222",
initialValue: "222222",
required: false,
}
// 表现正常
iView 出现这个问题,那我们继续对 element-ui 进行同样的测试,结果却发现并没有这样的问题。这里就不贴代码了,感兴趣的伙伴可以自行验证。
"element-ui": "^2.15.6"
然后 required 属性会出现问题,那我们采用 rules 属性是否会出现类似的问题呢,答案是不会出现。
综上所述,这是 view-design 的 bug。
三、解决方案
1、给表单项增加不同的属性 key,既然组件复用会导致这个问题,那我们直接不复用组件,就能从根源解决问题了,同时这也是解决表单校验的终极解决方案;对 iView 和 ElementUI 都适用。(推荐使用)
2、不用 required 属性,而改用 rules 属性,这个就不会引起重置的回调,也能解决上述问题,但是不能解决表单校验的问题,如果规则没有加入条件判断,那么规则也会被加入校验数组中,这个在源码中有个 fields 字段记录了校验方法,会导致提交的时候,无法通过校验。页面上可能没有校验提示,遇到这种情况,可以在表单的 validate 回调中打印错误信息就知道了。(一般推荐)
// 其他代码
<template v-if="form.componentType === '1'">
<FormItem
label="我是组件1"
prop="inputValue1"
:rules="
form.componentType === '1'
? { required: true }
: {}
" // <- 注意这一行,rules 里面也增加了条件判断
>
<Input
v-model="form.inputValue1"
placeholder="我是组件1"
></Input>
</FormItem>
</template>
// 其他代码
3、用 v-show 替换 v-if,v-show 实际上组件已经渲染,只是不显示,也做到了不复用,但是也会存在和第2点一样的问题,就是如果先选择了组件1,然后切换到组件2,在最后提交的时候,会发现无法提交数据,页面也没展示校验报错,原因是:尽管组件的 v-show 为 false,但实际上还是参与了校验,只是页面上没有显示,所以提交不了。(不推荐)