编写详情组件
本节示例代码可参看
oak-general-business/src/components/system/detail,其父组件是oak-general-business/src/components/system/panel
现在我们需要做一个用于查看 System 信息的详情组件。这里要先说明一个真实的公共组件写法:system/detail 并不是自己单独负责首轮取数,而是复用 system/panel 已经建立好的结点和 projection。
也就是说:
system/panel负责声明entity: 'system'、完整projection和根路径;system/detail通过复用同一条oakPath,在这个结点上继续做展示和提交入口;system/upsert也会挂到同一条路径上,负责编辑。
这个模式在 Oak 组件里很常见:一个父容器组件负责稳定取数,多个子组件共享同一条对象上下文。
逻辑层(index.ts)
system/detail/index.ts 的真实代码大致如下:
export default OakComponent({
isList: false,
entity: 'system',
formData({ data }) {
return {
...data,
oakExecutable: this.tryExecute(),
};
},
});
这里有两个关键信息:
- 这是一个单行组件,所以
isList为false; - 它没有自己声明
projection,因为当前组件通常挂在system/panel的同一路径上,父组件已经把需要的字段取好了。
同时它在 formData 中额外返回了:
oakExecutable: this.tryExecute()
这也是 Oak 项目里非常常见的写法。它表示:
- 当前结点上如果存在待提交修改;
- 并且这些修改通过了 checker / 权限检查;
- 那么渲染层就可以据此决定“确认按钮是否可点”。
这也再次说明:oakExecutable 并不是所有组件自动拥有的内置数据项,很多时候是组件作者自己在 formData 中计算并返回的。
渲染层(web.pc.tsx)
在 system/detail/web.pc.tsx 中,组件一方面展示详情,一方面提供一个“打开编辑弹窗并提交”的入口。核心代码大致如下:
export default function Render(props) {
const {
oakId,
name,
description,
oakFullpath,
oakExecutable,
oakExecuting,
} = props.data;
const { t, execute, clean } = props.methods;
return (
<>
<Modal
open={open}
onCancel={() => {
clean();
setOpen(false);
}}
footer={
<Space>
<Button
onClick={() => {
clean();
setOpen(false);
}}
disabled={oakExecuting}
>
{t('common::action.cancel')}
</Button>
<Button
type="primary"
onClick={async () => {
await execute();
setOpen(false);
}}
disabled={oakExecutable !== true || oakExecuting}
>
{t('common::action.confirm')}
</Button>
</Space>
}
>
<SystemUpsert oakId={oakId} oakPath={oakFullpath} />
</Modal>
...
</>
);
}
当前 Oak 编译器会根据同目录 index.ts 中的 entity、isList、properties、formData 和 methods 为传统 TSX render 注入 props.data / props.methods 的精确类型。因此这里不再手写 WebComponentProps,也不应保留仅为旧签名服务的 WebComponentProps、EntityDict 类型导入。业务输入仍必须在 index.ts 的 properties 中声明,不能因为 render 能取到某个字段就省略组件契约。
这里最值得学的不是具体 UI,而是这三个运行时动作:
clean():取消时清理当前路径上的未提交修改;execute():提交当前路径上的修改;<SystemUpsert oakId={oakId} oakPath={oakFullpath} />:把编辑组件挂到和详情组件相同的结点上。
也就是说,在这个例子里:
- 详情组件负责打开弹窗、显示按钮状态、发起提交;
- 更新组件负责写入待提交的数据;
- 两者通过相同的
oakPath共享同一个对象结点。
这个组件是如何被使用的
真正负责首轮取数的是 system/panel。在它的 web.pc.tsx 中,可以看到类似下面的写法:
<SystemDetail
oakId={id}
oakPath={oakFullpath}
/>
这里:
oakId指向当前System的主键;oakPath复用system/panel已建立好的那条路径。
这也是为什么 system/detail 自己可以不再声明 projection。
这个例子应该记住什么
- 详情组件不一定要自己负责 projection,完全可以复用父组件的对象结点。
- 详情组件在 Oak 中经常不只是“展示”,还会兼任“提交入口”。
- 如果详情组件和更新组件共享同一条
oakPath,就可以把编辑结果直接写到同一个 runningTree 结点上,再由详情组件统一execute()。
这一模式在后台管理类页面里非常常见。下一节的更新组件,就是这个例子里被弹窗打开的 system/upsert。