Skip to content

085. Form 监听、可编辑规则与保存流程 ​

学习目标 ​

这一节继续完善行内编辑。

学完后,你应该能理解:

  • record 和 Form 中实时编辑数据的区别;
  • 为什么要用 Form.useWatch;
  • 每一行为什么要维护自己的 Form;
  • 行编辑状态为什么放在表格外层管理;
  • 列级 editable 如何控制单元格是否可编辑;
  • required 和 rules 如何进入校验;
  • 保存时如何区分新增和更新。

自动撑开列 ​

表格里有些列需要固定宽度。

例如操作列通常可以固定为 120。

但如果所有列都有固定宽度,表格可能显得拥挤。

课程里加了一个自动撑开宽度的列。

它不设置明确宽度。

这样浏览器会把剩余空间分配给它。

底层可以从表格生成的 colgroup 看到每一列宽度如何生效。

原始行数据和编辑数据 ​

行内编辑里有两份数据。

第一份是原始行数据。

它来自表格的 dataSource。

可以理解为:

ts
record

这份数据只有在保存成功、表格数据被更新后才会变化。

第二份是 Form 里的实时编辑数据。

用户正在输入时,变化先发生在 Form 内部。

此时 record 还没有变化。

为什么需要 Form.useWatch ​

有些单元格渲染时,需要拿到最新编辑值。

例如两个列显示同一个字段。

当用户在一个输入框里修改值时,另一个单元格也应该能看到最新值。

如果只读 record,它不会更新。

因为 record 是保存后的数据。

所以需要通过 Form.useWatch 监听 Form 内部数据。

可以把它理解成:

ts
const formData = Form.useWatch([], form)

formData 表示当前行正在编辑的实时值。

Form.Item 才会纳入表单控制 ​

只有被 Form.Item 包裹并声明 name 的字段,才会进入 Form 管理。

没有对应 Form.Item 的字段,不会出现在 Form 的编辑值里。

这点很重要。

例如创建时间、更新时间、ID 这类字段通常不需要编辑。

它们可以展示在行数据里,但不一定进入 Form。

每一行一个 Form ​

行内编辑选择每一行维护一个 Form。

原因是保存、取消、校验都是按行发生的。

如果整张表只用一个 Form,所有行的字段都会混在一起。

这样会带来很多问题:

  • 保存一行时难以只校验这一行;
  • 取消一行时难以只重置这一行;
  • 新增多行时字段命名和状态管理会复杂很多;
  • 统一保存或统一取消也更难协调。

所以每一行一个 Form 是更稳定的选择。

Ant Design Pro 的可编辑表格也采用类似思路。

联动字段更新 ​

有些字段编辑后,不只更新自己。

例如项目负责人字段。

后端可能保存负责人 ID。

但界面还要显示负责人名称。

选择一个负责人后,可能需要同时更新:

  • userId;
  • userName。

这种情况下,可以通过 Form 实例更新多个字段。

类似:

ts
form.setFieldsValue({
  userId,
  userName,
})

这也是为什么单元格渲染需要拿到 Form 和实时编辑数据。

编辑状态放在外层 ​

行是否可编辑,不能只放在行组件内部。

如果状态只存在某个 TableRow 里,会遇到几个问题:

  • 新增行插入表格后,无法方便地让它立即进入编辑状态;
  • 批量取消时,需要找到所有行组件实例;
  • 批量保存时,也需要从所有行组件里收集状态;
  • 行组件卸载后,状态不容易统一管理。

所以编辑状态放在通用表格外层。

行组件只接收计算后的结果。

这样表格可以统一控制新增、编辑、取消和保存。

调试状态更新 ​

React 状态更新不是立即同步到闭包变量里。

在调试时,如果直接看闭包里的旧变量,可能会误以为状态没有变化。

课程里用额外方法读取最新值,并打印更新前和更新后的映射。

这能更直观看到:

  • 双击某行后,对应 ID 被标记为编辑状态;
  • 取消某行后,对应 ID 从编辑映射里移除。

这类调试对理解 React 状态更新很有帮助。

行组件优化 ​

行组件要处理两种情况。

第一种是普通数据行。

它需要创建 Form、注入上下文、绑定编辑状态。

第二种是空数据或占位行。

它没有真实 record 和 index,只需要正常渲染 tr。

这样可以避免加载中或空状态时读取行字段导致错误。

行上下文可以用 useMemo 缓存。

