编辑会话
import { DataGridEditSession, EditSession, FormSession,} from 'ds-ui'
编辑会话保存“进入编辑时的原值”和“当前草稿”之间的差异。它适合维护资料、录入申请单、弹窗编辑和其他需要保存、取消、脏状态判断的页面。
EditSession 只管理值和编辑状态;FormSession 在它上面组合校验、错误和提交。两者都不扫描组件树,不代理业务对象,也不依赖某个前端框架。
创建会话
interface OrderDraft { drugCode: string drugName: string specification: string unit: string price: number} const session = new EditSession<OrderDraft>({ drugCode: '', drugName: '', specification: '', unit: '', price: 0,}) session.setValue('drugCode', 'MED001')const draft = session.snapshot()
构造参数会被复制为 baseline 和 draft。默认复制是浅拷贝,字段相等判断使用 Object.is。
如果模型包含数组或嵌套对象,调用方应把它们当成不可变值,或者提供符合业务语义的 clone 和 equals:
interface TagDraft { name: string tags: string[]} const session = new EditSession<TagDraft>( { name: '常用模板', tags: ['内科'] }, { clone: value => ({ ...value, tags: [...value.tags] }), equals: (field, left, right) => ( field === 'tags' ? JSON.stringify(left) === JSON.stringify(right) : Object.is(left, right) ), },)
EditSession 不解析 patient.address.city 这样的字符串路径。复杂对象可以拆成页面需要的扁平草稿,或者由业务提供明确的读写函数。
一次修改多个字段
药品选择通常会同时回填编码、名称、规格、单位和价格。使用 setValues() 后,会话只向外通知一次完整变化:
interface MedicationDraft { drugCode: string drugName: string specification: string unit: string price: number} const session = new EditSession<MedicationDraft>({ drugCode: '', drugName: '', specification: '', unit: '', price: 0,}) session.setValues({ drugCode: 'MED001', drugName: '阿莫西林胶囊', specification: '0.25g × 24粒', unit: '盒', price: 18.5,})
多个不同操作也可以放在同步批次中:
interface Draft { value: string confirmed: boolean} const session = new EditSession<Draft>({ value: '', confirmed: false,}) session.batchUpdate(() => { session.setValue('value', '已复核') session.setValue('confirmed', true) session.markTouched('value')})
批次内的最新值立即可读,最外层批次结束后才通知订阅者。回调必须同步执行,不能返回 Promise 或跨越 await。
dirty 和 touched
interface Draft { name: string} const session = new EditSession<Draft>({ name: '原名称' }) session.setValue('name', '新名称')session.markTouched('name') session.isDirtysession.isFieldDirty('name')session.isTouchedsession.isFieldTouched('name')
- dirty 表示当前字段值与 baseline 不同。
- touched 表示页面确认用户已经操作或离开过该字段。
- 值改回 baseline 后,字段 dirty 自动消失。
- touched 不会因为值改回原值自动消失,可用
markTouched(field, false)、resetField()或reset()清除。
会话不会自行判断“点击”和“失焦”。页面根据输入组件的交互时机显式调用 markTouched()。
恢复和接受修改
| 方法 | 作用 |
|---|---|
resetField(field) |
把一个字段恢复到 baseline,并清除该字段 dirty 和 touched。 |
reset() |
恢复整份 baseline,并清除全部 dirty 和 touched。 |
acceptChanges() |
把当前 draft 设为新 baseline。 |
acceptChanges(value) |
用保存后返回的值同时替换 baseline 和 draft。 |
snapshot() |
返回当前草稿的副本。 |
保存成功后应显式接受服务端返回值:
interface Draft { id: string name: string} const session = new EditSession<Draft>({ id: '', name: '新项目' })const saved = { id: 'P001', name: '新项目' } session.acceptChanges(saved)
临时事务
事务适合“打开一个局部编辑器,确定后一次写回,取消时直接丢弃”的场景:
interface Draft { code: string name: string} const session = new EditSession<Draft>({ code: 'A', name: '原名称' })const transaction = session.beginTransaction() transaction.setValues({ code: 'B', name: '新名称' })transaction.commit()
事务维护独立的 staged draft。事务内通过 transaction.getValue() 可以立即读取暂存值,session.getValue() 和 session.snapshot() 在 commit 前仍返回正式草稿。commit 会一次性写回值、dirty 和 touched,并最多发出一次最终变化;rollback 直接丢弃暂存值,不会通知。
事务有明确边界:
- 不支持嵌套。
- 不能在
batchUpdate()内开启。 - 事务存续期间,正式会话的写方法会拒绝执行;所有暂存修改都应通过 transaction 对象完成。
- 调用方应在同一段同步流程内 commit 或 rollback,不要让事务跨越
await。 - 它是页面内的草稿检查点,不代表数据库事务。
订阅
interface Draft { name: string amount: number} const session = new EditSession<Draft>({ name: '', amount: 0 }) const stopAll = session.subscribe(change => { if (change.valueChanged) { const current = session.snapshot() void current }})const stopName = session.subscribeField('name', value => { void value}) stopName()stopAll()session.dispose()
整体订阅一次接收一批变化及其字段集合,字段订阅只在该字段的值、dirty 或 touched 发生变化时执行。取消函数和 dispose() 都可以重复调用。
与 FormSession 的关系
需要规则校验、服务端错误、首错定位或提交状态时,直接创建 FormSession<T>。它通过 form.edit 暴露同一个编辑会话,也代理了常用的值读写、dirty、touched、批次、事务、恢复和接受修改方法。
详细用法见表单校验与提交。
DataGrid 编辑会话
EditSession<T> 管理一份字段草稿,DataGridEditSession<TRow> 管理表格行的新增、修改和删除。后者直接订阅 GridView 的公开数据变更事件,不会为每个单元格创建一份响应式对象。
import { DataGridEditSession, RenderDataGrid, type GridColumnDef,} from 'ds-ui' interface OrderRow { id: string itemCode: string quantity: number} const columns: GridColumnDef<OrderRow>[] = [ { key: 'itemCode', title: '项目', type: 'text', editable: true }, { key: 'quantity', title: '数量', type: 'number', editable: true },]const rows: OrderRow[] = [ { id: 'O-001', itemCode: 'MED-001', quantity: 1 },]const grid = new RenderDataGrid({ columns, rows, editable: true })const orderGridEdits = new DataGridEditSession(grid) grid.setCellValue(rows[0]!, 'quantity', 2) orderGridEdits.isDirtyorderGridEdits.getRowState(rows[0]!)const changes = orderGridEdits.getChanges()
getChanges() 返回三个集合:
added:当前仍在表格中的新增行及其源数据索引。modified:原有行、当前源数据索引,以及每个变更字段的previousValue和当前value。deleted:被删除的原有行、原始源数据索引,以及删除前已经产生的字段修改。
新增行随后删除不会留下变更;字段改回原值后,对应字段修改会消失。resetRow(row) 只恢复一行,reset() 恢复整个表格,acceptChanges() 把当前行集合和值建立为新 baseline。
行身份
默认身份就是行对象本身,不要求 rowKey。这适合桌面业务组件常用的模式:页面拿到一组业务对象,表格排序、过滤、编辑和删除始终围绕这些对象工作。
const objectRows = [{ id: 'O-001', quantity: 1 }]const objectGrid = new RenderDataGrid({ columns: [{ key: 'quantity', title: '数量', type: 'number' }], rows: objectRows,})const objectIdentityEdits = new DataGridEditSession(objectGrid)
只有业务会用一批新对象整体替换 grid.rows,并且还要保留尚未保存的本地修改时,才启用按 key 对账:
const replacementRows = [{ id: 'O-001', quantity: 1 }]const replacementGrid = new RenderDataGrid({ columns: [{ key: 'quantity', title: '数量', type: 'number' }], rows: replacementRows, rowKey: 'id',})const replacementEdits = new DataGridEditSession(replacementGrid, { rowKey: 'id', preserveChangesOnRowsReplace: true,})
启用后 rowKey 必须稳定且唯一。新的数据集合到达时,会话把本地修改重放到同 key 的新行对象上。未启用时,替换 rows 表示业务已经重新绑定数据,新集合直接成为 baseline,旧变更被清空。
数据写入边界
编辑会话只跟踪经过 GridView 数据 API 的变化:
- 单元格写入使用
setCellValue()。 - 行结构变化使用
addRows()、insertRowsBefore()、insertRowsAfter()和removeRows()。 - 用户在单元格编辑器中的提交会自动进入同一条变更链路。
直接执行 row.quantity = 2 再调用 refreshRow(row) 只是在通知表格重绘,会话无法知道旧值。业务字段联动也应逐项调用 setCellValue(),这样保存变更集和撤销都能覆盖主字段与联动字段。
baseline 默认只保存字段旧值的引用。字段值是可变数组或对象时,可通过 cloneValue 和 equals 提供业务自己的复制与比较语义。
与 FormSession 合并
页面同时有普通字段和明细表格时,用 bindDataGridToForm() 把表格作为一个复合编辑、校验参与者接入:
import { DataGridEditSession, FormSession, RenderDataGrid, bindDataGridToForm,} from 'ds-ui' const detailGrid = new RenderDataGrid({ columns: [{ key: 'quantity', title: '数量', type: 'number' }], rows: [{ quantity: 1 }],})const detailForm = new FormSession({ remark: '' })const detailGridEdits = new DataGridEditSession(detailGrid)const unbindDetailGrid = bindDataGridToForm(detailForm, detailGrid, { editSession: detailGridEdits,})
接入后:
form.isDirty和form.isTouched同时包含普通字段与表格。form.reset()会恢复表单字段和表格行变更。form.acceptChanges()会同时接受两部分的新 baseline。form.validate()和form.runSubmit()会先收尾活动单元格,再执行 Grid 全表校验;失败时form.focusFirstError()可定位首个表格错误。
Grid 仍保留逐单元格错误和定位,FormSession 只保存一条汇总问题,不复制每个单元格错误。绑定不拥有 form、grid 或 edit session,页面释放时应先取消绑定,再分别释放会话和组件。
生命周期
会话应由当前页面、弹窗或工作区文档持有:
interface Draft { name: string} const session = new FormSession<Draft>({ name: '' }) class PageOwner { dispose(): void { session.dispose() }}
不要把页面草稿会话放进应用级单例。会话释放后会清理订阅;继续写入、开启事务或新增订阅会抛出错误,也不应依赖释放后的只读快照。