可编辑表格是 ProTable 中呼声比较高的功能,在项目中虽然使用频率不高,但是实现起来难度确不小。所以 ProTable 在完成了架构升级后,就开始着手可编辑表格的开发,现在它终于来了。

🧚🏻‍♀️ 默认好看的样式

作为了 Ant Design 的衍生作品,我们对 EditableTable 的样式进行了预设,我们很容易就能做出这么好看的可编辑表格。同时我们提供了顶部添加和底部添加两种模式,用于适应不同的场景。我们可以做到默认好看,同时默认好用。

image.png

为了改善在狭窄空间内的错误提示,我们重写错误信息的展示方式,使用 Tooltip 类似的方式来展示错误。当然为了防止抖动,我们也重写了Form 的样式使其更加的适合狭窄的输入区域,这样在使用 EditableTable 时并不会产生明显的跳动感,感觉非常顺滑。
image.png

👩🏻‍🦽 默认好用的 API

EditableTable 定义了一套和 Ant Design 相同的 API , 如果你是熟练的 Ant Design 使用者在使用时会感觉到非常的熟悉。为了方便大家使用 EditableTable 修改了 valueonChange ,只要放到 Form 中就会像 Input 一样自动绑定数据。

除了 value 和 onChange 我们还提供了 editable 来自定义编辑表格的行为,包括是否支持多行编辑,当前正在编辑行的 key 等,基本可以满足所有的开发需求。

属性 描述 类型
type 可编辑表格的类型,单行编辑或者多行编辑 single | multiple
editableKeys 正在编辑的行,受控属性。 默认 key 会使用 rowKey 的配置,如果没有配置会使用 index Key[]
onChange 行的数据发生改变时触发 (editableKeys:Key[], editableRows: T[]) => void
onSave 保存一行的时候触发,只更新 (key: Key, row: T,newLine?: newLineConfig) => Promise<boolean>
onDelete 删除一行的时候触发 (key: Key, row: T) => Promise<boolean>
onCancel 取消行编辑时触发 (key: Key, row: T,newLine?: newLineConfig) => Promise<boolean>
actionRender 自定义编辑模式的操作栏 (row: T, config: ActionRenderConfig<T>) => React.ReactNode[]

🎣 如何使用?

editable 编辑配置

市面上的可编辑表格是非常多的,但是很多使用起来非常麻烦,Table 的表单区域虽然小但是输入控件却不简单,常见的文本,下拉框,数组,日期甚至有时候还会有麻烦的日期区间等,那么 EditableTable 是如何解决这个问题的?

EditableTable 是基于 ProTable 实现的,在 ProTable 中我们是有查询表单这个功能的,通过配置不同的 valueType 就可以生成不同的查询表单,可编辑表格也是使用了同样的 API ,下图是 ProTable 支持的所有日期类的 valueType

image.png

在这样的能力加持下,EditableTable 的使用变得非常简单,我们可以像 rowSelection 那样使用 editable ,下面是一个简单的例子。

  1. const columns: ProColumns<DataSourceType>[] = [
  2. {
  3. title: '活动名称',
  4. dataIndex: 'title',
  5. formItemProps: {
  6. rules: [
  7. {
  8. required: true,
  9. message: '此项为必填项',
  10. },
  11. ],
  12. },
  13. },
  14. {
  15. title: '状态',
  16. key: 'state',
  17. dataIndex: 'state',
  18. valueType: 'select',
  19. valueEnum: {
  20. all: { text: '全部', status: 'Default' },
  21. open: {
  22. text: '未解决',
  23. status: 'Error',
  24. },
  25. closed: {
  26. text: '已解决',
  27. status: 'Success',
  28. },
  29. },
  30. },
  31. {
  32. title: '描述',
  33. dataIndex: 'decs',
  34. valueType: 'text'
  35. },
  36. {
  37. title: '操作',
  38. valueType: 'option',
  39. render: (text, record, _, action) => [
  40. <a
  41. key="editable"
  42. onClick={() => {
  43. action.startEditable?.(record.id);
  44. }}
  45. >
  46. 编辑
  47. </a>,
  48. ],
  49. },
  50. ];
  51. const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
  52. const [dataSource, setDataSource] = useState<DataSourceType[]>([]);
  53. <EditableProTable<DataSourceType>
  54. rowKey="id"
  55. headerTitle="可编辑表格"
  56. columns={columns}
  57. value={dataSource}
  58. onChange={setDataSource}
  59. recordCreatorProps={{
  60. position: 'end',
  61. record: { id: (Math.random() * 1000000).toFixed(0) },
  62. }}
  63. editable={{
  64. editableKeys,
  65. onChange: setEditableRowKeys,
  66. }}
  67. />;

