table 元件瞭解一下?

尤水就下發表於2019-11-06

前言

國慶去了一趟西藏,那邊的風景很贊?,但是天天都會頭疼?,不禁感慨,還是寫文章好啊✍,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>
複製程式碼

上面的表格渲染出來大概是下面這個樣子:

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 要求有要有 keydata 要求要有 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>
複製程式碼

其實就是把 thtd 迴圈一遍,應該不用過多解釋?。然後執行程式碼,看下效果:

table 元件瞭解一下?
這樣,一個簡簡單單的表格就有了,當然了,我們的目標不止於此。

全選

接下來我們需要給表格新增全選功能,首先還是看 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>
複製程式碼

然後元件裡面該怎麼改呢?也很簡單,在迴圈 thtd 的時候先判斷是否有 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>
複製程式碼

上面程式碼裡面的註釋應該解釋的還算清楚,下面看下實現的效果:

table 元件瞭解一下?
應該還行吧!

排序

現在我們需要給表格加上排序功能,還是照舊先看一下 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>
複製程式碼

雖然很簡單,但是這裡寫排序的主要目的是,讓我們的表格支援在表頭增加一些圖示和自定義事件,其執行結果如下:

table 元件瞭解一下?
也還 ok 吧!

展開

展開其實和上面的兩個功能一樣,api 需要在 columns 裡面做文章:

<script>
export default {
  ...
  data() {
    return {
      columns: [
        {
          type: 'expand'
        }
        ...
      ]
    }
  }
}
複製程式碼

這裡我們打算維護一個 expandIds,儲存所有展開行的資訊,和全選的 selectedRows 一毛一樣。但其實也可以有另一種做法:就是我們預先把傳進來的資料處理一下,在 data 裡面的每一行加上 isExpandisSelect 欄位,然後操作的時候進行相應的狀態修改即可,就不需要再額外宣告陣列了。但是怎麼實現我不管,做出來才是硬道理,所以請看下面的程式碼吧?:

...
 <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,一會我們會支援自定義。這裡我們也不注重樣式,畢竟不是重點,然後執行一下,看下效果?:

table 元件瞭解一下?
好像有點意思!

自定義內容

說講就講?,寫到這裡其實我們已經支援最基礎的表格需要,但問題是現在的表格也太基礎了吧,要是我想自定義內容咋辦(撓頭三連?),好歹支援一下吧。所以接下來我們需要 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 的新寫法,讓我們來看下官網的說明(不瞭解的同學可以去看下使用方法,並不難):

table 元件瞭解一下?
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>
複製程式碼

最後渲染出來就是我們要的模樣了,如下圖所示?:

table 元件瞭解一下?
同樣的道理,我們也可以稍稍修改下展開行使之能夠支援自定義,大家可以嘗試一下,這裡就不做展示羅?。

固定表頭

所謂固定表頭就是表頭不動,但表體可以滾動,這個東西實現起來就有點小麻煩了?,為什麼呢?因為本來我們的表格剛好是由 theadtbody 兩部分組成,常規的思路就是把 tbody 給個高度,然後讓它溢位滾動即可,但是事情沒那麼簡單,heightoverflow 這兩個樣式寫在 tbody 或者 table 上都是木有效果滴,So,我們就得去看看人家是咋做的啦(以 Element 為例):

table 元件瞭解一下?
通過上面這張圖我們能夠清晰的看到 theadtbody 分別被放在了兩個不同的 divtable 裡面,然後設定 tbody 的外層 div 可以滾動就行了。嗯,真實直白的想法?。其實其它幾大 UI 框架也是一樣的(就是分開寫)。那麼既然人家已經實踐過了,說明該方案是可行的,至少相容性是槓桿的,於是我們在集百家之長之後?,就可以開始動手寫屬於自己的程式碼了。當然了,第一步還是 api 的設計,這回我們只需要在標籤裡傳入 height 引數即可:

<xr-table
  :columns="columns"
  :data="data"
  height="150"
></xr-table>
複製程式碼

接下來我們需要改變一下元件的內部結構,把它轉換成兩段式的寫法。事實上表格的 theadtbody 本來就是分開的,所以拆出來並不難,就像下面這樣?:

<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>
複製程式碼

儲存一下程式碼,目前效果圖如下:

table 元件瞭解一下?
嗯,和原來的並沒有什麼差別,但是第一個問題來了,thead 的寬度和 tbody 的寬度對不齊了,這可咋整啊。還能咋整啊,就定寬唄?,通過 columns 傳入每列的 width 值就好了(當然可以有一列是可以不用給寬度的,這樣可以保持彈性),然後把 theadtbody 的每列設定成一樣寬就行了,你可能會覺得麻煩,但事實上這能很好的避免其他一些不必要的問題,這裡我給大家截了 Ant Design 和 Element 兩個官網上的圖:
table 元件瞭解一下?
table 元件瞭解一下?
你可以清楚的看到他們也是需要 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>
複製程式碼

這裡我們把倒數第二列的寬度留空,然後看一下效果:

table 元件瞭解一下?
恩,好像挺好?,那就繼續吧。接下來要做的就是讓表體定高並滾動了。首先我們傳進來的 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 元件瞭解一下?
嗯,也還不錯,雖然樣式有點瑕疵,但這不重要,畢竟功能已經實現了✌。需要看原始碼的可以狠狠點選這裡:table元件
six six six,大讚無疆 ???

結語

寫到這裡,可能內容有點多了,所以我們把多級表頭和固定列留到下一篇講,希望這個月下旬能寫好吧?。雖然寫的東西比較簡單,不過講的是從 0 開始的過程,希望能對大家有所幫助,回見?。

table 元件瞭解一下?table 元件瞭解一下?

相關文章