两百来行关键代码教你实现支持嵌套表头与列固定的可编辑表格

651 阅读5分钟

I don't wanna do this shit.png

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

前言

最近接到一个需求,需要在表单内嵌入一个支持用户编辑的表格,一看到原型图的那一刻,且出于对项目本身组件复杂度的考虑(是一个伴随着各种自定义计算规则、字段级联与联动场景的庞大的动态表单),以及 H5 端貌似没有适合的库,我说:“这做不了”。

但后来 pc 端的大佬没说啥,又耐不住项目紧急,于是开始着手组件设计了。

这个方案,pc 端也能使用,但接入的表单库可作调整,或者直接使用原生的表单控件也行。

Edit editable-table-demo

需求

  1. 支持嵌套表头

  2. 行支持多选

  3. 表格项可以为 input、checkbox、picker、radio 等控件

  4. 列需要支持 fixed,多列 fixed

  5. 表格是嵌入表单的,所以需要支持禁用

  6. 可调列宽、表头高度

  7. 支持展示模式,即为不可编辑的普通表格(为了适配后续其他增加的组件)

  8. 支持分页(也是为了适配后续新增的组件,本文暂不涉及,因为还没开始做)

组件设计

本着后续随时会接入动态表单的原则,组件的设计上需要尽量方便通过配置项来生成,也就是需要简单

我希望规定一种结构,能够很直观的体现出一行中,各个列与列内的关系。

也不希望而外传递一个表头的配置项,这样对于 fixed 以及 可调列宽而言,不好处理。

按照平常写组件的逻辑,children 的嵌套结构就很方便。于是乎有了如下结构:

按列划分关系


<Column>

    <HeaderCell>标题</HeaderCell>

    <Cell fieldKey="title"></Cell>{" "}

    {/* fieldKey 为当前单元格的字段名,取到的值应为 {title: ''} */}

</Column>

嵌套表头

那么,嵌套表头如何处理呢?

按照上面的设想得出如下结构:


<ColumnGroup header={"嵌套标题"}>

    <Column>

        <HeaderCell>标题1</HeaderCell>

        <Cell fieldKey="title1"></Cell>

    </Column>

    <Column>

        <HeaderCell>标题2</HeaderCell>

        <Cell fieldKey="title2"></Cell>

    </Column>

    <ColumnGroup header={"嵌套标题1"}>

        <Column>

            <HeaderCell>标题3-1</HeaderCell>

            <Cell fieldKey="title3_1"></Cell>

        </Column>

        <Column>

            <HeaderCell>标题3-2</HeaderCell>

            <Cell fieldKey="title3_2"></Cell>

        </Column>

    </ColumnGroup>

</ColumnGroup>

fixed、可调列宽

有了这种关系,指定 fixed 与 列宽也不是啥难事,直接传参就行。


<Column fixed>

{/* ... */}

</Column>



<ColumnGroup fixed>

{/* ... */}

</ColumnGroup>



<Column width={2}>

{/* 为了考虑到手机端的适配,简单采用 rem 方案 */}

{/* ... */}

</Column>



<ColumnGroup width={6}>

{/* ... */}

</ColumnGroup>

checkbox 列

此项一般用于第一列,用作行选中。


<Column fixed>

    <HeaderCell>——</HeaderCell>

    <Cell fieldKey="class" checkbox></Cell>

</Column>

自定义单元格渲染

可单独控制列其中一项的渲染方式。


<Column fixed>

    <HeaderCell>——</HeaderCell>

    <Cell

        fieldKey="class"

        checkbox

        renderCell={(val, index) => {

            // val 为当前的可编辑单元格的值,若是枚举则通过枚举取得对应的文字

            // index 为第 index 列

            // 配合 checkbox 使用会渲染到 checkbox 右侧

            if (index === 4) return "其他";

            return `套餐${index + 1}`;

        }}
    ></Cell>

</Column>

通过数据源生成行

按照上述设计,最终也只能渲染出一行,那么引入数据源即可根据数据源的长度渲染行数,也能做值回填等操作。