我们可以控制 editableKeys 来修改当前编辑的行,value 来控制当前的数据。以上的代码会生成下面的样式。

image.png

action 默认行为

可编辑表格的重点是 action ,这个变量有点像 Form 的 formInstance 实例,你可以调用它的方法来操作可编辑表格的一些行为
,以下是支持的 API 列举:

  • action.startEditable(rowKey) 开始编辑一行
  • action.cancelEditable(rowKey) 结束编辑一行,相当于取消
  • action.addEditRecord(row) 新增一行,row 相当于默认值,一定要包含 rowKey

recordCreatorProps 新建按钮配置

为了使用,我们预设了一个新建的功能,大多数情况下已经可以满足大部分新建的需求,但是很多时候需求总是千奇百怪。我们也准备了 recordCreatorProps 来控制生成按钮。与 Pro系列组件的API 相同,recordCreatorProps={false}就可以关掉按钮,同时使用 actionRef.current?.addEditRecord(row) 来控制新建行。

recordCreatorProps 也支持自定义一些样式,position='top'|'end' 可以配置增加在表格头还是表格尾部。record 可以配置新增行的默认数据。以下是一个列举

  1. recordCreatorProps={
  2. // 顶部添加还是末尾添加
  3. position: 'end',
  4. // 不写 key ,会使用 index 当行 id
  5. record: {},
  6. // https://ant.design/components/button-cn/#API
  7. ...antButtonProps,
  8. }

renderFormItem 自定义编辑组件

虽然我们很希望默认的 valueType 可以满足所有的需求,但是现实往往不尽如人意。所以我们也提供了 renderFormItem 来自定义编辑输入组件。

renderFormItem 可以理解为在 Form.Item 下面加入一个元素, 伪代码实现是下面这样的:

  1. const dom = renderFormItem();
  2. <Form.Item>
  3. {dom}
  4. </Form.Item>

所以与 Form.Item 相同,我们认为 renderFormItem 返回的组件都是拥有的 valueonChange 的,我们接下来将看到用 renderFormItem 将一个简单的 TagList 组件放入可编辑表格中。

没有 value 将会无法注入值,没有 onChange 会无法修改行数据

首先我们定义一个 TagList 组件。

  1. const TagList: React.FC<{
  2. value?: {
  3. key: string;
  4. label: string;
  5. }[];
  6. onChange?: (
  7. value: {
  8. key: string;
  9. label: string;
  10. }[],
  11. ) => void;
  12. }> = ({ value, onChange }) => {
  13. const ref = useRef<Input | null>(null);
  14. const [newTags, setNewTags] = useState<
  15. {
  16. key: string;
  17. label: string;
  18. }[]
  19. >([]);
  20. const [inputValue, setInputValue] = useState<string>('');
  21. const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  22. setInputValue(e.target.value);
  23. };
  24. const handleInputConfirm = () => {
  25. let tempsTags = [...(value || [])];
  26. if (inputValue && tempsTags.filter((tag) => tag.label === inputValue).length === 0) {
  27. tempsTags = [...tempsTags, { key: `new-${tempsTags.length}`, label: inputValue }];
  28. }
  29. onChange?.(tempsTags);
  30. setNewTags([]);
  31. setInputValue('');
  32. };
  33. return (
  34. <Space>
  35. {(value || []).concat(newTags).map((item) => (
  36. <Tag key={item.key}>{item.label}</Tag>
  37. ))}
  38. <Input
  39. ref={ref}
  40. type="text"
  41. size="small"
  42. style={{ width: 78 }}
  43. value={inputValue}
  44. onChange={handleInputChange}
  45. onBlur={handleInputConfirm}
  46. onPressEnter={handleInputConfirm}
  47. />
  48. </Space>
  49. );
  50. };

