前言
國慶去了一趟西藏,那邊的風景很贊?,但是天天都會頭疼?,不禁感慨,還是寫文章好啊✍,So,今天要和大家分享的是 table 元件的實現,是從 0 到 1 的實現哦,這個元件對於我們來說應該是挺複雜的一個了,看過那麼多個初級元件,是時候裝個叉了?。
知識回顧
表格這東西我們肯定都接觸過,尤其是在開發後臺管理系統的時候,不過大部分都是直接用 UI 框架寫的,久了都不知道原來是怎麼寫的了,所以在此我們先回顧一下?最原始的表格的基礎寫法:
<table border="1">
<colgroup>
<!-- 這裡可以針對每列做一些屬性設定,例如設定寬度,這個我還真不知道,也許曾經看過但忘了 -->
<col width="200">
<col width="150">
<col width="100">
</colgroup>
<thead>
<tr>
<th>姓名</th>
<th>職位</th>
<th>等級</th>
</tr>
</thead>
<tbody>
<tr>
<td>尤水就下</td>
<td>前端</td>
<td>小菜</td>
</tr>
</tbody>
</table>
複製程式碼
上面的表格渲染出來大概是下面這個樣子:
其中要注意的就是<col>
了,因為這個東西對我來說是挺陌生的(其實我不知道
?),然後就搜了一下,原來它是方便我們給每個列設定一些共同屬性的,比如寬度。因為後續我們會用到它,所以請務必記住?。
目標
首先簡要說下我們本篇文章要實現的東西:基礎展示 + 全選 + 排序 + 展開 + 自定義內容 + 固定表頭 +(多級表頭 + 固定列)。然後話不多說,擼起袖子就是幹?。
基礎展示
作為一個表格,展示資料是必備的功能了,當然它很容易實現,但更為重要的是 api 的設計,一個好的 api 能夠讓你事半功倍,所以在借鑑了各大框架的 api 之後,我們希望開發是這樣使用我們元件的?:
<template>
<div id="app">
<xr-table :columns="columns" :data="data"></xr-table>
</div>
</template>
<script>
import XrTable from './components/xr-table';
export default {
components: {
XrTable
},
data() {
return {
columns: [
{
title: '姓名',
key: 'name'
},
{
title: '年齡',
key: 'age'
},
{
title: '職位',
key: 'job'
}
],
data: [
{
id: 1,
name: 'Jasmine',
age: 18,
job: '產品',
desc: '這是展開的描述啊1'
},
{
id: 2,
name: 'Mango',
age: 18,
job: '設計',
desc: '這是展開的描述啊2'
},
{
id: 3,
name: 'Aking',
age: 24,
job: '前端',
desc: '這是展開的描述啊3'
},
{
id: 4,
name: 'Dick',
age: 30,
job: '後端',
desc: '這是展開的描述啊4'
},
{
id: 5,
name: 'Lucy',
age: 18,
job: '測試',
desc: '這是展開的描述啊5'
}
]
};
}
};
</script>
複製程式碼
也就是你給負責給資料(columns
要求有要有 key
、data
要求要有 id
),我來渲染,這部分其實沒有難度,加上點樣式表格就挺漂亮了,這裡就直接上程式碼了:
<template>
<div class="xr-table">
<table>
<thead>
<tr>
<!-- 表頭迴圈 -->
<th v-for="col in columns" :key="col.key">{{col.title}}</th>
</tr>
</thead>
<tbody>
<!-- 表體迴圈 -->
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">{{row[col.key]}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<style lang="scss">
.xr-table {
table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
empty-cells: show;
border: 1px solid #e9e9e9;
}
table th {
background: #f7f7f7;
color: #5c6b77;
font-weight: 600;
white-space: nowrap;
}
table td,
table th {
padding: 8px 16px;
border: 1px solid #e9e9e9;
text-align: left;
}
}
</style>
複製程式碼
其實就是把 th
和 td
迴圈一遍,應該不用過多解釋?。然後執行程式碼,看下效果:
全選
接下來我們需要給表格新增全選功能,首先還是看 api 咋弄,經過參考之後,我們可以在傳進來的 columns
裡面做文章,並在勾選的時候觸發一個 on-selection-change
事件,就像下面這樣?:
<template>
<xr-table :columns="columns" :data="data" @on-selection-change="onSelectionChange"></xr-table>
</template>
<script>
export default {
data() {
return {
columns: [
{
type: 'selection' // 這個地方可以不用寫 key,type 就相當於 key
}
...
]
}
}
}
</script>
複製程式碼
說實話我覺得這個 api 設計是挺巧妙的,不需要我們像 Element 那樣寫:
<el-table>
<el-table-column
type="selection"
width="55">
</el-table-column>
</el-table>
複製程式碼
然後元件裡面該怎麼改呢?也很簡單,在迴圈 th
和 td
的時候先判斷是否有 type
,如果有 type
並且值為 selection
就渲染成核取方塊,就像下面這樣?:
<template>
<div class="xr-table">
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">
<div>
<!-- 在這裡先判斷 type -->
<template v-if="col.type === 'selection'">
<input ref="allCheckbox" type="checkbox" :checked="isSelectAll" @change="selectAll">
</template>
<template v-else>{{col.title}}</template>
</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
<div>
<!-- 在這裡先判斷 type -->
<template v-if="col.type === 'selection'">
<input
type="checkbox"
:checked="formateStatus(row)"
@change="toggleSelect($event, row)"
>
</template>
<template v-else>{{row[col.key]}}</template>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
複製程式碼
當然了,上面的程式碼只是把核取方塊渲染出來而已,現在我們還需要為其加上點選的邏輯。這裡我們在元件裡面維護了一個 selectedRows
欄位,我們把當前的已選行都放入到這個陣列中來,每次點選核取方塊都會修改 selectedRows
的值。要注意的是表頭核取方塊的值 isSelectAll
是根據所有資料 data
和已選行 selectedRows
進行比較得到的,所以 isSelectAll
應該寫在計算屬性裡面,就像下面這樣?:
<script>
export default {
...
data() {
return {
selectedRows: [] // 當前已選中的行
};
},
computed: {
isSelectAll() { // 表頭全選的勾選狀態應該根據當前已選的來計算,最好不要直接比較陣列長度是否相等,而是應該在比較長度的基礎上比較每一項的 id 是否一樣,雖然目前看起來這個步驟很多餘
let all = this.data.map(item => item.id).sort();
let selected = this.selectedRows.map(item => item.id).sort();
let isSelectAll = true;
if (all.length === selected.length) {
for (let i = 0, len = all.length; i < len; i++) {
if (all[i] !== selected[i]) {
isSelectAll = false;
break;
}
}
} else {
isSelectAll = false;
}
this.$nextTick(() => { // 這個是選了部分之後把表頭的核取方塊改變成中間狀態(就是橫槓的狀態)
this.$refs['allCheckbox'][0].indeterminate =
selected.length && !isSelectAll;
});
return isSelectAll;
}
},
methods: {
selectAll(e) { // 單擊表頭的多選框並向外觸發事件
let checked = e.target.checked;
this.selectedRows = checked ? JSON.parse(JSON.stringify(this.data)) : [];
this.$emit('on-selection-change', this.selectedRows);
},
toggleSelect(e, row) { // 單擊表體的多選框並向外觸發事件
let checked = e.target.checked;
if (checked) {
this.selectedRows.push(row);
} else {
let idx = this.selectedRows.findIndex(item => item.id === row.id);
this.selectedRows.splice(idx, 1);
}
this.$emit(
'on-selection-change',
JSON.parse(JSON.stringify(this.selectedRows))
);
},
formateStatus(row) { // 表體的每個多選框是否被勾選
return this.selectedRows.findIndex(item => item.id === row.id) >= 0;
}
}
};
</script>
複製程式碼
上面程式碼裡面的註釋應該解釋的還算清楚,下面看下實現的效果:
應該還行吧!排序
現在我們需要給表格加上排序功能,還是照舊先看一下 api 的設計,其實它和全選功能異曲同工,我們還是在 columns
裡面做文章,然後支援向外觸發on-sort
事件即可,就像下面這樣?:
<template>
<div id="app">
<xr-table
:columns="columns"
:data="data"
@on-sort="onSort"
></xr-table>
</div>
</template>
<script>
export default {
...
data() {
return {
columns: [
...
{
title: '年齡',
key: 'age',
sortable: true
}
...
]
}
}
}
複製程式碼
不過這裡我們並沒有真正的進行排序操作?,因為排序更應該是偏向讓後端做,前端負責觸發事件、呼叫介面、得到資料、重新整理表格即可,畢竟單純的前端排序沒有什麼太大意義。那什麼時候可能需要前端排序呢,就是資料量不大,前端一次拿到所有資料的情況下,這樣可以省去呼叫介面的時間,不過能推給後端就推吧?,下面我們來看下程式碼實現:
...
<template v-if="col.type === 'selection'">
<input ref="allCheckbox" type="checkbox" :checked="isSelectAll" @change="selectAll">
</template>
<template v-else>
<!-- 改動在這裡 -->
<span>{{col.title}}</span>
<span v-if="col.sortable">
<i @click="handleSort(col.key, 'asc')">↑</i>
<i @click="handleSort(col.key, 'desc')">↓</i>
</span>
</template>
...
<script>
export default {
...
methods: {
handleSort(key, sortType) {
this.$emit('on-sort', { key, sortType });
}
}
}
</script>
複製程式碼
雖然很簡單,但是這裡寫排序的主要目的是,讓我們的表格支援在表頭增加一些圖示和自定義事件,其執行結果如下:
也還 ok 吧!展開
展開其實和上面的兩個功能一樣,api 需要在 columns
裡面做文章:
<script>
export default {
...
data() {
return {
columns: [
{
type: 'expand'
}
...
]
}
}
}
複製程式碼
這裡我們打算維護一個 expandIds
,儲存所有展開行的資訊,和全選的 selectedRows
一毛一樣。但其實也可以有另一種做法:就是我們預先把傳進來的資料處理一下,在 data
裡面的每一行加上 isExpand
和 isSelect
欄位,然後操作的時候進行相應的狀態修改即可,就不需要再額外宣告陣列了。但是怎麼實現我不管,做出來才是硬道理,所以請看下面的程式碼吧?:
...
<template v-if="col.type === 'selection'">
<input ref="allCheckbox" type="checkbox" :checked="isSelectAll" @change="selectAll">
</template>
<!-- 改動在這裡 -->
<template v-else-if="col.type === 'expand'"></template>
...
<tbody>
<template v-for="row in data">
<tr :key="row.id">
<td v-for="col in columns" :key="col.key">
...
</td>
</tr>
<!-- 這裡多加了一個是否展開的判斷 -->
<tr :key="`expand-${row.id}`" v-if="checkIsExpand(row.id)">
<!-- 橫跨所有列 -->
<td :colspan="columns.length">{{row.desc}}</td>
</tr>
</template>
</tbody>
...
<script>
export default {
...
data() {
return {
expandIds: []
};
},
methods: {
...
toggleExpand(id) {
let idx = this.expandIds.indexOf(id);
if (idx >= 0) {
this.expandIds.splice(idx, 1);
} else {
this.expandIds.push(id);
}
},
checkIsExpand(id) {
return this.expandIds.indexOf(id) >= 0;
}
}
}
</script>
複製程式碼
要注意的是展開的內容是需要橫跨所有行的,所以我們得把 colspan
的值設定成 columns.length
,也就是橫跨所有列的意思。當然目前我們是寫死了展開的欄位內容 desc
,一會我們會支援自定義。這裡我們也不注重樣式,畢竟不是重點,然後執行一下,看下效果?:
自定義內容
說講就講?,寫到這裡其實我們已經支援最基礎的表格需要,但問題是現在的表格也太基礎了吧,要是我想自定義內容咋辦(撓頭三連?),好歹支援一下吧。所以接下來我們需要 do it!
首先想都不用想,這個東西肯定是要用插槽的,但我們還是要從 api 的設計入手,現在假設我們要在年齡這一列的後面加上一個“歲”字,並且在最後一列增加編輯和刪除兩個按鈕,我們期待的應該是這樣的用法?:
<template>
<xr-table
:columns="columns"
:data="data">
<!-- 其中 age 是對應的插槽名,{row, col, index} 對應的是行、列、索引這三個引數 -->
<template v-slot:age="{row, col, index}">{{ row.age + '歲'}}</template>
<template v-slot:action="{row, col, index}">
<button>編輯{{index}}</button>
<button>刪除</button>
</template>
</xr-table>
</template>
<script>
export default {
...
data() {
return {
columns: [
...
{
title: '年齡',
slot: 'age', // 寫了 slot 也可以不用寫 key,因為它相當於 key
sortable: true
},
...
{
title: '操作',
slot: 'action'
}
]
}
...
}
複製程式碼
我們在 columns
裡面加了個 slot
欄位,它用來表明表格的某一列是否需要自定義內容,然後在 <xr-table></xr-table>
裡面寫了個形如 <template v-slot:age="{row, col, index}">{{ index }}</template>
這樣的一個東西,其中 age
是對應的插槽名,{row, col, index}
對應的是行、列、索引這三個引數,不知道大家對這個東西熟不熟悉,它其實就是 slot-scope
的新寫法,讓我們來看下官網的說明(不瞭解的同學可以去看下使用方法,並不難):
v-slot
插槽真是個好用的東西,本來寫自定義內容還是挺繁瑣的,v-slot
讓實現變得簡單了,此外它還有個簡寫方式,可以用 #
代替 v-slot
,就像 @
代替 v-on
一樣,也就是 <template #age="slotProps">{{ index }}</template>
。好了,現在我們來看下元件裡的程式碼具體怎麼寫,其實只需要對
tbody
裡面進行更改即可,沒有想象中那麼難,因為改動並不大,所以這裡直接上程式碼:
<tbody>
<template v-for="(row, index) in data">
<tr :key="row.id">
<td v-for="col in columns" :key="col.key">
<div>
<!-- 改動在這裡,我們我先判斷列是否有 slot 欄位 -->
<template v-if="col.slot">
<!-- row,col,index 是我們需要主動傳出去的引數,這樣在外面的 slotProps 才能擁有這幾個引數,當然我們還可以傳其他引數,他們最終都會被放在 slotProps 這一個物件裡面 -->
<slot :name="col.slot" :row="row" :col="col" :index="index"></slot>
</template>
<template v-else-if="col.type === 'selection'">
...
</template>
...
</div>
</td>
</tr>
...
</template>
</tbody>
複製程式碼
最後渲染出來就是我們要的模樣了,如下圖所示?:
同樣的道理,我們也可以稍稍修改下展開行使之能夠支援自定義,大家可以嘗試一下,這裡就不做展示羅?。固定表頭
所謂固定表頭就是表頭不動,但表體可以滾動,這個東西實現起來就有點小麻煩了?,為什麼呢?因為本來我們的表格剛好是由 thead
和 tbody
兩部分組成,常規的思路就是把 tbody
給個高度,然後讓它溢位滾動即可,但是事情沒那麼簡單,height
和 overflow
這兩個樣式寫在 tbody
或者 table
上都是木有效果滴,So,我們就得去看看人家是咋做的啦(以 Element 為例):
thead
和 tbody
分別被放在了兩個不同的 div
的 table
裡面,然後設定 tbody
的外層 div
可以滾動就行了。嗯,真實直白的想法?。其實其它幾大 UI 框架也是一樣的(就是分開寫)。那麼既然人家已經實踐過了,說明該方案是可行的,至少相容性是槓桿的,於是我們在集百家之長之後?,就可以開始動手寫屬於自己的程式碼了。當然了,第一步還是 api 的設計,這回我們只需要在標籤裡傳入 height
引數即可:
<xr-table
:columns="columns"
:data="data"
height="150"
></xr-table>
複製程式碼
接下來我們需要改變一下元件的內部結構,把它轉換成兩段式的寫法。事實上表格的 thead
和 tbody
本來就是分開的,所以拆出來並不難,就像下面這樣?:
<template>
<!-- 這裡只是單純改了結構,裡面的 tr 並沒有改變 -->
<div class="xr-table">
<div class="xr-table__header">
<table>
<thead>...</thead>
</table>
</div>
<div class="xr-table__body">
<table>
<tbody>..</tbody>
</table>
</div>
</div>
</template>
複製程式碼
儲存一下程式碼,目前效果圖如下:
嗯,和原來的並沒有什麼差別,但是第一個問題來了,thead
的寬度和 tbody
的寬度對不齊了,這可咋整啊。還能咋整啊,就定寬唄?,通過 columns
傳入每列的 width
值就好了(當然可以有一列是可以不用給寬度的,這樣可以保持彈性),然後把 thead
和 tbody
的每列設定成一樣寬就行了,你可能會覺得麻煩,但事實上這能很好的避免其他一些不必要的問題,這裡我給大家截了 Ant Design 和 Element 兩個官網上的圖:
你可以清楚的看到他們也是需要 width
的,所以這裡我們需要給 columns
多加上一個欄位 width
,形如這樣 {title: '姓名', key: 'name', width: 100}
,這裡的單位預設是 px
。然後怎麼設定寬度呢?哈哈?,這裡就要用到我們在開篇提到的 <colgroup>
裡面的 <col>
啦,這個東西用來設定寬度實在是恰好不過了。元件裡要改的地方也不多,把寬度加上就行了,就像下面這樣?:
<div class="xr-table__header">
<table>
<colgroup>
<col v-for="col in columns" :width="col.width || ''">
</colgroup>
...
</table>
</div>
<div class="xr-table__body">
<table>
<colgroup>
<col v-for="col in columns" :width="col.width || ''">
</colgroup>
...
</table>
</div>
複製程式碼
這裡我們把倒數第二列的寬度留空,然後看一下效果:
恩,好像挺好?,那就繼續吧。接下來要做的就是讓表體定高並滾動了。首先我們傳進來的height
應該是整個表格的高度,所以要在最外層的 xr-table
要加上 height: 150px; overflow: hidden
的樣式,然後需要計算並設定 xr-table__body
的高度(總高 - 表頭)以及 overflow
的值,這樣表體就能滾動了。具體看下面的程式碼,其實改動也不多:
<template>
<!-- 加了個 tableStyle -->
<div class="xr-table" :style="tableStyle">
<!-- 加了個 ref -->
<div class="xr-table__header" ref="tableHeader">...</div>
<div class="xr-table__body" ref="tableBody">...</div>
</div>
</template>
<script>
export default {
...
mounted() {
let { tableHeader, tableBody } = this.$refs;
let headerH = parseInt(window.getComputedStyle(tableHeader).height);
let bodyH = this.height - headerH;
tableBody.style.height = `${bodyH}px`;
},
computed: {
tableStyle() {
return this.height ? `height: ${this.height}px` : '';
}
}
...
}
</script>
<style lang="scss">
.xr-table {
overflow: hidden;
&__body {
overflow: auto;
}
}
</style>
複製程式碼
儲存執行一下,效果如下:
嗯,也還不錯,雖然樣式有點瑕疵,但這不重要,畢竟功能已經實現了✌。需要看原始碼的可以狠狠點選這裡:table元件。six six six,大讚無疆 ???
結語
寫到這裡,可能內容有點多了,所以我們把多級表頭和固定列留到下一篇講,希望這個月下旬能寫好吧?。雖然寫的東西比較簡單,不過講的是從 0 開始的過程,希望能對大家有所幫助,回見?。