試想一種比較復雜的業務場景:
表格(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>