美文网首页
实现一个Table泛型组件

实现一个Table泛型组件

作者: YM雨蒙 | 来源:发表于2022-05-27 12:02 被阅读0次

Table 组件

可以先看代码, 代码查看

表格组件在我们开发工作中是最常用的整理数据的一种组件, 会在很多页面用到, 我们不可能每个页面都用 table tr td 这些属性写一个表格, 那我们改如何写一个通用的表格组件呢? 我们从一个最基本的表格慢慢完善一个功能强大的表格

我们思考一下如何让用户更好的使用, 我们可以做哪些简单的工作?

  1. 用户可以使用 基本表格
  2. 用户可以设置 边框/紧凑/条纹表格
  3. 用户可以设置序号列, 不需要自己在 columns 里面设置
  4. 用户可以自定义设置单元格需要展示的数据
  5. 用户可以设置选择框, 获取表格数据
  6. 用户可以根据列排序 (后端排序, 前端获取数据渲染)
  7. 排序的过程需要时间, 加一个 loading 效果
  8. 空数据效果
  9. 固定表头
  10. etc....

1. 基本表格

  • 最基本的表格需要表头,和每一行的数据, 那我们可不可以只让用户传递 表头 props数据 data, 我们就可以绘制出一个好看的表格, 我们可以参考优秀的社区组件别人是怎么实现的, 向优秀的人学习
    • 我们可以先定义下面的 columnsdata 的类型以及数据结构
type DataPropa = {
  age: number;
  name: string;
  gender: string;
};

type ColProps = {
  title: string;
  key: keyof DataPropa;
};

const columns: ColProps[] = [
  {
    title: "年龄",
    key: "age",
  },
  {
    title: "姓名",
    key: "name",
  },
  {
    title: "性别",
    key: "gender",
  },
];

const data: DataPropa[] = [
  {
    age: 15,
    name: "yym",
    gender: "男",
  },
];

<Table columns={columns} data={data} />
  • 我们在组件实现我们应该把这些数据渲染到 html 元素上呢? 先完成下面基础的结构
    • 可以循环 columnsth 元素上, 渲染出我们的表头
    • 循环 datatbody > tr 有几个数据, 就有几行, 在每个行表格里匹配 columns 里面 key 对应的值
import { ReactElement, ReactNode } from "react";
import classnames from "classnames";

import "./table.scss";

