编写更新组件
上一节的详情组件里,真正负责编辑 System 数据的是 oak-general-business/src/components/system/upsert。这一节就用它来说明 Oak 中单行更新组件的典型写法。
逻辑层(index.ts)
system/upsert/index.ts 的真实代码大致如下:
export default OakComponent({
isList: false,
entity: 'system',
projection: {
id: 1,
name: 1,
config: 1,
description: 1,
oldestVersion: 1,
super: 1,
},
formData({ data }) {
return data || {};
},
});
这里有三个要点:
- 它仍然是单行组件,所以
isList: false; - 它显式声明了
projection,说明这个 Upsert 组件本身可以独立工作,而不完全依赖父组件兜底取数; formData只是把当前行数据原样展开给渲染层。
这点和上一节刚好形成对照:
system/detail倾向于复用父组件已有结点,并额外返回oakExecutable;system/upsert更像一个可独立复用的编辑表单,因此把自己需要的字段写在projection里。
所以在实际项目里,不要硬记“详情一定有 projection、upsert 一定没有 projection”这种结论。正确规则是:谁需要独立承担取数责任,谁就应该声明 projection。
渲染层如何更新数据
在 system/upsert/web.pc.tsx 中,主要工作是把输入控件和 update(...) 绑在一起:
export default function Render(props) {
const {
name,
description,
super: super2,
oldestVersion,
} = props.data;
const { t, update } = props.methods;
return (
<Form>
<Form.Item label={t('system:attr.name')}>
<Input
value={name}
onChange={(e) => {
update({
name: e.target.value,
});
}}
/>
</Form.Item>
<Form.Item label={t('system:attr.description')}>
<Input.TextArea
value={description}
onChange={(e) => {
update({
description: e.target.value,
});
}}
/>
</Form.Item>
<Form.Item label={t('system:attr.oldestVersion')}>
<Input
value={oldestVersion}
onChange={(e) => {
update({
oldestVersion: e.target.value,
});
}}
/>
</Form.Item>
<Form.Item label={t('system:attr.super')}>
<Switch
checked={super2}
onChange={(checked) => {
update({
super: checked,
});
}}
/>
</Form.Item>
</Form>
);
}
这里的 props 类型由 Oak 编译器从 index.ts 自动推导并注入,不需要手写 WebComponentProps。如果表单还需要父组件传入额外业务字段,应把字段加入 index.ts -> properties;编译器会把它们合并进 render 合同。不要只在 TSX 中补一个手写类型,因为那不会建立 Oak 组件的真实运行时属性声明。
这里的 update(...) 并不会立刻向后端提交请求,它做的是:
- 把当前修改记录到 runningTree 对应结点上;
- 让当前路径上的“新值”立即反映到
formData和渲染层中; - 等待之后统一
execute()。
因此,Oak 的 Upsert 组件通常天然适合:
- 表单分块编辑;
- 多个子组件协同编辑同一对象;
- 在确认前统一校验并提交。
提交为什么通常不放在 Upsert 组件里
以 system/upsert 这个例子来说,提交按钮并不在 Upsert 组件内部,而是放在上一节的 system/detail 组件里,由详情组件统一调用:
await execute();
这是一种很值得借鉴的组织方式。因为很多时候:
- Upsert 只是某个详情页里的一个弹窗或一个 tab;
- 页面上可能还有别的子组件也在改同一条数据;
- 提交、取消、按钮可用性判断,往往更适合由父组件统一控制。
换句话说:
- Upsert 组件负责“怎么改”;
- 父组件负责“什么时候提交、什么时候回滚、按钮怎么展示”。
新建数据时要特别注意的地方
Oak 中,单行组件是否处于“创建态”,和 oakId 是否已经稳定,非常相关。
1. 没有 oakId 时,单行组件会进入 create 语义
如果一个单行组件初始化时没有 oakId,框架会把它当成创建流程来处理。此时:
- 可以通过
this.isCreation()判断当前是否是 create; - 也可以在
formData中通过data.$$createAt$$ === 1判断当前行是否是新建态。
2. 不要让 oakId 在组件初始化后“从无到有”
例如:
<SystemUpsert oakId={application?.systemId} oakPath={`${oakFullpath}.system`} />
这种写法就有风险。因为很可能第一次渲染时 application 还没取到,oakId 是 undefined,组件会按 create 初始化;等数据回来后 oakId 又变成了已有主键,运行树会认为这是一种异常状态切换。
更稳妥的写法是:
{!!application && (
<SystemUpsert
oakId={application.systemId}
oakPath={`${oakFullpath}.system`}
/>
)}
也就是:等主键真的确定后,再渲染这个单行组件。
3. 如果要连续创建,需要在提交后显式再 create 一次
Oak 不会在一次创建提交完成后自动帮你进入下一轮创建。如果你需要“连续创建”,要在 execute() 成功后手动再准备下一条:
await this.execute();
this.create({
...
});
这一节最重要的结论
- Upsert 组件的核心职责是调用
update/create把待提交修改写进 runningTree。 - 是否在 Upsert 自己身上声明
projection,取决于它是否需要独立承担取数责任。 - 提交按钮不一定要放在 Upsert 组件里,很多场景下交给父组件统一
execute更合理。 - 单行组件一旦涉及创建流程,
oakId的时序一定要特别小心。