iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0

元件介紹

Table 顧名思義就是一個表格元件,用來整齊的顯示行列數據。

參考設計 & 屬性分析

我自己覺得 table 是一個還蠻繁瑣的元件,要組成一個 table 就需要各式各樣的 tag,例如 table, thead, tbody, tr,td。

特別是當我們的 table 資料比較複雜的時候,程式碼的結構也會跟著複雜起來,甚至會需要夾雜 JavaScript 的邏輯判斷在裡面,當程式碼變得很難一眼看懂的時候,維護起來所要下的功夫也會隨之增加。

<table>
    <thead>
        <tr>
            <th colspan="2">The table header</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>The table body</td>
            <td>with two columns</td>
        </tr>
    </tbody>
</table>

因此有時候我們也可以擁有另一種選擇,就是我們希望能夠做一個元件,讓我們避免每次都要撰寫這些複雜的巢狀結構,而是只要給定表格的欄位以及資料,這個元件就能夠自動幫我們產生 Table。例如 Antd 的 Table 就是這樣設計的:

import { Table } from 'antd';

<Table dataSource={dataSource} columns={columns} />;

MUI 雖然他有自己提供的一套 Table library 來讓我們用類似原生 html 的方式來組裝這些 Table 的巢狀結構,但同樣的他也有出一套 DataGrid 讓我們能夠只用欄位及資料來產生一個 Data Table:

import { DataGrid } from '@material-ui/data-grid';

<DataGrid rows={rows} columns={columns} />

用 Data 直接映射出 Table 的方式,雖然在功能和樣式上有一些限制,但是這樣的犧牲可以為我們帶來維護上的好處,特別是我們網站上有許多的 Table,並且這些 Table 並不會差異太大的時候,或許可以考慮這樣的方式,例如可能某個後台管理系統在不同的分頁會需要類似的表格,會員管理表格、文章管理表格、訂單管理表格......等等。

因此本篇中會演示如何做出一個簡單的 Data table,並且選擇一些我覺得可能會容易用到的屬性來當範例。

columns

columns 這個屬性用來描述表格欄位的配置,每一欄用一個物件來表示,其中,title 用來描述要顯示的欄位名稱;dataIndex 用來做與資料的對應;render 可以幫助我們在這一欄當中生成比較複雜的數據,例如這一欄當中需要顯示 icon 或需要實現點擊事件等等;width 用來指定欄位的寬度;align 用來設置欄位對齊的方式。

const columns = [
  {
    title: 'Name',
    dataIndex: 'name',
    width: '100px',
		align: 'center',
    render: ({ name }) => <a href="...">{name}</a>,
  },
  //... 其他欄位
];

dataSource

dataSource 用來指定表格的數據內容,一個 object 代表一列,以下面為例,會有 name, age, address 等資料。

const dataSource = [
  {
    key: '1',
    name: '胡彦斌',
    age: 32,
    address: '西湖区湖底公园1号',
		tags: ['nice', 'developer'],
  },
  {
    key: '2',
    name: '胡彦祖',
    age: 42,
    address: '西湖区湖底公园1号',
		tags: ['cool', 'teacher'],
  },
];

介面設計

Table

屬性 說明 類型 默認值
columns 描述表格欄位的配置 ColumnsType[]
dataSource 指定表格的數據內容 object[]

Columns

屬性 說明 類型 默認值
title 欄位名稱 string
dataIndex 用來對應數據 string
width 設置寬度 string, number
align 設置對齊方式 left, right, center
render 生成數據複雜的渲染 (data) => {}

元件實作

按照上述的分析以及說明,我們可以開始 Data Table 的實作。
因為我們已經有了 columns 這個資料,所以我們可以把 table header 用迭代的方式產生出來:

const columns = [
  {
    title: 'Name',
    dataIndex: 'name',
    width: '100px',
		align: 'center',
    render: ({ name }) => <a href="...">{name}</a>,
  },
  //... 其他欄位
];
<table>
  <thead>
    <tr>
      {
        columns.map((column) => (
          <th key={column.key}>
            {column.title}
          </th>
        ))
      }
    </tr>
  </thead>
  <tbody>...</tbody>
</table>

再來因為我們已經有了 dataSource ,也就是每一筆 row 的資料,所以我們也可以用迭代的方式把內容產生出來。

但這邊會比 header 較複雜一點,我用了兩層的迴圈,最外層的迴圈是一筆一筆的 row 的資料,而內層的迴圈,是一筆 row 當中每個 column 的資料。

所以外層用 dataSource 來迭代,而內層用剛剛迭代出 header 的 columns 來迭代,因此程式碼如下:

<tbody>
  {
    dataSource.map((data) => (
      <tr key={data.key}>
        {
          columns.map((column) => {
            const { dataIndex } = column;
            const foundCellData = column.render
              ? column.render(data[dataIndex])
              : data[dataIndex];
            return (
              <td key={column.key}>
                {foundCellData}
              </td>
            );
          })
        }
      </tr>
    ))
  }
</tbody>

好啦,用以上的方式,我們就可以不用自己去寫 table 的結構,直接從外面定義好 columns 以及 dataSource,就能夠產生出一個 table 了!這樣即使資料增加很多筆,我們程式碼中的 table 也不會越來越大坨。

下面就是我們產生出的不帶樣式的 table:

但是這個 table 也不是真的完全不帶樣式啦,我至少有給他 border,然後有處理一下 border-collapse 的問題,border-collapse 屬性的功能是用來將表格欄位邊框合併,讓表格變得更美化:

