【Day16】数据展示元件 - Table

元件介绍

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


<<:  Day 15:目前 NOJ 的部署流程

>>:  Day 14— To Do List (1) 专案前置

{DAY 1}开始吧!探索data世界

目标 主题是【从资料库到资料分析视觉化】, 希望可以更深入的了解data, 从资料库的架构,资料的...

Day 6 Swift语法-基础篇(4/5)-Function

今天谈到最常用的函式 function 一般来说,函式的定义方式如图中所示 name: 代表函式的名...

[Day 27] 使用GCP部署机器学习API

使用GCP部署机器学习API 此范例使用鸢尾花朵资料集进行 XGBoost 分类器模型训练。将模型储...

D4 - 加盐不加价 严格模式开启

前言 JavaScript 相较是个自由的语言,在学习语法时会发现,咦 明明规则是这样,怎麽那样也可...

Day09_插班车~风险评估的概念应用在日常工作上~XD"

今天这个,真的是插班车,因为今天作完弱扫,总共八份的测报。 我自已看得都要吐了。 在思考,这个月,因...