试想一种比较复杂的业务场景:
表格(el-table)的每一行数据的第一列是勾选框,最后一列是输入框。当某一行的勾选框勾上时,启用该行的输入框,并开启该行输入框的表单验证;取消该行的勾选框,则禁用该行的输入框,并禁用该行输入框的表单验证。
思路分析
动态表单验证
这里显然是一个数据遍历产生的动态表单验证问题,并且与el-table相结合。动态表单验证主要的难点在于表单项的prop
属性的设置问题,由于是el-table中的表单,只需要使用scope.$index
传递给prop,并将prop设置成形如"'productList.' + scope.$index + '.bidPrice'"
的形式即可。具体可参见elementUI官网示例,这里不再赘述。
输入框启用和禁用切换
推荐的一个思路是,每当勾选项改变时,就设置每行数据的标志属性,如checked
,并且每行输入框与该行数据的checked
属性相帮定。每行数据原本并没有checked
属性,即为undefined
,因而输入框默认为禁用的。需要注意的是,我们需要使用$set
来增加和修改该属性以实现响应式。当勾选项变化之时,根据勾选状态设置每行数据checked的值为true
或false
。
难点所在:输入框启用和禁用表单验证的切换
在上面的应用场景中,输入框是否启用验证是随着该行的勾选框状态切换而切换的,如果我们给el-input的上一层el-form-item设置固定的prop值,显然是不能达成目标的,无论启用或禁用,都会开启验证,难点在于如何让表单验证也能动态切换。
常见解决方案分析
1.给el-form-item的prop绑定一个计算属性。
问题在于:
由于prop需要随着切换而变化,因此计算属性也需要传递参数,但是计算属性一般是不传递参数
的,为解决参数问题,容易陷入“奇技淫巧”之中。个人认为,更关键的问题在于,prop在表单项
渲染的时候就已经确定,即便后来改了prop的值,由于表单没有重新渲染,因此不会生效。
2.给el-form-item的prop绑定一个函数,由函数计算prop值。
问题同样在于:
prop在表单项渲染的时候就已经确定,即便后来改了prop的值,由于表单没有重新渲染,
因此不会生效。
3.设置两个el-form-item,一个启用表单验证,一个不启用表单验证,两个基于v-if进行条件渲染,v-if绑定该行的checked属性。
该法理论上是可行的,并且思路清晰,比较简单。然而实际使用的时候却容易忽略Vue的复用机
制,出现没有重新渲染的问题,导致失败,进而怀疑v-if不能解决该问题。后面就会看到,本文
实际上就是使用了该法。
4.设置两个el-form-item,一个启用表单验证,一个不启用表单验证,两个基于v-show进行切换,v-show绑定该行的checked属性。
问题在于:
v-show不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。因而
即使只显示了不启用表单验证的el-form-item,另一个启用了表单验证的el-form-item的
验证也会生效,这种方案行不通。
5.自行遍历数据,进行全局提示,不使用elementUI的表单验证。
问题在于:
该方案较为复杂,需要根据数据进行大量的逻辑判断,实质上会比使用ElementUI的表单验证更为
复杂,并且只能进行全局消息提示,不能针对出错的某一行进行提示。
推荐解决方案: 基于v-if切换,但必须设置key属性
(一)方案简介
我们只需要写两个除了是否开启表单验证之外,完全相同的输入框表单项,使他们基于v-if进行条件渲染,并且切换时强制使他们重新渲染,就能实现业务需求。为了强制重新渲染,我们需要设置key属性,否则组件会复用,导致失败。
(二)条件渲染和复用
Vue官方文档指出,
v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建
适当
的含义在于:
- 若组件能够复用,比如v-if和v-else对应的组件几乎相同,只有极少数属性有差别,那么切换时,组件不会重新渲染,以实现复用。好处在于可以提高性能。
- 若组件不能复用,比如v-if和v-else对应的组件差距太大,或者仅有v-if语句,或者强制使用key属性告诉vue v-if和v-else对应的组件是两个组件,不要复用;这样,当条件变化时,组件会重新渲染,包括事件也会重新绑定。
(三)key属性的作用
Vue为了尽可能高效地渲染元素以提高性能,通常会复用已有元素而不是从头开始渲染。这样也不总是符合实际需求,我们可能需要明确告诉Vue“这两个元素是完全独立的,不要复用它们”。具体方式是,只需添加一个具有唯一值的 key 属性即可。
- 由上可知,当我们不添加key属性的时候,由于Vue的复用机制,将使得v-if和v-else之间的元素可能不会重新渲染,如果要强制重新渲染,则必须使用key属性。
完整案例代码(可直接运行)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<style>
.btn-container{
margin-top: 40px;
text-align: center;
}
.btn-container .confirm-btn{
min-width: 70px;
}
</style>
</head>
<body>
<div id="app">
<el-form :model='formData' :rules='formRules' ref='bidForm'>
<el-table :data='formData.productList' @selection-change='handleSelectChange'>
<el-table-column type='selection'></el-table-column>
<el-table-column label='名称' prop='name'></el-table-column>
<el-table-column label='初始价格' prop='initPrice'></el-table-column>
<el-table-column label='报价' width='180'>
<template slot-scope='scope'>
<el-form-item v-if='!scope.row.checked' key='noProp'>
<el-input size='small' v-model='scope.row.bidPrice' :disabled='!scope.row.checked' placeholder="<=50"></el-input>
</el-form-item>
<el-form-item :prop="'productList.' + scope.$index + '.bidPrice'" :rules='formRules.bidPrice' v-else key='hasProp'>
<el-input size='small' v-model='scope.row.bidPrice' :disabled='!scope.row.checked' placeholder="<=50"></el-input>
</el-form-item>
</template>
</el-table-column>
</el-table>
</el-form>
<div class="btn-container">
<el-button type='primary' class="confirm-btn" @click='confirmBid'>确认投标</el-button>
</div>
</div>
<script>
var app = new Vue({
el:'#app',
data:{
formData:{
productList:[{
id:0,
name:'土豆',
initPrice:3.2
},{
id:1,
name:'西红柿',
initPrice:4.2
}],
},
formRules:{
bidPrice:[{
validator(rule,value,callback){
if (value === '' || value === undefined) {
callback(new Error('请输入投标价格!'));
} else if (isNaN(value)) {
callback(new Error('请输入数字值!'));
} else if (Number(value) > 50 || Number(value) <= 0) {
callback(new Error('需大于0,小于等于50'));
} else if (/\.\d{4}/.test(value)) {
callback(new Error('最多带3位小数'));
} else {
callback();
}
},
trigger:'blur'
}]
},
selectArr:[]
},
methods:{
// 选中项改变
handleSelectChange(arr){
this.selectArr = arr;
// 给每项增加一个表示当前是否选中的标志属性
this.formData.productList.forEach(item => {
let checked = false;
arr.forEach(e => {
if(e.id === item.id){
checked = true;
};
});
if(checked){
this.$set(item,'checked',true);
}else{
this.$set(item,'checked',false);
}
});
},
// 点击确认
confirmBid(){
this.$refs.bidForm.validate(valid => {
if(valid){
alert(111);
}
});
}
}
});
</script>
</body>
</html>