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.Item 的 name ​

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

name 对应字段路径。

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

同时使用 noStyle。

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

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

取消编辑要重置值 ​

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

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

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

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

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

Omit 的用途 ​

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

课程里提到 Omit。

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

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

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

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

ts
delete nextMapper[id]

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

这一节的核心 ​

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

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

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

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

AI Agent 课程学习文档。