编写更新组件

在上一节,我们描述了如何编写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才被关闭。

system upsert

你可能会注意到,在上述例子中,有好几个框架级别的变量被使用,比如:OakExecuting表示是否在更新中,oakExecutable表示更新是否可行。关于系统有哪些变量,可以查阅todo章节。

新建数据

新建数据的逻辑和更新非常相似,需要对对象的属性进行update操作,并执行exeute来提交数据,这也是为什么在Oak框架里我们常常使用Upsert这个名词。有几个需要注意的要点:

  1. 如果一个组件在创建之时就没有设置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。

  1. 要判断当前组件是update还是create,有两种方法:
    • 在OakComponent中,调用*this.isCreation()*方法
    • 如果一行数据是create,则在formData中取到的data,其$$createAt$$值为1
  2. 一个create页面,在提交之后不会再自动进行create,如果您需要这个组件进行连续的create,需要像下面这样,在execute之后显式调用this.create
    ...
    await this.execute();
    await this.create({
        ...
    });
    ...