切换日光/暗黑模式
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;
- 单元格根据编辑状态选择显示或编辑组件;
- 取消编辑时重置未保存的表单值。
这套关系搭起来后,通用表格才有能力继续扩展保存、新增、多行编辑和字段校验。