目的不是改变功能,而是减少不必要的重新计算和重新渲染。

列级 editable ​

不是每个单元格都可编辑。

列配置可以提供 editable。

它可以是布尔值。

例如:

ts
editable: false

表示这一列永远不可编辑。

它也可以是函数。

例如根据当前行数据判断是否可编辑。

这种场景很常见。

比如某字段只有在状态为草稿时才能修改,审批后就不能修改。

行可编辑和列可编辑 ​

单元格是否进入编辑态,要同时看两个条件。

第一,当前行是否处于编辑状态。

第二,当前列是否允许编辑。

所以逻辑可以理解成:

ts
cellEditable = rowEditable && columnEditable

如果行没有进入编辑状态,列配置再允许也不会显示编辑组件。

如果行进入编辑状态,但列配置禁止编辑,也只显示普通内容。

校验规则 ​

列配置里可以提供校验规则。

例如 Ant Design Form 的 rules。

也可以提供快捷的 required。

当 required 为真时,自动追加必填规则。

例如:

ts
required: true

可以转成:

ts
{ required: true, message: "请填写" }

如果列本身还有更多规则,也要一起传给 Form.Item。

例如用 pattern 要求字段必须以某个文本开头。

表格参数用类型约束 ​

课程里还优化了传给 Table 的参数。

例如分页参数、行事件参数。

在 TypeScript 中,如果直接返回对象,编辑器可能不知道具体类型。

可以通过显式类型或 satisfies 约束。

这样写配置时能拿到字段提示。

对新手来说,先记住一个经验:

复杂组件的配置对象,最好让 TypeScript 知道它到底是什么类型。

否则自动补全和类型检查都会变弱。

保存流程 ​

保存行数据时,需要先判断它是新增还是更新。

判断方式来自临时 ID。

如果 ID 是前端生成的新建 ID,就走新增接口。

如果是后端已有 ID,就走更新接口。

流程大致是:

  1. 找到当前行在数据数组里的位置;
  2. 找到这一行对应的 Form 实例;
  3. 根据配置决定是否校验;
  4. 读取编辑后的表单值;
  5. 判断新增还是更新;
  6. 组装请求;
  7. 调用模块接口;
  8. 用接口返回的新数据替换表格里的旧行;
  9. 清理编辑状态或新增状态;
  10. 提示保存成功。

新增和更新接口 ​

新增时调用 insert。

更新时调用 update。

它们都属于通用模块对象提供的接口能力。

这样通用表格不需要知道具体业务接口路径。

它只需要根据配置里的模块信息,生成对应请求。

保存前清理临时 ID ​

新增行有前端临时 ID。

这个 ID 只是为了让表格能渲染。

保存到后端时不能把它当成真实主键。

所以新增请求前要把这个临时 ID 清掉。

由后端生成真正的 ID。

保存成功后,再用后端返回的新数据更新表格行。

undefined 转成 null ​

保存前还会处理字段值。

如果某个字段是 undefined,请求序列化时可能会被忽略。

但有时用户的真实意图是清空这个字段。

如果字段被忽略,后端按需更新时就不会改它。

所以课程里把 undefined 转成 null。

这样能明确告诉后端:这个字段要清空。

保存前校验 ​

保存时可以先执行 Form 校验。

如果校验失败,就不要发请求。

例如字段必须以指定文本开头。

输入不符合规则时,Form 会提示错误。

校验通过后,才读取表单值并进入保存请求。

这比请求失败后再回头提示更适合前端体验。

更新表格数据 ​

接口成功后,会返回新增或更新后的行数据。

前端要用它替换当前表格里的旧行。

不能只相信本地编辑值。

因为后端可能补充了字段。

例如:

  • 新 ID;
  • 创建时间;
  • 更新时间;
  • 关联对象名称;
  • 服务端计算字段。

所以保存成功后,应该以接口返回值为准。

这一节的核心 ​

行内编辑逐渐从“能显示输入框”变成真正可用的表格能力。

关键变化是:

  • 用 Form.useWatch 读取实时编辑数据;
  • 用列级 editable 控制单元格是否可编辑;
  • 用 rules 和 required 接入校验;
  • 用 Form 实例执行校验和取值;
  • 用模块接口区分新增和更新;
  • 用后端返回结果更新本地表格数据。

到这里,通用表格已经具备保存单行编辑数据的基础闭环。

AI Agent 课程学习文档。