表单配置项

既然规定了行与列的关系以及单元格字段名,那么如何嵌入 input、radio、picker 等等的控件呢?

可以通过 children 或者传入表单配置项来实现。由于我公司使用的表单库是支持通过 json 配置来生成的,所以我选择直接传入。若各位与此有出入可以采用 children 传入 Cell 组件来实现。

最终结构


const data = [

    // 数据源

    { id: "0", title_1: "1", title_2: "0", title_3: "0", title_4: "11111" },

    { id: "1", title_1: "2", title_2: "2", title_3: "2", title_4: "22222" },

    { id: "2", title_1: "0", title_2: "1", title_3: "1", title_4: "33333" },

    { id: "3", title_1: "1", title_2: "1", title_3: "2", title_4: "44444" },

];



const yesOrNot = [

    // 枚举值

    {

        label: "√",

        value: "1",

    },

    {

        label: "-",

        value: "2",

    },

    {

        label: "x",

        value: "0",

    },

];



const formsData = [

    // 表单配置项

    {

        type: "picker",

        fieldProps: "title_1",

        data: yesOrNot,

        title: "标题1",

        defaultValue: "1",

    },

    {

        type: "picker",

        fieldProps: "title_2",

        data: yesOrNot,

        title: "标题2",

    },

    {

        type: "picker",

        fieldProps: "title_3",

        data: yesOrNot,

        title: "标题3",

    },

    {

        type: "input",

        fieldProps: "title_4",

        title: "标题4",

    },

];


<EditableTable

    disabled={false}

    headerHeight={3}

    data={data}

    formsData={formsData}

    onChange={(values) => {

        console.log({ editableTableValues: values });

    }}

>

    <Column fixed>

        <HeaderCell>分类</HeaderCell>

        <Cell fieldKey="class" checkbox></Cell>

    </Column>

    <ColumnGroup header={"嵌套标题"}>

        <ColumnGroup header={"嵌套标题1"}>

            <Column>

                <HeaderCell>标题1</HeaderCell>

                <Cell fieldKey="title_1"></Cell>

            </Column>

            <Column width={2}>

                <HeaderCell>标题2</HeaderCell>

                <Cell fieldKey="title_2"></Cell>

            </Column>

            <Column>

                <HeaderCell>标题3</HeaderCell>

                <Cell fieldKey="title_3"></Cell>

            </Column>

        </ColumnGroup>

    </ColumnGroup>

    <Column width={3}>

        <HeaderCell>标题4</HeaderCell>

        <Cell fieldKey="title_4"></Cell>

    </Column>

</EditableTable>

关键代码(生成逻辑)解析

相信各位看到这里还是会有些许迷糊的,心中也会有不少问题。

哎呀你这个讲的不清不楚的,就给了个结构,什么能够很直观的体现出一行中各个列与列内的关系。那要怎么去生成表格啊,按照你这结构,就只有列关系,那行关系呢?关键的东西不讲,写的个啥嘞。

诶!这就讲了。后面还会附有 demo 的。

关于行渲染以及为什么需要行渲染,是因为最终我们希望取得的结构是一个数组,数组内每一项则是行内的所有字段值,就像上述最终结构中的 data。而表头则需要与行分开来单独渲染。

实际上,我们需要点骚操作,才能取生成行,这边用到了操作 React.Children递归 来实现。

表头渲染

我们需要取到 Column 或者 ColumnGroup 内的 header 以及 HeaderCell 来生成表头:

code1.png

表格列渲染

组件内部增加了一个 Row 组件,只在内部使用。其目的在于获取行的 onChange,行的点击弹窗编辑(这个受限于 h5 端,这么实现比较方便用户操作)

code1.png

code1.png

code1.png

最终实现

image.png

image.png

Cell、HeaderCell、Column、ColumnGroup、Row 中的实现倒是简单,受限于篇幅问题,不详细展开,各位看官可移步 demo。上面部分代码复制过来会有格式问题,所以采用图片的形式,烦请各位不要介意。

Edit editable-table-demo