自定义组件之手动渲染弹出层(v4)

阅读之前

你应该:

弹出层组件

我们内置了两个弹出层组件,弹窗(Dialog)抽屉(Drawer),以下所有内容全部围绕弹窗(Dialog)进行描述,抽屉相关内容与弹窗完全一致。

下面这个对照表格可以帮助你区分两个弹出层组件的异同:

弹出层相关功能 弹窗(Dialog) 抽屉(Drawer)
API方法 Dialog#create Drawer#create
内置组件 DialogWidget DrawerWidget
内置插槽 header, default, footer header, default, footer

渲染弹出层的方式

我们提供了三种渲染弹出层组件的方式,根据不同的情况使用不同的方式可以让实现变得更简单。

  • 使用Dialog#createAPI方法创建弹窗:一般用于简单场景,动作区无法进行自定义。
  • 使用DSL渲染能力创建弹窗
    • 调用ActionWidget#click方法打开弹窗(适用于自动渲染场景)
    • 使用createWidget创建DialogWidget并打开弹窗(适用于手动渲染场景)
  • 使用第三方组件创建弹窗:一般用于弹窗需要自定义的场景中,性能最优,可用于任何场景。

使用Dialog#createAPI方法创建弹窗

以下是一个自定义组件的完整示例,其使用ViewCache#compule方法获取视图。

view.ts
export const template = `<view>
    <field data="id" invisible="true" />
    <field data="code" label="编码" />
    <field data="name" label="名称" />
</view>`;
ManualDemoWidget.ts
import {
  BaseElementWidget,
  createRuntimeContextForWidget,
  Dialog,
  FormWidget,
  MessageHub,
  RuntimeView,
  SPI,
  ViewCache,
  ViewType,
  Widget
} from '@kunlun/dependencies';
import ManualDemo from './ManualDemo.vue';
import { template } from './view';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ManualDemo' }))
export class ManualDemoWidget extends BaseElementWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(ManualDemo);
    return this;
  }

  @Widget.Method()
  public async openPopup() {
    // 获取运行时视图
    const view = await this.fetchViewByCompile();
    if (!view) {
      console.error('Invalid view');
      return;
    }

    // 创建运行时上下文
    const runtimeContext = createRuntimeContextForWidget(view);
    const runtimeContextHandle = runtimeContext.handle;

    // 获取初始化数据
    const formData = await runtimeContext.getInitialValue();

    // 创建弹窗
    const dialogWidget = Dialog.create();

    // 设置弹窗属性
    dialogWidget.setTitle('这是一个演示弹窗');

    // 创建所需组件
    dialogWidget.createWidget(FormWidget, undefined, {
      metadataHandle: runtimeContextHandle,
      rootHandle: runtimeContextHandle,
      dataSource: formData,
      activeRecords: formData,
      template: runtimeContext.viewTemplate,
      inline: true
    });

    dialogWidget.on('ok', () => {
      MessageHub.info('click ok');

      // 关闭弹窗
      dialogWidget.onVisibleChange(false);
    });

    dialogWidget.on('cancel', () => {
      MessageHub.info('click cancel');

      // 关闭弹窗
      dialogWidget.onVisibleChange(false);
    });

    // 打开弹窗
    dialogWidget.onVisibleChange(true);
  }

  public async fetchViewByCompile(): Promise<RuntimeView | undefined> {
    // 模型编码
    const model = ${model};

    // 通过编译获取视图(非完整视图数据)
    const view = await ViewCache.compile(model, '$$popup_demo', template);
    if (!view) {
      return;
    }
    // 补充视图类型
    view.type = ViewType.Form;

    return view;
  }
}
ManualDemo.vue
<template>
  <div class="manual-demo">
    <oio-button @click="openPopup">打开弹窗</oio-button>
  </div>
</template>
<script lang="ts">
import { OioButton } from '@kunlun/vue-ui-antd';
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'ManualDemo',
  components: {
    OioButton
  },
  props: {
    openPopup: {
      type: Function
    }
  }
});
</script>
视图DSL
<view type="FORM">
    <template slot="form">
        <field data="id" invisible="true" />
        <field data="code" />
        <field data="name" />
        <element widget="ManualDemo" />
    </template>
</view>

关键点详解

......

