問題描述
有一個簡單的表格,產品要求實現雙擊可編輯
看了一下網上的帖子,大多數都是搞兩部分dom,一塊是輸入框,用於編輯狀態填寫
;另一塊是普通標籤,用於在不編輯顯示狀態下呈現單元格文字內容
。再加上一個flag標識搭配v-if和v-else
去控制編輯狀態、還是顯示狀態。大致程式碼如下:
<el-table-column
align="center"
label="姓名"
>
<template slot-scope="scope">
<!--isClick就是標識狀態,狀態處於編輯時候,顯示輸入框,狀態屬於呈現狀態就顯示文字內容-->
<el-input v-if="scope.row.isClick" v-model="scope.row.name" @blur="blurFn(scope.row)"></el-input>
<span @click="clickCell(scope.row)" v-else>{{scope.row.name}}</span>
</template>
</el-table-column>
這種方式有其適用場景,但是得每個el-table-column列中都加上el-input和span以及v-if和v-else
。我們嘗試一下動態新增el-input,就是點選那個單元格,給那個單元格新增el-input讓其處於可編輯狀態,然後適時移除即可。這樣的話,很多列的時候,就不用加很多個v-if和v-else啦。我們先看一下效果圖
效果圖
程式碼思路
- 第1步:給el-table繫結雙擊事件
@cell-dblclick='dblclick'
,再雙擊事件的回撥函式中,可以得知點選的是哪一行、那一列、那個單元格dom,以及點選事件。dblclick(row, column, cell, event) {...}
,這個是餓了麼官方提供的,沒啥好說的 第2步:重點來嘍
- 第2.1步:單元格雙擊事件以後,我們首先建立一個el-input標籤,然後把點選的這個單元格的值,作為引數props讓這個el-input接收,這樣的話el-input就會顯示這個單元格的值了,就可以編輯了。
問題一:如何建立一個el-input標籤?
,客官稍等,下方會解答 - 第2.2步:把建立好的el-input標籤替換掉原來的單元格span標籤,這樣的話,就可以看到單元格變成了可輸入的輸入框了。
問題二:如何把新建立的el-input標籤,替換原有的span標籤
,客官稍等,下方會解答 - 第2.3步,當使用者編輯完了點選別處時候,即輸入框失去焦點的時候,再把el-input輸入框標籤移除掉,恢復預設的span標籤(當然失去焦點的時候,就要發請求修改資料了)
問題三:如何移除el-input標籤,並恢復原有的span標籤
,客官稍等,下方會解答
- 第2.1步:單元格雙擊事件以後,我們首先建立一個el-input標籤,然後把點選的這個單元格的值,作為引數props讓這個el-input接收,這樣的話el-input就會顯示這個單元格的值了,就可以編輯了。
- 這樣的話,每次雙擊搞一個input標籤用於修改,每次改完了失去焦點,就恢復預設單元格展示狀態了,功能就實現了
程式碼思路中的三個問題解答
問題一:如何建立一個el-input標籤?
我們知道,如果是建立原生的input標籤並指定一個值,比較簡單,直接:
let input = document.createElement('input') // 建立一個input標籤
input.value = '孫悟空' // 給input標籤賦值
document.body.appendChild(input) // 把input標籤追加到文件body中
不過el-input標籤不能通過上述方式建立,因為document.createElement()方法雖然可以建立出來el-input標籤,但是dom並不認識這個el-input標籤,所以頁面沒有變化。畢竟餓了麼的el-input也是把input標籤做一個二次封裝的
所以,這裡我們可以使用Vue.extend()方法去繼承一個元件並暴露出去,而繼承的這個元件中又有一個input標籤,所以那個需要使用,那裡就可以引入並new出來一個el-input了
。關於Vue.extend()的定義啥的,這裡不贅述,詳情看官方文件。筆者之前也寫過一篇Vue.extend文章,傳送門:https://segmentfault.com/a/11...
首先搞一個.vue檔案,用於繼承
// input.vue檔案
<template>
<div class="cell">
<el-input
ref="elInputRef"
size="mini"
v-model.trim="cellValue"
></el-input>
</div>
</template>
props: {
cellValue: {
type: String | Number,
default: "",
},
}
然後定義一個data.js檔案,繼承input.vue檔案,並暴露
// data.js
import Vue from "vue";
import definedInput from "./input.vue";
// vue繼承這個input元件,就相當於一個建構函式了
const inputC = Vue.extend(definedInput);
// 暴露出去,哪裡需要哪裡引入
export default {
inputC,
}
頁面中引入並使用
// page.vue
import extendComponents from "./threeC/data"; // 1. 引入
new extendComponents.inputC({ // 2. 例項化
propsData: {
// 使用propsData物件傳遞引數,子元件在props中可以接收到
cellValue: cellValue, // 傳遞單元格的值
},
}).$mount(cell.children[0]);// 3. 掛載
propsData物件用於給繼承的元件傳遞引數,也可以傳遞一個函式,從而繼承元件通過這個函式通知外部使用元件,詳情見後續完整程式碼
問題二三:el-input標籤和span標籤的來回替換恢復
使用$mount
方法去做來回替換,$mount
可以把一個子dom元素追加到父dom元素內部,相當於appendChild
然後這裡需要有一個替換的時機,就是例項化的元件中的el-input失去焦點的時候,去通知外部使用的元件,所以可以在外部使用是,在propsData中傳遞一個函式到繼承的元件,如:
// 外部元件傳遞
new extendComponents.inputC({
propsData: {
cellValue: cellValue, // 傳遞單元格的值
saveRowData: this.saveRowData, // 傳遞迴調函式用於通知,繼承元件中可以觸發之
},
}).$mount(cell.children[0]);
saveRowData(params){
console.log('收到繼承元件訊息通知啦引數為:',params)
}
// 內部元件失去焦點時候通知
<el-input
ref="elInputRef"
size="mini"
v-model.trim="cellValue"
@blur="blurFn"
></el-input>
props: {
cellValue: {
type: String | Number,
default: "",
},
saveRowData: Function, // 外部,傳遞進來一個函式,當這個el-input失去焦點的時候,通過此函式通知外部
}
blurFn() {
// 失去焦點,再丟擲去,通知外部
this.saveRowData({
cellValue: this.cellValue,
// 其他引數
});
},
所以當內層失去焦點的時候,就可以通知外層去做一個替換了,就是把單元格dom重新做一個$mount
掛載,就把el-input替換成了span了,為了進一步理解,這裡的span我們也可以使用繼承的方式,是new例項化使用,詳情見下方完整程式碼
完整程式碼
目錄結構
threeC
-- data.js
-- input.vue
-- span.vue
three.vue
用於繼承的el-input元件
input.vue
<template>
<div class="cell">
<el-input
ref="elInputRef"
size="mini"
v-model.trim="cellValue"
@blur="blurFn"
></el-input>
</div>
</template>
<script>
export default {
props: {
cellValue: {
type: String | Number,
default: "",
},
saveRowData: Function, // 外部,傳遞進來一個函式,當這個el-input失去焦點的時候,通過此函式通知外部
cellDom: Node, // 單元格dom
row: Object, // 單元格所在行資料
property: String, // 單元格的key
},
mounted() {
// 使用者雙擊後,讓其處於獲取焦點的狀態
this.$refs.elInputRef.focus();
},
methods: {
blurFn() {
// 失去焦點,再丟擲去,通知外部
this.saveRowData({
cellValue: this.cellValue,
cellDom: this.cellDom,
row: this.row,
property: this.property,
});
},
},
};
</script>
<style>
.cell {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
padding: 0 8px;
}
</style>
用於繼承的span元件
span.vue
<template>
<span class="cell">{{ cellValue }}</span>
</template>
<script>
export default {
props: {
cellValue: {
type: String | Number,
default: "",
},
},
};
</script>
統一繼承並暴露data.js檔案
import Vue from "vue";
import definedInput from "./input.vue";
import definedSpan from "./span.vue";
const inputC = Vue.extend(definedInput);
const spanC = Vue.extend(definedSpan);
export default {
inputC,
spanC,
}
使用繼承的three.vue元件
<template>
<div id="app">
<el-table
@cell-dblclick="dblclick"
:cell-class-name="cellClassName"
height="480"
:data="tableData"
border
>
<el-table-column align="center" type="index" label="序號" width="50">
</el-table-column>
<el-table-column align="center" prop="name" label="姓名" width="100">
</el-table-column>
<el-table-column align="center" prop="age" label="年齡" width="100">
</el-table-column>
<el-table-column align="center" prop="home" label="家鄉">
</el-table-column>
</el-table>
</div>
</template>
<script>
// 引入繼承元件物件,可取其身上的inputC建構函式、或spanC建構函式生成元件dom
import extendComponents from "./threeC/data";
export default {
data() {
return {
tableData: [
{
name: "孫悟空",
age: 500,
home: "花果山水簾洞",
},
{
name: "豬八戒",
age: 88,
home: "高老莊",
},
{
name: "沙和尚",
age: 1000,
home: "通天河",
},
],
/**
* 存一份舊的值,用於校驗是否發生變化,是否修改
* */
oldCellValue: null,
};
},
methods: {
cellClassName({ row, column, rowIndex, columnIndex }) {
row.index = rowIndex; // 自定義指定一個索引,下方能夠用到
},
dblclick(row, column, cell, event) {
// 1. 序號列單元格不允許編輯,別的列單元格可以編輯
if (column.label == "序號") {
this.$message({
type: "warning",
message: "序號列不允許編輯",
});
return;
}
// 2. 存一份舊的單元格的值
this.oldCellValue = row[column.property];
// 3. 然後把單元格的值,作為引數傳遞給例項化的input元件
let cellValue = row[column.property];
// 4. 例項化元件以後,帶著引數,再掛載到對應位置
new extendComponents.inputC({
propsData: {
// 使用propsData物件傳遞引數,子元件在props中可以接收到
cellValue: cellValue, // 傳遞單元格的值
saveRowData: this.saveRowData, // 傳遞迴調函式用於儲存行資料,元件中可以觸發之
cellDom: cell, // 傳遞這個dom元素
row: row, // 傳遞雙擊的行的資料
property: column.property, // 傳遞雙擊的是哪個欄位
},
}).$mount(cell.children[0]); // 5. $mount方法,用於將某個dom掛載到某個dom上
},
/**
* 失去焦點的時候有以下操作
* 1. 校驗新值是否等於原有值,若等於,說明使用者未修改,就不發請求。若不等於就發請求,然後更新tableData資料
* 2. 然後使用$mount方法,掛載一個新的span標籤dom在頁面上,即恢復原樣,而span標籤也是例項化的哦
* */
saveRowData(params) {
console.log("繼承的子元件傳遞過來的資料", params);
// 1. 看看使用者是否修改了
if (params.cellValue == this.oldCellValue) {
console.log("未修改資料,不用發請求");
} else {
params.row[params.property] = params.cellValue;
// 這裡模擬一下發了請求,得到最新表體資料以後,更新tableData
setTimeout(() => {
// 給那個陣列的 第幾項 修改為什麼值
this.$set(this.tableData, params.row.index, params.row);
}, 300);
}
// 2. 恢復dom節點成為原來的樣子,有下面兩種方式
/**
* 方式一:使用官方推薦的$mount去掛載到某個節點上,上方也是
* */
new extendComponents.spanC({
propsData: {
cellValue: params.cellValue,
},
}).$mount(params.cellDom.children[0]);
/**
* 方式二:使用原生js去清空原節點內容,同時再新增子元素
* */
// let span = document.createElement("span"); // 建立一個span標籤
// span.innerHTML = params.cellValue; // 指定span標籤的內容的值
// span.classList.add("cell"); // 給span標籤新增class為cell
// params.cellDom.innerHTML = ""; // 清空剛操作的input標籤的內容
// params.cellDom.appendChild(span); // 再把span標籤給追加上去,恢復原樣
},
},
};
</script>
<style lang="less" scoped>
#app {
width: 100%;
height: 100vh;
box-sizing: border-box;
padding: 50px;
}
</style>
總結
使用Vue.extend()
方法,可以繼承一些元件,甚至繼承一些複雜的元件,在實際業務場景中會有巧妙的使用。具體業務場景具體分析。
此外,上述程式碼中是el-input的繼承
,其實,我們也可以做el-select的繼承
,思路和上方類似,這樣就可以在表格中雙擊單元格,選擇並更改對應的下拉框更改el-table的單元值了,比如如果有性別這一列,那是下拉框的形式的。道友們可以按照這個思路發散哦...
好記性不如爛筆頭,記錄一下吧 ^_^