在列中我们可以这样配置它。

  1. {
  2. title: '标签',
  3. dataIndex: 'labels',
  4. width: '40%',
  5. renderFormItem: () => <TagList />,
  6. render: (_, row) => row?.labels?.map((item) => <Tag key={item.key}>{item.label}</Tag>),
  7. },

转化成的编辑表格效果如下 :

image.png

value 和 onChange 会自动注入,我们不需要显式的注入。数据绑定也是由编辑表格自己注入的,我们在 onSave 中可以拿到处理完成的数据。虽然我们可以行内的写出复杂的逻辑甚至网络请求,但是我们仍然推荐拆分组件,这样不仅性能更好,逻辑也可以拆分到另外的地方。

renderFormItem 同时也用来生成查询表单,如果我们需要区分这两种情况,可以使用 renderFormItem: (_, { isEditable }) => (isEditable ? <TagList /> : <Input /> ) 这样的方式来进行分别渲染。

actionRender 自定义操作栏

可编辑表格默认提供了三大金刚, 保存,删除 和 取消,如果我们要实现复制一行,或者需求只需要的 保存和取消,不需要删除按钮就需要自定义了。可编辑表格提供了 API 来进行自定义,以下会直接展示代码:

复制一行到底部

  1. render: (text, record, _, action) => [
  2. <a
  3. key="editable"
  4. onClick={() => {
  5. action.startEditable?.(record.id);
  6. }}
  7. >
  8. 编辑
  9. </a>,
  10. <EditableProTable.RecordCreator
  11. record={{
  12. ...record,
  13. id: (Math.random() * 1000000).toFixed(0),
  14. }}
  15. >
  16. <a>复制此行到末尾</a>
  17. </EditableProTable.RecordCreator>,
  18. ]

自定义操作栏

  1. const editable = {
  2. actionRender: (row, config) => [
  3. <a
  4. key="save"
  5. onClick={async () => {
  6. const values = (await config?.form?.validateFields()) as DataSourceType;
  7. const hide = message.loading("保存中。。。");
  8. await config?.onSave?.(config.recordKey, { ...row, ...values });
  9. hide();
  10. }}
  11. >
  12. 保存
  13. </a>,
  14. <a
  15. key="save"
  16. onClick={async () => {
  17. await config?.onCancel?.(config.recordKey, row);
  18. }}
  19. >
  20. 取消
  21. </a>,
  22. ],
  23. };

🔐 何时应该使用

ProComponents 的测试覆盖了达到了 97%,虽然离 antd 的 100% 还有很长的距离,但是已经可以保证不会因为变更而出现恼人的不兼容问题,同时在内部已经在数个项目中使用。如果你仍然保有疑虑,可以在我们的官网体验。
image.png
如果使用中碰到了任何问题,都可以提 issue,或者直接进行 PR。也许你的想法和意见可以帮助到更多的人。

🔔 其他事情

ProComponents 同时还提供了其他的组件。

  • ProLayout 解决布局的问题,提供开箱即用的菜单和面包屑功能
  • ProTable 解决表格问题,抽象网络请求和表格格式化
  • ProForm 解决表单问题,预设常见布局和行为
  • ProCard 提供卡片切分以及栅格布局能力
  • ProDescriptions 提供与 table 使用同等配置的能力
  • ProSkeleton 页面级别的骨架屏

其中的 ProTable ,ProForm ,ProDescriptions 使用了同样的架构,以 valueType 为核心的场景化的格式工具,如果你喜欢可编辑表格,那么你也会喜欢同样的可编辑定义列表,同样的还有只读的表单。

最后如果有什么问题,欢迎来提 issue吐槽讨论。 ProComponents 还是个新生的组件库,如果有 star 和 PR 那就更加完美。