使用DSL渲染能力创建弹窗(调用ActionWidget#click方法)

以下是一个自定义组件的完整示例,视图被定义在DSL模板中。

ManualDemoWidget.ts
import { ActionWidget, BaseElementWidget, DslDefinitionWidget, SPI, Widget } from '@kunlun/dependencies';
import ManualDemo from './ManualDemo.vue';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ManualDemo' }))
export class ManualDemoWidget extends BaseElementWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(ManualDemo);
    return this;
  }

  @Widget.Method()
  public async openPopup() {
    const actionWidget = this.getChildren().find((v) => {
      if (v instanceof DslDefinitionWidget) {
        const slotName = v.getSlotName();
        if (slotName === 'popupAction' && v instanceof ActionWidget) {
          return v;
        }
      }
      return false;
    }) as ActionWidget;

    if (actionWidget) {
      await actionWidget.click();
      actionWidget.forceUpdate();
    }
  }
}
ManualDemo.vue
<template>
  <div class="manual-demo">
    <oio-button @click="openPopup">打开弹窗</oio-button>
    <div style="display: none">
      <slot name="popupAction" />
    </div>
  </div>
</template>
<script lang="ts">
import { OioButton } from '@kunlun/vue-ui-antd';
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'ManualDemo',
  components: {
    OioButton
  },
  props: {
    openPopup: {
      type: Function
    }
  }
});
</script>
视图DSL
<view type="FORM">
    <template slot="form">
        <field data="id" invisible="true" />
        <field data="code" />
        <field data="name" />
        <element widget="ManualDemo">
            <template slot="popupAction">
                <action name="$$popup_action_demo" actionType="VIEW" contextType="CONTEXT_FREE" target="DIALOG" resModel="${model}">
                    <template slot="default" title="这是一个演示弹窗">
                        <view type="FORM">
                            <template slot="actionBar">
                                <action name="$$internal_DialogCancel" type="default" />
                                <action name="create" />
                            </template>
                            <template slot="form">
                                <field data="id" invisible="true" />
                                <field data="code" />
                                <field data="name" />
                            </template>
                        </view>
                    </template>
                </action>
            </template>
        </element>
    </template>
</view>

关键点详解

......

使用DSL渲染能力创建弹窗(创建DialogWidget组件)

以下是一个自定义组件的完整示例,其使用ViewCache#compule方法获取视图,该视图对应的是弹窗组件的渲染模板。

view.ts
export const template = `<view title="这是一个演示弹窗">
    <template slot="default">
        <view type="FORM">
            <element widget="form">
                <field data="id" invisible="true" />
                <field data="code" />
                <field data="name" />
            </element>
        </view>
    </template>
    <template slot="footer">
        <element widget="actionBar">
            <action name="$$internal_DialogCancel" type="default" />
            <action name="create" />
        </element>
    </template>
</view>`;
ManualDemoWidget.ts
import {
  BaseElementWidget,
  createRuntimeContextForWidget,
  DialogWidget,
  RuntimeView,
  SPI,
  ViewCache,
  ViewType,
  Widget
} from '@kunlun/dependencies';
import ManualDemo from './ManualDemo.vue';
import { template } from './view';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ManualDemo' }))
export class ManualDemoWidget extends BaseElementWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(ManualDemo);
    return this;
  }

  @Widget.Method()
  public async openPopup() {
    // 获取运行时视图
    const view = await this.fetchViewByCompile();
    if (!view) {
      console.error('Invalid view');
      return;
    }

    // 创建运行时上下文
    const runtimeContext = createRuntimeContextForWidget(view);
    const runtimeContextHandle = runtimeContext.handle;

    // 获取初始化数据
    const formData = await runtimeContext.getInitialValue();

    const dialogWidget = this.createWidget(new DialogWidget(), 'popupWidget', {
      metadataHandle: runtimeContextHandle,
      rootHandle: runtimeContextHandle,
      dataSource: formData,
      activeRecords: formData,
      template: runtimeContext.viewTemplate,
      inline: true,
      mountedVisible: true
    });

    dialogWidget.on('ok', async () => {
      dialogWidget.onVisibleChange(false);
    });
    dialogWidget.on('cancel', () => {
      dialogWidget.onVisibleChange(false);
    });

    this.forceUpdate();
  }

  public async fetchViewByCompile(): Promise<RuntimeView | undefined> {
    // 模型编码
    const model = ${model};

    // 通过编译获取视图(非完整视图数据)
    const view = await ViewCache.compile(model, '$$popup_demo', template);
    if (!view) {
      return;
    }
    // 补充视图类型
    view.type = ViewType.Form;

    return view;
  }
}
ManualDemo.vue
<template>
  <div class="manual-demo">
    <oio-button @click="openPopup">打开弹窗</oio-button>
    <div style="display: none">
      <slot name="popupWidget" />
    </div>
  </div>
