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