Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

编写更新组件

上一节的详情组件里,真正负责编辑 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 || {};
    },
});

这里有三个要点:

  1. 它仍然是单行组件,所以 isList: false
  2. 它显式声明了 projection,说明这个 Upsert 组件本身可以独立工作,而不完全依赖父组件兜底取数;
  3. 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 还没取到,oakIdundefined,组件会按 create 初始化;等数据回来后 oakId 又变成了已有主键,运行树会认为这是一种异常状态切换。

更稳妥的写法是:

{!!application && (
    <SystemUpsert
        oakId={application.systemId}
        oakPath={`${oakFullpath}.system`}
    />
)}

也就是:等主键真的确定后,再渲染这个单行组件。

3. 如果要连续创建,需要在提交后显式再 create 一次

Oak 不会在一次创建提交完成后自动帮你进入下一轮创建。如果你需要“连续创建”,要在 execute() 成功后手动再准备下一条:

await this.execute();
this.create({
    ...
});

这一节最重要的结论

  1. Upsert 组件的核心职责是调用 update / create 把待提交修改写进 runningTree。
  2. 是否在 Upsert 自己身上声明 projection,取决于它是否需要独立承担取数责任。
  3. 提交按钮不一定要放在 Upsert 组件里,很多场景下交给父组件统一 execute 更合理。
  4. 单行组件一旦涉及创建流程,oakId 的时序一定要特别小心。