</template>
<script lang="ts">
import { OioButton } from '@kunlun/vue-ui-antd';
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'ManualDemo',
  components: {
    OioButton
  },
  props: {
    openPopup: {
      type: Function
    }
  }
});
</script>
视图DSL
<view type="FORM">
    <template slot="form">
        <field data="id" invisible="true" />
        <field data="code" />
        <field data="name" />
        <element widget="ManualDemo" />
    </template>
</view>

关键点详解

......

使用第三方组件创建弹窗

以下是一个自定义组件的完整示例,其使用ViewCache#compule方法获取视图。

view.ts
export const template = `<view>
    <field data="id" invisible="true" />
    <field data="code" label="编码" />
    <field data="name" label="名称" />
</view>`;
ManualDemoWidget.ts
import {
  BaseElementWidget,
  createRuntimeContextForWidget,
  FormWidget,
  MessageHub,
  RuntimeView,
  SPI,
  ViewCache,
  ViewType,
  Widget
} from '@kunlun/dependencies';
import ManualDemo from './ManualDemo.vue';
import { template } from './view';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ManualDemo' }))
export class ManualDemoWidget extends BaseElementWidget {
  private formWidget: FormWidget | undefined;

  public initialize(props) {
    super.initialize(props);
    this.setComponent(ManualDemo);
    return this;
  }

  @Widget.Method()
  public async openPopup(): Promise<void> {
    // 获取运行时视图
    const view = await this.fetchViewByCompile();
    if (!view) {
      console.error('Invalid view');
      return;
    }

    // 销毁旧组件
    this.formWidget?.dispose();
    this.formWidget = undefined;

    // 创建运行时上下文
    const runtimeContext = createRuntimeContextForWidget(view);
    const runtimeContextHandle = runtimeContext.handle;

    // 获取初始化数据
    const formData = await runtimeContext.getInitialValue();

    // 创建所需组件
    this.formWidget = this.createWidget(FormWidget, 'formWidget', {
      metadataHandle: runtimeContextHandle,
      rootHandle: runtimeContextHandle,
      dataSource: formData,
      activeRecords: formData,
      template: runtimeContext.viewTemplate,
      inline: true
    });
    this.forceUpdate();
  }

  @Widget.Method()
  public onEnter() {
    MessageHub.info('click ok');
    return true;
  }

  @Widget.Method()
  public onCancel() {
    MessageHub.info('click cancel');
    return true;
  }

  public async fetchViewByCompile(): Promise<RuntimeView | undefined> {
    // 模型编码
    const model = ${model};

    // 通过编译获取视图(非完整视图数据)
    const view = await ViewCache.compile(model, '$$popup_demo', template);
    if (!view) {
      return;
    }
    // 补充视图类型
    view.type = ViewType.Form;

    return view;
  }
}
ManualDemo.vue
<template>
  <div class="manual-demo">
    <oio-button @click="openModal">打开弹窗</oio-button>
    <oio-modal v-model:visible="visible" title="这是一个演示弹窗" :enter-callback="onEnter" :cancel-callback="onCancel">
      <slot name="formWidget" />
    </oio-modal>
  </div>
</template>
<script lang="ts">
import { ManualWidget } from '@kunlun/dependencies';
import { OioButton, OioModal } from '@kunlun/vue-ui-antd';
import { defineComponent, nextTick, ref } from 'vue';

export default defineComponent({
  name: 'ManualDemo',
  mixins: [ManualWidget],
  components: {
    OioButton,
    OioModal
  },
  props: {
    openPopup: {
      type: Function
    },
    onEnter: {
      type: Function
    },
    onCancel: {
      type: Function
    }
  },
  setup(props) {
    const visible = ref(false);

    const openModal = async () => {
      await props.openPopup?.();
      nextTick(() => {
        visible.value = true;
      });
    };

    return {
      visible,

      openModal
    };
  }
});
</script>
视图DSL
<view type="FORM">
    <template slot="form">
        <field data="id" invisible="true" />
        <field data="code" />
        <field data="name" />
        <element widget="ManualDemo" />
    </template>
</view>

关键点详解

......

Oinone社区 作者:nation原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/42.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(0)
nation的头像nation数式员工
上一篇 2023年6月20日 pm4:07
下一篇 2023年11月2日 pm1:58