const StyledTable = styled.table`
  border-collapse: collapse;
  * {
    border: 1px solid #000;
    box-sizing: border-box;
  }
`;

當然這個樣式真的是太陽春,不過沒關係,因為這個 table 是我們自己手刻的,所以也可以按照自己心意調整樣式,這邊我示範一個透過 styled-components 來客製化樣式的例子,我以一個 Antd 樣式的 table 為例:

const AntdStyle = styled(Table)`
  width: 100%;
  * {
    border: none;
    white-space: nowrap;
    text-align: left;
  }
  th {
    background: #fafafa;
  }
  td, th {
    padding: 16px;
  }
  tr {
    border-bottom: 1px solid #f0f0f0;
  }
`;

成果如下圖,簡單幾個 css 就能夠讓他看起來有模有樣,而且我們的 props 傳入介面也完全不會受到影響:

指定欄位寬度

我們也可以像 antd 一樣,從 columns 資料結構當中,給他 width 的屬性,讓他可以指定那個欄位要多大的寬度,像這樣:

const columns = [
  {
    title: 'Name',
    dataIndex: 'name',
    width: 130,
  },
  //... 其他欄位
];

而我們 table 的結構就能夠根據這個 width 來調整我們欄位的寬度:

<thead>
  <tr>
    {
      columns.map((column) => (
        <th key={column.key} style={{ width: column.width }}>
          {column.title}
        </th>
      ))
    }
  </tr>
</thead>

Sticky column

我們螢幕不夠寬,但是 table 很寬,欄位很多的時候,勢必會需要 sticky column 的功能,先開門見山給大家看一下效果:

為了做到可以 scroll 的效果,我們必須要調整一下 table 元件的結構,我們要在 table 外面再包一層 div ,使得 div 容納不下 table 的寬度的時候可以出現 scroll bar:

<div style={{ width: '100%', overflow: 'auto' }}>
  <StyledTable
    className={className}
    $columnsCount={columns.length}
  >
    <thead>...</thead>
    <tbody>...</tbody>
  </StyledTable>
</div>

我們想要做到的效果是,當 columns 的資料裡面有 fixed: true 的時候,我們要可以凍結住那一欄,像是下面這樣:

const columns = [
  {
    title: 'Name',
    dataIndex: 'name',
    width: 130,
    fixed: true,
  },
  //... 其他欄位
];

準備好 props 的資料之後,我們就要把 fixed 這個 props 傳入元件中對應的節點,我們需要傳入的節點是 thead 上面第一個 column 的 th,以及 tbody 當中第一個 column 的 td

在 styled-components 當中拿到這個 fixed 之後我們來決定要不要讓他可以凍結:

const Th = styled.th`
  width: ${(props) => props.$width}px;
  ${(props) => props.$fixed && stickyLeftStyle};
`;

const Td = styled.td`
  background: #FFF;
  ${(props) => props.$fixed && stickyLeftStyle};
`;

凍結的關鍵 CSS 在這邊,我們用 position: sticky; 這個屬性來幫助我們做到凍結,

const stickyLeftStyle = css`
  position: sticky;
  left: 0px;
  z-index: 2;

  /* ...(略) */
`;

到目前為止我們就能夠做出一個沒有陰影樣式的 sticky column 效果了:

沒有陰影或是 border 真的是很難看出欄位之間的邊界,但是我們實際上動手做過就會知道,這邊的 boder 或是要做陰影真的沒有那麼直覺就能夠做到,所以我去偷看了一下 Antd 的樣式,學到了他的撇步:

const stickyLeftStyle = css`
  position: sticky;
  left: 0px;
  z-index: 2;
  &:after {
    content: "";
    position: absolute;
    right: 0px;
    top: 0px;
    width: 30px;
    height: 100%;
    box-shadow: inset 10px 0 8px -8px #00000026;
    transform: translateX(100%);
  }
`;

這邊的陰影並不是 column 自己本身的陰影,而是透過他的偽元素 ::after 來模擬陰影的效果,讓 ::after 往右邊外面延伸,並且給他陰影,讓整個看起來很像是 column 自己的陰影:

客製化表格內容

當然我們要讓表格內容除了能夠顯示文字之外,我們也能夠支援其他的內容,例如下面我們能夠放入刪除按鈕,我先做一個很陽春的樣式來示意:

要怎麼做到這件事呢?我們看一下 Antd 介面上是怎麼設計,他是透過在 column 資料結構裡面定義一個 render 的屬性,他是一個 function ,可以幫助我們在指定的 cell 當中 render 出我們期待的內容:

const columns = [
  //... 其他欄位
  {
    title: '操作',
    dataIndex: 'actions',
    key: 'actions',
    render: () => (
      <Button themeColor="secondary">
        <span>刪除</span>
      </Button>
    ),
  },
];

在我們的 table 元件當中,當然就是判斷有沒有這個 render 的欄位,如果有的話就呼叫他,把內容畫出來,如果沒有的話,就顯示預設的文字:

以上就是我演示的簡易 Data table,當然要把一個 table 做好,還有許多細節需要注意,也有許多功能值得我們擴充,但是不見得每個功能我們都會需要,因此大家按照自己的專案的需求來調整就可以了。


Table 元件原始碼:
Source code

Storybook:
Table


上一篇
【Day15】數據展示元件 - Carousel
下一篇
【Day17】數據展示元件 - Infinite scroll
系列文
30 天擁有一套自己手刻的 React UI 元件庫30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言