interface TableProps<T> {
  columns: {
    title: string;
    key: keyof T;
  }[];
  data: T[];
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const { columns, data } = props;

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table")}>
        <thead className="g-table-head">
          <tr>
            {/* 循环表头 */}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item) => {
            return (
              <tr>
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      <span>{item[col.key] as unknown as string}</span>
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
  • 我们给 上面 table 的 class 加上 scss, 美化成我们希望的样式
.g-table-wrap {
  background-color: #fff;
  border-radius: 4px;
  box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.06), 0 4px 4px 0 rgba(0, 0, 0, 0.12);

  .g-table {
    width: 100%;
    border-spacing: 0;
    border-collapse: collapse;

    &-head {
      line-height: 20px;
      tr {
        th {
          padding: 10px;
          font-size: 12px;
          font-weight: 400;
          color: #8e8e93;
        }
      }
    }

    &-body {
      line-height: 20px;
      tr {
        td {
          padding: 13px 10px;
          font-size: 14px;
          color: #575757;
        }
      }
    }

    tr {
      text-align: left;
      border-bottom: 1px solid #f2f2f5;
      &:hover {
        background: #f2faff;
      }
    }
  }
}
普通表格

2. 边框/紧凑/条纹表格

这些都是样式的变化,我们来给这些添加对应的 class 修改样式

  • 给每个表格行添加边框 bordered: boolean
  • 给一个紧凑的表格 compact: boolean
  • 给一个条纹相间的表格 striped: boolean
// 使用
<Table columns={columns} data={data} />
<Table columns={columns} data={data} bordered compact />
// 如何实现
import { ReactElement, ReactNode } from "react";
import classnames from "classnames";

import "./table.scss";

interface TableProps<T> {
  columns: {
    title: string;
    key: keyof T;
  }[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
  } = props;

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {/* 循环表头 */}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item) => {
            return (
              <tr>
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {item[col.key] as unknown as string}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
// 样式
.g-table-wrap {
  background-color: #fff;
  border-radius: 4px;
  box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.06), 0 4px 4px 0 rgba(0, 0, 0, 0.12);
  margin-top: 20px;

  .g-table {
    width: 100%;
    border-spacing: 0;
    border-collapse: collapse;

    &-head {
      line-height: 20px;
      tr {
        th {
          padding: 10px;
          font-size: 12px;
          font-weight: 400;
          color: #8e8e93;
        }
      }
    }

    &-body {
      line-height: 20px;
      tr {
        td {
          padding: 13px 10px;
          font-size: 14px;
          color: #575757;
        }
      }
    }

    tr {
      text-align: left;
      border-bottom: 1px solid #f2f2f5;
      &:hover {
        background: #f2faff;
      }
    }

    &.g-table-bordered {
      border: 1px solid #f2f2f5;
      border-radius: 6px;
      th,
      td {
        border: 1px solid #f2f2f5;
      }
    }
    &.g-table-compact {
      td,
      th {
        padding: 5px;
      }
    }
    &.g-table-striped {
      .g-table-body {
        tr {
          &:nth-child(even) {
            background: #f7f7fa;
          }
          &:hover {
            background: #f2faff;
          }
        }
      }
    }
  }
}
边框表格

3. 序号列

让用户通过配置自动添加序号列, 我们可以设置一个开关, 来自己添加这一列 numberVisible: boolean

// 如何使用 props numberVisible
<Table columns={columns} data={data} bordered compact numberVisible />
// 实现
import { ReactElement, ReactNode } from "react";
import classnames from "classnames";

import "./table.scss";

interface TableProps<T> {
  columns: {
    title: string;
    key: keyof T;
  }[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,
  } = props;

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {/* 是否显示序号 */}
            {numberVisible && <th>序号</th>}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            return (
              <tr key={index}>
                {/* 显示序号的字段 */}
                {numberVisible && <td>{index + 1}</td>}
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {item[col.key] as unknown as string}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
image.png

4. 自定义设置单元格

用户如果不设置希望在里面展示的数据, 我们就自动匹配对应 columns key 的值, 如果用户希望展示一个 按钮 输入框等, 那我们改怎么弄呢?

  • columns 里面设置一个回调函数 render , 让用户自定义该列怎么渲染数据, 我们返回(该单元格内容, 整行数据, 下标)
  • render 就使用 render 返回渲染, 没有就使用默认的值
// 设置类型
type columns<T> = {
  title: string;
  key: keyof T;
  render?: (text: string, record: T, index: number) => void;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
}

看一下代码如何实现, 需要改变 columns 数据, 多了一个参数 render, 正好可以使用到我们之前写好的 Button 组件

const columns: ColProps[] = [
  {
    title: "年龄",
    key: "age",
  },
  {
    title: "姓名",
    key: "name",
  },
  {
    title: "性别",
    key: "gender",
  },
  {
    title: "地址",
    key: "address",
  },
  {
    title: "操作",
    key: "action",
    render: (text: string, record: DataProps, index: number) => {
      console.log(text, record, index, "data...");
      return <Button type="danger">删除</Button>;
    },
  },
];

用户 columns 传递了 render, 代码里我们接收一下

import { ReactElement, ReactNode } from "react";
import classnames from "classnames";

import "./table.scss";

type columns<T> = {
  title: string;
  key: keyof T;
  render?: (text: string, record: T, index: number) => ReactNode;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,
  } = props;

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {/* 是否显示序号 */}
            {numberVisible && <th>序号</th>}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            return (
              <tr key={index}>
                {/* 显示序号的字段 */}
                {numberVisible && <td>{index + 1}</td>}
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {/* 渲染的数据 */}
                      {col.render
                        ? col.render(
                            item[col.key] as unknown as string,
                            item,
                            index
                          )
                        : (item[col.key] as unknown as string)}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
image.png

5. 设置选择框 全选/全不选

一般我们在使用表格组件时都会有选择数据的需求, 所以我们把这个功能集成在 Table 组件里

  • 用户可以设置开关打开选择框, 可以选择一个/多个
  • 表头行可以控制所有行 全选/全不选
  • 选择时触发 change 事件把数据给吐出去
  • Checkbox 使用我们之前开发的组件

前面我们设置了序号列, 我们通过控制 numberVisible 来控制显隐, 这次我们设置一个 checkable 来控制选择框列是否显隐

// 使用
<Table
  columns={columns}
  data={data}
  bordered
  compact
  numberVisible
  checkable // 控制弹框是否显示
/>
// 实现
import { ReactElement, ReactNode } from "react";
import { Checkbox } from "../index";

import classnames from "classnames";

import "./table.scss";

type columns<T> = {
  title: string;
  key: keyof T;
  render?: (text: string, record: T, index: number) => ReactNode;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
  // 选择框
  checkable?: boolean;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,
    checkable = false,
  } = props;

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {checkable && (
              <th>
                <Checkbox />
              </th>
            )}
            {/* 是否显示序号 */}
            {numberVisible && <th>序号</th>}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            return (
              <tr key={index}>
                {checkable && (
                  <td>
                    <Checkbox />
                  </td>
                )}
                {/* 显示序号的字段 */}
                {numberVisible && <td>{index + 1}</td>}
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {/* 渲染的数据 */}
                      {col.render
                        ? col.render(
                            item[col.key] as unknown as string,
                            item,
                            index
                          )
                        : (item[col.key] as unknown as string)}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
选择框

上面的图片我们可以看到选择框已经出现在表格列中了, 但是现在有两个问题没有解决?

  1. 我们无法从表头行控制表格数据的 全选/全不选
  2. 我们无法得知我们选择了哪些数据

解决1的逻辑就是: 我们可以在组件内部当表格头的选择框被选中时, 给每一行的选择框 checked = true, 反之 checked = false, 而当我们触发表格行的选择框时, 判断 checked = true 的数量 等于 data.length 则表头为选中状态, 否则就是不完全选择, 当为 0 时, 表头选择框为不选, 我们在组件内部维护一个 [selected] = useState([]), 选择框 change 事件触发, 根据 selected 的值来判断 checked

  • 给每个选择框添加 change 事件
// 实现方案
import { ChangeEvent, ReactElement, ReactNode, useMemo, useState } from "react";
import { Checkbox } from "../index";

import classnames from "classnames";

import "./table.scss";

type columns<T> = {
  title: string;
  key: keyof T;
  render?: (text: string, record: T, index: number) => ReactNode;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
  // 选择框
  checkable?: boolean;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,
    checkable = false,
  } = props;

  const [selected, setSelected] = useState<any[]>([]);

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  const handleSelectItem = (e: ChangeEvent<HTMLInputElement>, item: any) => {
    const { checked } = e.target;
    // 改变 checked 的值
    checked
      ? setSelected([...selected, item])
      : setSelected(selected.filter((i) => i.key !== item.key));
  };

  const handleSelectAllItem = (e: ChangeEvent<HTMLInputElement>) => {
    const { checked } = e.target;
    setSelected(checked ? data : []);
  };

  // 判断表格行是否被选中
  const areItemSelected = (item: any) =>
    useMemo(
      () => selected.filter((i) => i.key === item.key).length > 0,
      [selected]
    );

  // 表格头的状态
  const areAllItemsSelected: boolean = useMemo(
    () => data.length === selected.length,
    [selected]
  );

  // 不完全选择
  const areNotAllItemsSelected: boolean = useMemo(
    () => data.length !== selected.length && selected.length !== 0,
    [selected]
  );

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {checkable && (
              <th>
                <Checkbox
                  checked={areAllItemsSelected}
                  indeterminate={areNotAllItemsSelected}
                  onChange={(e) => handleSelectAllItem(e)}
                />
              </th>
            )}
            {/* 是否显示序号 */}
            {numberVisible && <th>序号</th>}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            console.log(areItemSelected(item), "丁东坑");

            return (
              <tr key={index}>
                {checkable && (
                  <td>
                    <Checkbox
                      checked={areItemSelected(item)}
                      onChange={(e) => handleSelectItem(e, item)}
                    />
                  </td>
                )}
                {/* 显示序号的字段 */}
                {numberVisible && <td>{index + 1}</td>}
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {/* 渲染的数据 */}
                      {col.render
                        ? col.render(
                            item[col.key] as unknown as string,
                            item,
                            index
                          )
                        : (item[col.key] as unknown as string)}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
状态变化

解决2的逻辑就是: 我们可以通过 传递一个回调函数, 把选择的数据给暴露出去, changeSeletedItems props, 在 1 中, 我们创建了 state selected , 我们每次 选择框改变都会 setSelected 修改值的变化, 我们可以把值传递给外部

// 使用
const handleChangeSelected = (val: DataProps[]) => {
  console.log(val, "选中的值");
};

<Table
  columns={columns}
  data={data}
  bordered
  compact
  numberVisible
  checkable
  // 新增 change 事件
  changeSeletedItems={handleChangeSelected}
/>
// 实现方案, 每次 selected 变化时, 把 selected 暴露出去

useEffect(() => {
  changeSeletedItems && changeSeletedItems(selected);
}, [selected]);
选中的值

6. 数据排序 sorter

数据排序我们可以方法给用户, 让用户来改变 data 的顺序, 重新渲染表格来达到排序的目的, 我们做成下面图片的样式, 我们可以在 column 上做文章, 开始是没有排序, 点击后会排序 data, 重新渲染

  1. column 存在排序属性, 我们就显示改排序图标, 图标变化从 无状态 => 升序 => 降序
    • 我们可以在内部维护一个 useRef order => "asc" | "desc" | "unsc", 每次点击规律变化, 把当前状态告诉用户
  2. 用户使用时在 columns 添加 sorter 函数, 里面调用后端接口, 改变 data
排序
// 使用
import { Button, Checkbox, CheckboxGroup, Table } from "./lib/index";
import "./App.scss";
import { ChangeEvent, ReactNode, useState } from "react";

type DataProps = {
  age: number;
  name: string;
  gender: string;
  address: string;
  action?: any;
  key?: string;
};
type orderType = "asc" | "desc" | "unsc";

type ColProps<T> = {
  title: string;
  key: keyof DataProps;
  render?: (text: string, record: T, index: number) => ReactNode;
  sorter?: (val: orderType) => void;
};

const App = () => {
  const columns: ColProps<DataProps>[] = [
    {
      title: "年龄",
      key: "age",
      sorter: (val) => {
        // TODO: 开始排序 
        console.log(val, "我是怎么排序规则?");
        if (val === "asc") {
          data.sort((a, b) => Number(a.age) - Number(b.age));
        } else if (val === "desc") {
          data.sort((a, b) => Number(b.age) - Number(a.age));
        } else {
          // ajax
        }
      },
    },
    {
      title: "姓名",
      key: "name",
    },
    {
      title: "性别",
      key: "gender",
    },
    {
      title: "地址",
      key: "address",
    },
    {
      title: "操作",
      key: "action",
      render: (text: string, record: DataProps, index: number) => {
        return (
          <>
            <Button type="danger" style={{ marginRight: "8px" }}>
              删除
            </Button>
            <Button type="primary">编辑</Button>
          </>
        );
      },
    },
  ];

  const data: DataProps[] = [
    {
      key: "1",
      age: 15,
      name: "yym",
      gender: "男",
      address: "深圳市",
    },
    {
      key: "2",
      age: 18,
      name: "张三",
      gender: "女",
      address: "安徽省",
    },
    {
      key: "3",
      age: 35,
      name: "李四",
      gender: "女",
      address: "张家界",
    },
    {
      key: "4",
      age: 6,
      name: "小黑",
      gender: "男",
      address: "蚌埠",
    },
  ];

  const handleChangeSelected = (val: DataProps[]) => {
    console.log(val, "选中的值");
  };

  return (
    <div className="App">
      <Table
        columns={columns}
        data={data}
        bordered
        compact
        numberVisible
        checkable
        changeSeletedItems={handleChangeSelected}
      />
    </div>
  );
};

export default App;
// 代码实现排序
import {
  ChangeEvent,
  ReactElement,
  ReactNode,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { Checkbox } from "../index";

import classnames from "classnames";

import "./table.scss";

type orderType = "asc" | "desc" | "unsc";

type columns<T> = {
  title: string;
  key: keyof T;
  render?: (text: string, record: T, index: number) => ReactNode;
  sorter?: (val: orderType) => void;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
  // 选择框
  checkable?: boolean;
  changeSeletedItems?: (selected: T[]) => void;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,

    checkable = false,
    changeSeletedItems,
  } = props;

  const [update, setUpdate] = useState(0); // 更新页面
  const [selected, setSelected] = useState<any[]>([]);
  const order = useRef<"asc" | "desc" | "unsc">("unsc");

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  useEffect(() => {
    changeSeletedItems && changeSeletedItems(selected);
  }, [selected]);

  const handleSelectItem = (e: ChangeEvent<HTMLInputElement>, item: any) => {
    const { checked } = e.target;
    // 改变 checked 的值
    checked
      ? setSelected([...selected, item])
      : setSelected(selected.filter((i) => i.key !== item.key));
  };

  const handleSelectAllItem = (e: ChangeEvent<HTMLInputElement>) => {
    const { checked } = e.target;
    setSelected(checked ? data : []);
  };

  // 判断表格行是否被选中
  const areItemSelected = (item: any) =>
    useMemo(
      () => selected.filter((i) => i.key === item.key).length > 0,
      [selected]
    );

  // 表格头的状态
  const areAllItemsSelected: boolean = useMemo(
    () => data.length === selected.length,
    [selected]
  );

  // 不完全选择
  const areNotAllItemsSelected: boolean = useMemo(
    () => data.length !== selected.length && selected.length !== 0,
    [selected]
  );

  const handleOrderBy = (col: columns<any>) => {
    if (order.current === "unsc") {
      order.current = "asc";
    } else if (order.current === "asc") {
      order.current = "desc";
    } else if (order.current === "desc") {
      order.current = "unsc";
    }

    setUpdate(Math.random());
    col.sorter && col.sorter(order.current);
  };

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {checkable && (
              <th>
                <Checkbox
                  checked={areAllItemsSelected}
                  indeterminate={areNotAllItemsSelected}
                  onChange={(e) => handleSelectAllItem(e)}
                />
              </th>
            )}
            {/* 是否显示序号 */}
            {numberVisible && <th>序号</th>}
            {columns.map((col) => {
              return (
                <th key={col.key as string}>
                  {/* 排序按钮 */}
                  {col.sorter ? (
                    <span
                      className="g-table-sort-wrap"
                      onClick={() => handleOrderBy(col)}
                    >
                      {col.title}
                      <span className="g-table-sort">
                        <i
                          className={classnames("g-table-up", {
                            "g-table-active": order.current === "asc",
                          })}
                        ></i>
                        <i
                          className={classnames("g-table-down", {
                            "g-table-active": order.current === "desc",
                          })}
                        ></i>
                      </span>
                    </span>
                  ) : (
                    <>{col.title}</>
                  )}
                </th>
              );
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            return (
              <tr key={index}>
                {checkable && (
                  <td>
                    <Checkbox
                      checked={areItemSelected(item)}
                      onChange={(e) => handleSelectItem(e, item)}
                    />
                  </td>
                )}
                {/* 显示序号的字段 */}
                {numberVisible && <td>{index + 1}</td>}
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {/* 渲染的数据 */}
                      {col.render
                        ? col.render(
                            item[col.key] as unknown as string,
                            item,
                            index
                          )
                        : (item[col.key] as unknown as string)}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
原始
asc
desc

7. 添加loading

添加 loading 效果, 就是我们设置一个 loading 动画, 用户传递参数 true/false, 展示/隐藏

// 使用
<Table columns={columns} data={data} loading />
// 实现
// html 结构 wrap 下和 table 平级
{loading && <div className="g-table-loading">加载中...</div>}
.g-table-loading {
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0.7;
  background: #fff;
}

8. 空数据

当用户没有数据时, 我们不能只展示 表头, 而是展示一个默认的样式, 可以通过 data.length > 0 ? 空数据样式 : tbody, 也可以让用户传进来一个 empty 的内容, 我们接受放入空数组页面里

// 实现方案
{data.length === 0 && (
  <tr>
    <td
      className="g-table-empty"
      colSpan={columns.length + colSpan()}
    >
      <span className="default">暂无数据</span>
    </td>
  </tr>
)}
.g-table-empty {
  text-align: center;
  .default {
    display: flex;
    justify-content: center;
    padding: 20px 0;
  }
}

9. 固定表头

很多情况下当我们希望给 table 一个固定高度, 超出滚动时, 我们希望用户能够一直看到表头, 那这个我们应该怎么做呢?

  1. 使用 position: sticky 粘性布局, 优点是比较简单, 缺点是兼容性不是很好, can i use position sticky
    • 用户设置 height={固定高度} 超过出现滚动条 overflow: auto, 给 thead设置 position: sticky
// 使用
<Table columns={columns} data={data} height={400} />
// 实现

<div
  className="g-table-wrap"
  style={{ height: height, overflow: height ? "auto" : "unset" }}
>
  <thead
    className={classnames("g-table-head", {
      "g-table-sticky": !!height,
    })}
  >
</div>

&.g-table-sticky {
    position: -webkit-sticky;
    position: sticky;
    top: 0px; /* 列首永远固定在头部  */
    background: #fff;
    z-index: 10;
}

// 固定第一列
&:first-child {
  position: -webkit-sticky;
  position: sticky;
  left: 0;
  background-color: #fff;
}
// 固定第二列
&:nth-child(2) {
  position: -webkit-sticky;
  position: sticky;
  left: 93px; // 让用户指定固定列的宽度, 获取设置
  background: #fff;
}
固定两列
  • 一些主流 UI 组件库因为有很多用户使用, 做到兼容性比较好, 所以通过 操作 DOM 来把 thead clone 一份, 通过定位放到 table 的上面 来完成表头固定, 但比较复杂, 会发现宽度出现对不齐的情况. 优点是兼容性好, 实现起来复杂
    • 设置了 height 第一次渲染, 把 tHeadtBody 分开, 让 tBody overflow: auto
    • 会有 theadtBody 对齐的问题, 简单的弄, 让用户给每个 columns 设置宽度
    • 下面的实现 当 启用严格模式 时, 会触发副作用, 实现原理是这样的, 暂时先不修复
// 实现原理: 操作DOM

import {
  ChangeEvent,
  ReactNode,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { Checkbox } from "../index";

import classnames from "classnames";

import "./table.scss";

type orderType = "asc" | "desc" | "unsc";

type columns<T> = {
  title: string;
  key: keyof T;
  width?: number;
  render?: (text: string, record: T, index: number) => ReactNode;
  sorter?: (val: orderType) => void;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
  // 选择框
  checkable?: boolean;
  changeSeletedItems?: (selected: T[]) => void;

  loading?: boolean;
  height?: number;
}
// function Table<T>(props: TableProps<T>)
const Table = <T,>(props: TableProps<T>) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,

    checkable = false,
    changeSeletedItems,

    loading = false,
    height,
  } = props;

  const [_, setUpdate] = useState(0); // 更新页面
  const [selected, setSelected] = useState<any[]>([]);
  const order = useRef<"asc" | "desc" | "unsc">("unsc");
  const wrapRef = useRef<any>(null);
  const tableRef = useRef<any>(null);

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  useEffect(() => {
    changeSeletedItems && changeSeletedItems(selected);
  }, [selected]);

  // 固定表头计算
  useEffect(() => {
    let table1: any;
    let table2: any;
    if (height) {
      table1 = tableRef.current.cloneNode(false);
      table2 = tableRef.current.cloneNode(false);

      const tHead = tableRef.current.children[0];
      const tBody = tableRef.current.children[1];
      const divBody = document.createElement("div");

      table1.appendChild(tHead);
      divBody.appendChild(table2).appendChild(tBody);
      divBody.style.height = height + "px";
      divBody.style.overflowY = "auto";

      wrapRef.current.appendChild(table1);
      wrapRef.current.appendChild(divBody);
    }

    return () => {
      height && table1.remove();
      height && table2.remove();
    };
  }, []);

  const handleSelectItem = (e: ChangeEvent<HTMLInputElement>, item: any) => {
    const { checked } = e.target;
    // 改变 checked 的值
    checked
      ? setSelected([...selected, item])
      : setSelected(selected.filter((i) => i.key !== item.key));
  };

  const handleSelectAllItem = (e: ChangeEvent<HTMLInputElement>) => {
    const { checked } = e.target;
    setSelected(checked ? data : []);
  };

  // 判断表格行是否被选中
  const areItemSelected = (item: T) =>
    useMemo(
      () => selected.filter((i) => i.key === item.key).length > 0,
      [selected]
    );

  // 表格头的状态
  const areAllItemsSelected: boolean = useMemo(
    () => data.length === selected.length,
    [selected]
  );

  // 不完全选择
  const areNotAllItemsSelected: boolean = useMemo(
    () => data.length !== selected.length && selected.length !== 0,
    [selected]
  );

  const handleOrderBy = (col: columns<T>) => {
    if (order.current === "unsc") {
      order.current = "asc";
    } else if (order.current === "asc") {
      order.current = "desc";
    } else if (order.current === "desc") {
      order.current = "unsc";
    }

    setUpdate(Math.random());

    col.sorter && col.sorter(order.current);
  };

  // 计算 colspan 的 值
  const colSpan = (): number => {
    let length = 0;
    if (numberVisible) {
      length += 1;
    }
    if (checkable) {
      length += 1;
    }

    return length;
  };

  return (
    <div ref={wrapRef} className="g-table-wrap">
      <table ref={tableRef} className={classnames("g-table", tableClasses)}>
        <thead className={classnames("g-table-head")}>
          <tr>
            {checkable && (
              <th style={{ width: "50px" }}>
                <Checkbox
                  checked={areAllItemsSelected}
                  indeterminate={areNotAllItemsSelected}
                  onChange={(e) => handleSelectAllItem(e)}
                />
              </th>
            )}
            {/* 是否显示序号 */}
            {numberVisible && <th style={{ width: "50px" }}>序号</th>}

            {columns.map((col) => {
              return (
                <th key={col.key as string} style={{ width: `${col.width}px` }}>
                  {/* 排序按钮 */}
                  {col.sorter ? (
                    <span
                      className="g-table-sort-wrap"
                      onClick={() => handleOrderBy(col)}
                    >
                      {col.title}
                      <span className="g-table-sort">
                        <i
                          className={classnames("g-table-up", {
                            "g-table-active": order.current === "asc",
                          })}
                        ></i>
                        <i
                          className={classnames("g-table-down", {
                            "g-table-active": order.current === "desc",
                          })}
                        ></i>
                      </span>
                    </span>
                  ) : (
                    <>{col.title}</>
                  )}
                </th>
              );
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            return (
              <tr key={index}>
                {checkable && (
                  <td style={{ width: "50px" }}>
                    <Checkbox
                      checked={areItemSelected(item)}
                      onChange={(e) => handleSelectItem(e, item)}
                    />
                  </td>
                )}
                {/* 显示序号的字段 */}
                {numberVisible && (
                  <td style={{ width: "50px" }}>{index + 1}</td>
                )}
                {columns.map((col) => {
                  return (
                    <td
                      key={col.key as string}
                      style={{ width: `${col.width}px` }}
                    >
                      {/* 渲染的数据 */}
                      {col.render
                        ? col.render(
                            item[col.key] as unknown as string,
                            item,
                            index
                          )
                        : (item[col.key] as unknown as string)}
                    </td>
                  );
                })}
              </tr>
            );
          })}
          {data.length === 0 && (
            <tr>
              <td
                className="g-table-empty"
                colSpan={columns.length + colSpan()}
              >
                <span className="default">暂无数据</span>
              </td>
            </tr>
          )}
        </tbody>
      </table>
      {loading && <div className="g-table-loading">加载中...</div>}
    </div>
  );
};

export default Table;
固定表头

后续会增加 固定一列, 展开行等功能

相关文章

  • 实现一个Table泛型组件

    Table 组件 可以先看代码, 代码查看[https://stackblitz.com/edit/react-t...

  • Swift进阶之泛型

    泛型Generic在swift中非常重要,它提升了代码的通用性和简洁性,很多开源的组件都是通过泛型来实现。泛型是什...

  • ts02

    ref从零实现,主要跟着这篇文章来练习的 插件 ts-node:编译 + 运行。 泛型正向 泛型:创建可重用的组件...

  • 008-自定义泛型,Collections

    自定义泛型 泛型类 代码实现 测试 泛型接口 代码实现 泛型方法 代码演示 测试 泛型上下边界 Collectio...

  • vue封装基础table表格

    table组件具有的功能/特点: table功能实现解析 1.选择render函数还是采用template实现组件...

  • TypeScript 06 - 泛型

    基本示例 使用泛型变量 泛型类型 泛型类 泛型约束 1. 基本示例 考虑到组件的可重用性,引入了泛型的概念,可以使...

  • 泛型

    泛型 Why:为什么需要泛型 What:泛型是什么; How:泛型怎么实现 When:泛型什么时候使用 Where...

  • Java 19-5.1泛型

    泛型类定义泛型类可以规定传入对象 泛型类 和泛型方法 泛型接口 如果实现类也无法确定泛型 可以在继承类中确定泛型:

  • Kotlin之泛型的实化、协变、逆变

    1、泛型的实化 Java中泛型是在JDK1.5引入的,是一个伪泛型,它是通过泛型擦除机制来实现的。泛型只存在编译时...

  • Java-泛型

    关键字:泛型、类型擦除、泛型实现、泛型缺点、泛型运用。 1.不使用泛型会怎么样? 例子1,int、String元素...

网友评论

      本文标题:实现一个Table泛型组件

      本文链接:https://www.haomeiwen.com/subject/aqtxprtx.html