编写更新组件
在上一节,我们描述了如何编写System的详情组件,接下来我们描述如何编写一个System的更新组件。代码见oak-general-business/src/components/system/upsert目录。
更新组件也是针对对象的单行进行操作,因此,其index.ts和详情组件没有太大区别,如果我们查看代码,会发现在这里没有声明projection。
export default OakComponent({
isList: false,
entity: 'system',
formData({ data }) {
return data || {};
},
});
可以这样做的原因,是我们在detail组件中将引用此upsert组件,并声明它的oakPath和detail的oakFullpath一致。这样两个组件就将共享路径上的结点,可以直接使用detail声明的projection。
更新数据
我们重点来看如何更新数据。在web.tsx文件中,可以看到如下代码:
import React from 'react';
import { Form, Switch, Input } from 'antd';
import { EntityDict } from '../../../oak-app-domain';
import { WebComponentProps } from 'oak-frontend-base';
export default function Render(
props: WebComponentProps<
EntityDict,
'system',
false,
{
name: string;
description: string;
folder: string;
super: boolean;
}
>
) {
const {
name,
description,
folder,
super: super2,
} = props.data;
const { t, update } = props.methods;
return (
<Form
colon={true}
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
>
<Form.Item
label={t('system:attr.name')}
required
>
<Input
onChange={(e) => {
update({
name: e.target.value,
});
}}
value={name}
/>
</Form.Item>
<Form.Item
label={t('system:attr.desc')}
required
>
<Input.TextArea
onChange={(e) => {
update({
description: e.target.value,
});
}}
value={description}
/>
</Form.Item>
<Form.Item
label={t('system:attr.isSuper')}
required
tooltip={t('tips.isSuper')}
>
<Switch
checkedChildren={t('common::yes')}
unCheckedChildren={t('common::no')}
checked={super2}
onChange={(checked) => {
update({
super: checked,
});
}}
/>
</Form.Item>
</Form>
);
}
在这里,我们利用了antd当中的部分输入组件,并在props.methods中引出了一个框架提供的update方法,这个方法可以将更新的属性记录在当前组件的组件树结点之中。要注意的是,此时formData函数所取到的数据项data是应用了相关更新后的数据。
提交更新
通过上面的代码,我们已经可以更新某一对象的数据项了。如何将更新提交呢?可以使用框架提供的execute方法。
在这个例子中,我们并没有把execute交给system/upsert组件来执行,这是因为如果将提交按钮放在此组件上,会降低此组件的复用性(在其它页面或组件上,System的upsert可能只是一个子对象比如Application upsert的一部分,如果您现在还不能理解这个也没有关系,先跳过这一段即可)。
因此我们将提交动作放在system/detail组件的代码中,这部分代码大致如下:
export default function Render(
props: WebComponentProps<
EntityDict,
'platform',
false,
{
name: string;
description: string;
oakId: string;
folder: string;
super: boolean;
}
>
) {
const { oakId, folder, name, description, 'super': isSuper, oakFullpath, oakExecutable, oakExecuting } = props.data;
const { t, execute, clean } = props.methods;
const [open, setOpen] = useState(false);
if (oakFullpath) {
return (
<>
<Modal
open={open}
onCancel={() => {
clean();
setOpen(false);
}}
width={500}
footer={
<Space>
<Button
// type='primary'
onClick={async () => {
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>
}
>
<div className={Styles.upsert}>
<SystemUpsert oakId={oakId} oakPath={oakFullpath} />
</div>
</Modal>
...
<Button
type="primary"
onClick={() => setOpen(true)}
>
{t('common::action.update')}
</Button>
...
</>
);
}
...
}
在上述代码中,当点击“更新”按钮时,就弹出一个Modal对话框,Upsert组件放置在对话框中,并且其oakPath与当前组件保持一致(在宽屏下这是一个常见的设计模式)。当对话框的确定按钮被点击时,则触发execute行为,此时本结点(以及子孙结点)上所有的更新会被执行,因此这是一个异步动作。当执行完成后,Modal才被关闭。

你可能会注意到,在上述例子中,有好几个框架级别的变量被使用,比如:OakExecuting表示是否在更新中,oakExecutable表示更新是否可行。关于系统有哪些变量,可以查阅todo章节。
新建数据
新建数据的逻辑和更新非常相似,需要对对象的属性进行update操作,并执行exeute来提交数据,这也是为什么在Oak框架里我们常常使用Upsert这个名词。有几个需要注意的要点:
- 如果一个组件在创建之时就没有设置oakId,则框架会自动执行create动作,为它创建出一条“新”数据。因此,当在使用Upsert组件来更新数据时,如果确认是更新,则应当等确认了主键之后再渲染该组件。像下面的代码(这里模拟的是在一个Application Upsert组件上,嵌入一个System的Upsert组件去插入或更新它所指向的System行,我们毌须去关注这个需求是不是合理):
<SystemUpsert oakId={application?.systemId} oakPath={`${oakFullpath}.system`} />
这样编写代码是很危险的,除非你确认跑到这里的时候,application一定已经确认取到值了,否则就会出现这样的情况:当SystemUpsert组件开始渲染的时候,application还未取到,所以其oakId就是undefined,组件初始化时,认为是一个create动作,所以新建了一行system数据,而当application数据取到后,再用其systemId去置oakId,此时系统会认为这是异常情况而报错。
正确的写法应当是:
{!!application && <SystemUpsert oakId={application.systemId} oakPath={`${oakFullpath}.system`} />}
这样系统就会正确的根据application.systemId是否有值,正确的决定相应的System应当是create还是update。
- 要判断当前组件是update还是create,有两种方法:
- 在OakComponent中,调用*this.isCreation()*方法
- 如果一行数据是create,则在formData中取到的data,其$$createAt$$值为1
- 一个create页面,在提交之后不会再自动进行create,如果您需要这个组件进行连续的create,需要像下面这样,在execute之后显式调用this.create:
...
await this.execute();
await this.create({
...
});
...