携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
前言
最近接到一个需求,需要在表单内嵌入一个支持用户编辑的表格,一看到原型图的那一刻,且出于对项目本身组件复杂度的考虑(是一个伴随着各种自定义计算规则、字段级联与联动场景的庞大的动态表单),以及 H5 端貌似没有适合的库,我说:“这做不了”。
但后来 pc 端的大佬没说啥,又耐不住项目紧急,于是开始着手组件设计了。
这个方案,pc 端也能使用,但接入的表单库可作调整,或者直接使用原生的表单控件也行。
需求
-
支持嵌套表头
-
行支持多选
-
表格项可以为 input、checkbox、picker、radio 等控件
-
列需要支持 fixed,多列 fixed
-
表格是嵌入表单的,所以需要支持禁用
-
可调列宽、表头高度
-
支持展示模式,即为不可编辑的普通表格(为了适配后续其他增加的组件)
-
支持分页(也是为了适配后续新增的组件,本文暂不涉及,因为还没开始做)
组件设计
本着后续随时会接入动态表单的原则,组件的设计上需要尽量方便通过配置项来生成,也就是需要简单。
我希望规定一种结构,能够很直观的体现出一行中,各个列与列内的关系。
也不希望而外传递一个表头的配置项,这样对于 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 来生成表头:
表格列渲染
组件内部增加了一个 Row 组件,只在内部使用。其目的在于获取行的 onChange,行的点击弹窗编辑(这个受限于 h5 端,这么实现比较方便用户操作)
最终实现
Cell、HeaderCell、Column、ColumnGroup、Row 中的实现倒是简单,受限于篇幅问题,不详细展开,各位看官可移步 demo。上面部分代码复制过来会有格式问题,所以采用图片的形式,烦请各位不要介意。