相关推荐

  • 多对多的表格 点击添加按钮打开一个表单弹窗

    多对多的表格 点击添加按钮打开一个表单弹窗 默认情况下,多对多的表格上方的添加按钮点击后,打开的是个表格 ,如果您期望点击添加按钮打开的是个表单页面,那么可以按照下方的操作来 1: 先从界面设计器拖一个多对多的字段进来 2: 将该字段切换成表格,并拖入一些字段到表格上 3: 选中添加按钮,将其隐藏 4: 从组件区域的动作分组中拖一个跳转动作,并且进行如下的配置 5: 属性填写好后进行保存,然后在设计弹窗 6: 拖入对应的字段到弹窗中, 当弹窗界面设计完成后,再把保存的按钮拖入进来 这样多对多的添加弹窗就变成了表单

    前端 2023年11月1日
    2.8K00
  • 在前端视图添加自定义的区域块

    添加自定义区域块 平台提供了一系列默认的视图布局,可以帮助开发人员快速构建出复杂的企业应用系统。当然,我们可以使用自定义区域块来扩展表格、表单、画廊、树形等视图。 自定义区域块概述 平台视图布局都是通过XML配置实现的。在视图布局中,我们可以使用一些特定的元素标签来构建视图的表头、表单、搜索区域等部分。而自定义区域块,就是这些元素标签之外的部分。我们可以通过在视图布局的XML中添加自定义区域块,来扩展页面功能。 视图类型及相关元素 视图类型分为表格(TABLE)、表单(FORM)、画廊(GALLERY)、树形(TREE)等。不同类型的视图布局,包含的元素也有所不同。 下面是几种视图类型及其对应的元素: 表格:搜索区域、表格主体,其中表格主体包含了表格上面的动作、表格区域等部分。 表单:表单区域,包含了表单动作、表单区域等部分。 画廊:动作、卡片详细信息。 在表格页面添加自定义区域块 以下是一个示例,演示如何在表格页面顶部添加自定义区域块。 1. 修改视图布局XML 首先,我们需要修改表格视图的XML布局,添加自定义区域块元素标签。 <view type="TABLE"> <!– 这是搜索区域 –> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field" /> </view> </pack> <!– 这是表格主体 –> <pack widget="group" slot="tableGroup"> <!– 在这里添加自定义区域块元素标签 –> <element widget="MyCustomElement"></element> <!– 这是表格上面的动作 –> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <!– 这是表格区域 –> <element widget="table" slot="table" slotSupport="field"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" slotSupport="action" /> </element> </pack> </view> 在上述代码中,我们添加了一个名为MyCustomElement的元素标签。这将作为我们自定义区域块的容器。 2. 创建自定义Vue组件 接下来,我们需要创建一个Vue组件,并将其指定为自定义元素标签MyCustomElement的模板。 <template> <div> <!– 在这里编写自定义区域块的内容 –> <p>Hello, world!</p> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ components: { }, props: [], setup(props) { return {}; } }); </script> 在上述代码中,我们定义了一个非常简单的Vue组件,它在页面上显示一个“Hello, world!”的文本信息。 3. 创建自定义Element Widget 为了使自定义Vue组件与XML布局文件关联起来,我们需要创建一个对应的Element Widget。 import { BaseElementWidget, SPI, BaseElementViewWidget, Widget, ViewMode, FormWidget, BaseElementWidgetProps } from '@kunlun/dependencies'; import MyCustomElement from './MyCustomElement.vue'; @SPI.ClassFactory(BaseElementWidget.Token({ widget: 'MyCustomElementWidget' })) export class MyCustomElementWidget extends BaseElementWidget { public initialize(props: BaseElementWidgetProps): this { super.initialize(props) this.setComponent(MyCustomElement) return this } } 在上述代码中,我们继承了BaseElementWidget类,并在其中指定了Vue组件MyCustomElement。这样,XML布局文件中的元素标签就能够正确地与Vue组件关联起来。 4. 注册视图布局 最后,我们需要将上述代码配置注册。具体而言,我们需要调用registerLayout方法来将XML布局文件、模块名和视图类型进行关联。 import { registerLayout, ViewType } from '@kunlun/dependencies'; registerLayout( `<view type="TABLE"> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field" />…

    2023年11月1日
    2.8K00
  • 【路由】浏览器地址栏url参数介绍

    介绍 浏览器地址栏url为路由类型的视图动作(viewAction)的访问url 详情页示例url https://one.oinone.top/page;module=resource;viewType=DETAIL;model=resource.ResourceDistrict;action=redirectDetailPage;scene=redirectDetailPage;target=ROUTER;menu=%7B%22selectedKeys%22:%5B%22%E5%8C%BA%22%5D,%22openKeys%22:%5B%22%E5%9C%B0%E5%9D%80%E5%BA%93%22,%22%E5%9C%B0%E5%8C%BA%22%5D%7D;id=575733837679260950;path=%2Fresource%2F%E5%8C%BA%2FACTION%23resource.ResourceDistrict%23redirectDetailPage 通过调试工具查看解析后的信息 参数介绍 module 动作所在模块名称 viewType 视图类型 model 动作所在模型的编码 action 动作名称 target 动作打开方式,ROUTER为当前路由打开,OPEN_WINDOW为新窗口打开 menu 【选填】菜单栏控制参数,该参数不影响页面的业务逻辑,仅影响菜单栏展开哪些菜单项(通过openKeys属性),选中哪些菜单项(通过selectedKeys属性)),该参数经过JSON.stringify(menu)方式处理过 # 示例参数 { "selectedKeys": ["区"], "openKeys": ["地址库", "地区"] } id 【选填】详情、编辑等单行数据页面的数据id searchBody 列表页搜索区域的搜索条件,该参数在前端经过encodeURIComponent(JSON.stringify(searchBody))方式处理过 # 示例参数 { "code": "11" } searchConditions 列表页高级搜索条件,用于处理searchBody之外的复杂搜索条件,日常开发中无需关心该参数encodeURIComponent(JSON.stringify(searchConditions))方式处理过 # 示例参数 [ { "leftValue":["sourceType"], "operator":"==", "right":"GD" } ] context 上下文参数,该参数经过JSON.stringify(menu)方式处理过 列表页的此参数会填充到搜索区域的字段中作为默认的查询条件,详情 详情页和表单页此参数会作为页面加载函数的入参 # 示例参数 { “cateId”: “61723712399821” } path 权限验证路径,父页面编译的时候自动加上该参数,在父页面点击当前动作的时候会自动拼该参数 scene 【选填】动作场景值 代码中如何获取 这里介绍在组件内如何获取 import { BaseElementWidget } from ‘@kunlun/dependencies’; export class DemoElementWidget extends BaseElementWidget { protected test() { const { module, model, action } = this.urlParameters; } } 推荐阅读相关文档 上下文在字段和动作中的应用 如何实现页面间的跳转

    2024年8月19日
    3.9K00
  • 提交数据动作如何把弹窗内的数据完全返回

    场景介绍 表格行的操作列有一个打开弹窗的动作 弹窗内为表格行数据的表单,表单内有一个o2m字段,展示了除关联关系字段(大部分场景为id)外的其他字段 弹窗底部动作区域有一个提交数据的客户端动作,该动作会将弹窗内表单的数据回写到表格行的数据上 场景截图 问题现象 点击提交数据的客户端动作,会将数据回写到表格行的数据上,但是表格行拿到的o2m字段的数据只有id字段,o2m字段的编码和名称并不会提交到表格行,o2m字段默认提交规则是只会提交关联关系字段(大部分场景为id)的数据 解决方案 该方案适合5.x版本,4.x版本新建的字段组件无法自动生成属性面板,建议拿下面的组件在SPI加上model和name注册实现 自定义o2m字段所在的组件,m2m字段或者其他类似问题的组件也是如下面组件一样覆写submit方法即可 import { BaseFieldWidget, FormO2MTableFieldWidget, ModelFieldType, SPI, SubmitRelationValue, SubmitValue, ViewType } from '@kunlun/dependencies'; @SPI.ClassFactory( BaseFieldWidget.Token({ viewType: ViewType.Form, ttype: ModelFieldType.OneToMany, widget: 'SubmitAllDataO2MTable' }) ) export class MyFormO2MTableFieldWidget extends FormO2MTableFieldWidget { public async submit(submitValue: SubmitValue): Promise<Record<string, unknown> | SubmitRelationValue | undefined> { return { [this.itemName]: this.value }; } } 在界面设计器的组件功能内新增对应业务类型的自定义组件,组件内的元件api名称填写上面组件的名称SubmitAllDataO2MTable 在界面设计器的设计页面切换o2m字段组件为SubmitAllDataO2MTable 参考文档 自定义组件文档

    2024年6月28日
    1.9K00
  • oio-spin 加载中

    用于页面和区块的加载中状态。 何时使用 页面局部处于等待异步数据或正在渲染过程时,合适的加载动效会有效缓解用户的焦虑。 API 参数 说明 类型 默认值 版本 delay 延迟显示加载效果的时间(防止闪烁) number (毫秒) – loading 是否为加载中状态 boolean true wrapperClassName 包装器的类属性 string –

    2023年12月18日
    1.3K00

Leave a Reply

登录后才能评论