Skip to content

084. 行内编辑、操作列与表单上下文

学习目标

这一节实现通用表格行内编辑的基础效果。

学完后,你应该能理解:

  • 行编辑为什么要维护多个状态;
  • 初始化配置为什么不要每次重新计算;
  • 操作列如何进入通用列体系;
  • React Context 如何把表格能力传给子组件;
  • 行组件和单元格组件如何配合 Form;
  • 取消编辑时为什么要重置表单值。

行编辑的四类状态

行内编辑不是一个布尔值就能解决。

课程里先维护四类数据。

第一类是普通编辑行映射。

它记录哪些已有数据正在编辑。

例如:

ts
updateIdMapper[id] = true

第二类是新增行映射。

它记录哪些数据是前端临时插入、尚未保存到后端的新行。

第三类是计算后的编辑映射。

行组件不需要关心自己是新增还是编辑。

它只需要知道:当前行是否应该进入编辑状态。

第四类是 FormInstanceManager

它维护行对象和 Form 实例之间的关系。

新增行和编辑行为什么分开

已有数据取消编辑时,只需要退出编辑状态。

新增数据取消编辑时,应该把这行直接删掉。

所以它们不能只放在一个状态里。

否则取消时就不知道应该“恢复显示”还是“删除行”。

这也是前端状态设计里的一个原则:

界面看起来相似的状态,业务含义不同,就应该分开保存。

Form 实例的作用

Ant Design Form 的实例对象很重要。

它可以做几类事情:

  • 触发表单校验;
  • 读取当前编辑值;
  • 设置字段值;
  • 重置字段值。

行内编辑时,每一行都需要自己的 Form。

这样保存某一行时,才能只校验这一行,只读取这一行当前编辑的值。

WeakMap 保存行和 Form 的关系

行对象和 Form 实例之间是一对一关系。

课程里使用 WeakMap 保存。

关系类似:

ts
record -> formInstance

因为 key 是对象,所以普通对象不适合。

WeakMap 允许对象作为 key。

当行对象被移除后,它也更适合被垃圾回收。

对于刚接触这块的同学,先记住用途就够了:通过某一行的数据对象,找到这一行自己的表单实例。

初始化配置不要反复计算

表格配置里包含列配置。

列配置要经历默认值填充、操作列注入、渲染函数补齐等处理。

如果父组件每次渲染都传一个新对象,通用表格就会反复重新计算。

所以这里把配置放入内部状态。

可以把初始配置传成函数。

类似 React 里:

ts
useState(() => createInitialConfig())

这种写法只会在第一次初始化时执行函数。

后续如果真的要修改配置,再通过专门的方法更新内部配置。

操作列进入列体系

行内编辑需要操作列。

操作列在非编辑状态下显示:

  • 编辑;
  • 删除。

编辑状态下显示:

  • 保存;
  • 取消。

操作列不应该作为页面临时写死的列。

它应该像输入框列、下拉列、开关列一样,进入通用列体系。

因此它也有自己的默认宽度、固定位置、渲染逻辑。

操作列拆成组件

操作列的非编辑状态和编辑状态被拆成独立组件。

这样做有两个原因。

第一,代码更清楚。

非编辑状态只关心编辑和删除。

编辑状态只关心保存和取消。

第二,React 热更新对组件文件结构更敏感。

把组件拆开,开发时更稳定。

表格上下文

操作列里的按钮需要调用表格能力。

例如:

ts
editRecord(record)
deleteRecord(record)
saveRecord(record)
cancelEditRecord(record)

这些方法定义在通用表格对象上。

如果一层层通过 props 传,会很繁琐。

所以课程里使用 React Context。

表格在外层提供上下文。

操作列、行组件、单元格组件从上下文里读取表格对象。

Context 默认值与报错

React 创建 Context 时需要默认值。

但通用表格的上下文没有合理默认值。

所以可以把默认值设为 null

再封装一个读取函数。

如果读取时发现没有注入上下文,就直接报错。

这种写法比让组件继续运行更好。

因为没有表格上下文时,编辑、删除、保存这些行为本来就无法正确工作。

自动注入操作列

真正传给 Ant Design Table 的列,不只是业务配置里的列。

它会先加上操作列。

然后再统一填充默认值。

流程可以理解成:

ts
const tableColumns = [
  ...runningConfig.columns,
  operationColumn,
].map(fillDefaultColumn)

这样页面只需要声明业务字段。

表格会自己补上操作列。

单元格渲染

Ant Design Table 使用 render 渲染单元格。

而通用表格内部设计了:

  • inlineRender:非编辑状态显示;
  • inlineEditor:编辑状态显示。

所以需要一个单元格组件做中转。

它根据当前行是否正在编辑,选择不同渲染方式。

非编辑状态:

  • 优先使用 inlineRender
  • 没有就显示原始值。

编辑状态:

  • 优先使用 inlineEditor
  • 没有就回退到普通显示。

行组件负责创建 Form

每一行会被包装成一个行组件。

行组件要做几件事:

  • 判断当前行是否处于编辑状态;
  • 创建这一行自己的 Form 实例;
  • 把行数据和 Form 实例注册到 FormInstanceManager
  • 把行上下文传给单元格。

行组件的上下文会包含:

  • 行数据;
  • 行索引;
  • 是否编辑;
  • Form 实例。

单元格通过这个上下文知道自己应该怎么渲染。

加载占位行要特殊处理

表格加载时,可能会渲染空状态或占位行。

这种行没有真实 record

所以行组件要判断:

如果没有行数据,就不要进入编辑逻辑。

否则会出现读取 record.id 的错误。

Form.Itemname

编辑状态下,单元格内容会放进 Form.Item

name 对应字段路径。

这里用列配置里的字段作为表单字段名。

同时使用 noStyle

因为行内编辑不需要额外的表单布局标签。

它只需要让当前单元格成为表单字段。

取消编辑要重置值

用户编辑一行后,如果点击取消,界面应该恢复到原始值。

如果只退出编辑状态,不重置 Form,下一次再进入编辑时,可能还会看到上次未保存的输入。

所以行组件要监听编辑状态变化。

当这一行从编辑状态变成非编辑状态时,调用 Form 的重置方法。

这样再次编辑时,字段值会回到当前行数据。

Omit 的用途

取消某些行的编辑状态,本质是从映射对象里删除这些 ID。

课程里提到 Omit

类型层面的 Omit 是从对象类型里去掉某些 key。

业务代码里也会做类似事情:

从编辑映射对象里移除某些 ID。

对 JS 开发者来说,可以理解成:

ts
delete nextMapper[id]

只是实际实现可能会用不可变数据方式返回新对象。

这一节的核心

行内编辑的关键不是按钮本身。

关键是建立一套稳定的数据关系:

  • 表格对象提供编辑、保存、取消、删除方法;
  • 操作列触发这些方法;
  • 行组件判断是否编辑;
  • 行组件创建并注册 Form;
  • 单元格根据编辑状态选择显示或编辑组件;
  • 取消编辑时重置未保存的表单值。

这套关系搭起来后,通用表格才有能力继续扩展保存、新增、多行编辑和字段校验。

AI Agent 课程学习文档。