切换日光/暗黑模式
083. 列默认值与行编辑状态管理
学习目标
这一节把通用表格的列配置继续往前推进,并开始进入行编辑功能。
学完后,你应该能理解:
- 为什么列类型要能生成完整的列配置;
- 列默认值为什么要放到各自列模块里;
getRowsMapper这类工具函数解决什么问题;- 下拉、多选、开关列的显示和编辑逻辑有什么差异;
- 行编辑状态为什么需要独立的 ID 映射;
FormInstance为什么要和行数据建立对应关系。
从列类型到完整列配置
前面已经设计了可扩展的列类型。
核心流程是:
- 每种列在自己的模块里定义扩展类型;
- 通过扩展入口把类型合并起来;
- 给每种列补上
type; - 再叠加基础列属性;
- 最后得到完整的列联合类型。
对刚接触 TypeScript 的同学,可以先把它理解成“列配置注册表”。
不同列都把自己的类型放进注册表里,通用表格只面向这个统一结果工作。
单独取某一种列类型
完整列类型是联合类型。
例如:
ts
InputColumn | SelectColumn | ToggleColumn有时还需要单独拿到某一种列。
例如只取输入框列、下拉列、开关列。
这样做的价值是:
- 下拉列必须有
options; - 开关列必须有真假值配置;
- 输入框列不需要这些额外字段。
类型系统会提醒你某一类列应该具备哪些属性。
这比全都写成 any 更适合长期维护。
列默认值
列配置不能只靠业务页面手写。
通用表格要自动补充默认行为。
例如:
- 索引列默认标题是序号;
- 输入框列默认宽度可以是
100; - 输入框列默认显示原始值;
- 输入框列行内编辑时默认渲染输入框;
- 日期列、开关列可以有不同默认宽度。
这些默认值组成一个映射对象。
它的 key 对应列类型。
它的 value 是一个函数,负责接收原始列配置,再返回补全后的列配置。
可以把它理解成:
ts
const defaultColumnMap = {
input: fillInputColumnDefaults,
select: fillSelectColumnDefaults,
toggle: fillToggleColumnDefaults,
}真正写代码时,key 和函数参数都要跟列类型系统对齐。
默认值为什么放回列模块
如果所有默认值都写在一个总文件里,后面新增列类型就会不断改总文件。
这会让通用组件变得很难扩展。
更合适的方式是:
- 输入框列自己定义输入框列类型;
- 输入框列自己注册输入框列默认值;
- 下拉列自己处理下拉选项;
- 开关列自己处理真假值。
通用表格的工具层只负责组织流程。
它不应该知道每种业务列的全部细节。
选项数组转换
下拉列的 options 可能有两种来源。
一种是字符串数组:
ts
["启用", "停用"]另一种是对象数组:
ts
[
{ label: "启用", value: "Y" },
{ label: "停用", value: "N" },
]为了后面统一读取,需要先把它们转成稳定结构。
否则显示、编辑、多选、查询都会重复写判断。
getRowsMapper
课程里写了一个工具函数,用来把数组转换成映射对象。
常见用途包括:
- 根据
id找code; - 根据
value找label; - 根据某个字段找完整对象。
例如:
ts
const valueToLabel = getRowsMapper(options, {
key: "value",
value: "label",
})对于 JS 开发经验来说,这就是把数组预处理成对象。
好处是后面查值不用每次 find。
单选与多选
下拉列分为单选和多选。
单选时,值可以直接通过 valueToLabel 找到显示文本。
多选时,课程里先约定后端保存逗号分隔字符串。
例如:
ts
"read,write,admin"显示时要拆成数组,再逐个找到 label。
编辑时组件可能返回数组,但提交给后端前要再转回字符串。
这里的重点不是逗号分隔一定最好,而是前后端必须有一致的数据协议。
开关列
开关列不是简单的 true 和 false。
很多业务系统会用别的值表示真假。
例如:
ts
trueValue: "Y"
falseValue: "N"也可能是:
ts
trueValue: 1
falseValue: 0所以开关列要自己维护真假值映射。
非编辑状态下,开关组件只负责展示。
编辑状态下,开关变化后要把业务值写回表单。
TypeScript 类型保护与 any
根据 column.type 分支判断时,TypeScript 可以收窄类型。
例如:
ts
if (column.type === "select") {
// 这里可以安全读取 select 列字段
}这种方式类型最安全。
但它有一个扩展问题:每新增一种列,总文件就要新增一个分支。
课程里选择在确认运行路径可控的位置使用 any。
这里不是鼓励随便写 any,而是在“列类型和默认值注册机制已经保证对应关系”的前提下,避开 TypeScript 暂时无法推导的地方。
实际项目里可以记住一个判断:
如果 any 是为了偷懒,风险很高。
如果 any 是在明确的注册机制内部使用,而且外层类型已经约束好,风险会小很多。
行编辑状态
列默认值处理完后,课程开始进入表格编辑功能。
行编辑需要维护几个状态。
第一个是正在编辑的行 ID 映射。
例如:
ts
updateIdMapper[id] = true它表示某一行正在编辑。
这种写法比只存一个 editingId 更灵活,因为未来可能支持多行同时编辑。
新增行状态
新增行还没有后端 ID。
但 Ant Design Table 要求每一行都有稳定 ID。
所以新增行会先生成一个临时 ID。
例如用 new 作为前缀。
保存时,如果检测到这是临时 ID,就不要把它当作真实 ID 传给后端。
新增行状态也会用一个映射记录。
它表示这条数据还没有真正落库。
计算编辑状态
真正决定某一行是否进入编辑态时,会综合两个来源:
- 已有数据正在编辑;
- 新增数据正在编辑。
所以会有一个计算出来的 ID 映射。
只要某个 ID 出现在编辑映射或新增映射里,这一行就应该进入编辑状态。
这也是前端状态管理的一个常见思路:
原始状态分开保存,界面需要的状态再计算出来。
FormInstanceManager
行编辑不是只改一份普通对象。
每一行内部会有一个 Ant Design Form。
Form 实例负责:
- 触发表单校验;
- 读取当前编辑值;
- 设置字段值;
- 重置编辑状态。
所以需要把“行数据”和“Form 实例”关联起来。
课程里使用 WeakMap。
它的 key 可以是行对象,value 是对应的 Form 实例。
这样每一行渲染时把自己的 Form 注册进去,卸载时再删除。
为什么用 WeakMap
普通对象的 key 只能稳定使用字符串或 Symbol。
而这里要用行对象作为 key。
WeakMap 正好适合这种场景。
它能表达:
ts
record -> formInstance对刚接触这块的同学,可以先不用记所有细节。
先记住用途:通过当前行数据,找到这一行自己的表单控制对象。
这一节的核心
通用表格不是把 Ant Design Table 包一层就结束。
它要自己补齐一整套协议:
- 列类型;
- 列默认值;
- 选项映射;
- 显示渲染;
- 编辑渲染;
- 新增行状态;
- 编辑行状态;
- 行和表单实例的关系。
这些协议看起来比普通页面复杂。
但它们带来的结果是:后面业务页面只需要声明字段,通用表格就能自动生成展示、编辑和保存流程。