自定义组件之自动渲染(组件插槽的使用)(v4)

阅读之前

你应该:

自定义组件简介

前面我们简单介绍过一个简单的自定义组件该如何被定义,并应用于页面中。这篇文章将对自定义组件进行详细介绍。

自定义一个带有具名插槽的容器组件(一般用于Object数据类型的视图中)

使用BasePackWidget组件进行注册,最终体现在DSL模板中为<pack widget="SlotDemo">。

SlotDemoWidget.ts
import { BasePackWidget, SPI } from '@kunlun/dependencies';
import SlotDemo from './SlotDemo.vue';

@SPI.ClassFactory(BasePackWidget.Token({ widget: 'SlotDemo' }))
export class SlotDemoWidget extends BasePackWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(SlotDemo);
    return this;
  }
}

定义一个Vue组件,包含三个插槽,分别是default不具名插槽、title具名插槽、footer具名插槽。

SlotDemo.vue
<template>
  <div class="slot-demo-wrapper" v-show="!invisible">
    <div class="title">
      <slot name="title" />
    </div>
    <div class="content">
      <slot />
    </div>
    <div class="footer">
      <slot name="footer" />
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'SlotDemo',
  props: {
    invisible: {
      type: Boolean,
      default: undefined
    }
  }
});
</script>

在一个表单(FORM)的DSL模板中,我们可以这样使用这三个插槽:

<view type="FORM">
    <pack widget="SlotDemo">
        <template slot="default">
            <field data="id" />
        </template>
        <template slot="title">
            <field data="name" />
        </template>
        <template slot="footer">
            <field data="isEnabled" />
        </template>
    </pack>
</view>

这样定义的一个组件插槽和DSL模板就进行了渲染上的结合。

针对不具名插槽的特性,我们可以缺省slot="default"标签,缺少template标签包裹的所有元素都将被收集到default不具名插槽中进行渲染,则上述DSL模板可以改为:

<view type="FORM">
    <pack widget="SlotDemo">
        <field data="id" />
        <template slot="title">
            <field data="name" />
        </template>
        <template slot="footer">
            <field data="isEnabled" />
        </template>
    </pack>
</view>

自定义一个数组渲染组件(一般用于List数据类型的视图中)

由于表格无法体现DSL模板渲染的相关能力,因此我们以画廊视图(GALLERY)进行演示。

先定义一个数组每一项的数据结构:

typing.ts
export interface ListItem {
  key: string;
  data: Record<string, unknown>;
  index: number;
}
ListRenderDemoWidget.ts
import { BaseElementListViewWidget, BaseElementWidget, SPI } from '@kunlun/dependencies';
import ListRenderDemo from './ListRenderDemo.vue';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ListRenderDemo' }))
export class ListRenderDemoWidget extends BaseElementListViewWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(ListRenderDemo);
    return this;
  }
}
ListItemDemoWidget.ts
import { ActiveRecord, ActiveRecordsOperator, BaseElementWidget, SPI, Widget } from '@kunlun/dependencies';
import ListItemDemo from './ListItemDemo.vue';
import { ListItem } from './typing';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ListItemDemo' }))
export class ListItemDemoWidget extends BaseElementWidget {
  @Widget.Reactive()
  protected get formData(): ActiveRecord {
    return this.activeRecords?.[0] || {};
  }

  @Widget.Reactive()
  protected rowIndex: number | undefined;

  public initialize(props) {
    const slotContext = props.slotContext as ListItem;
    if (slotContext) {
      props.activeRecords = ActiveRecordsOperator.repairRecords(slotContext.data);
      this.rowIndex = slotContext.index;
    }
    super.initialize(props);
    this.setComponent(ListItemDemo);
    return this;
  }
}
ListRenderDemo.vue
<template>
  <div class="list-render-demo-wrapper">
    <div class="list-render-item-wrapper" v-for="item in list" :key="item.key">
      <slot v-bind="item" />
    </div>
  </div>
</template>

<script lang="ts">
import { ActiveRecord, uniqueKeyGenerator } from '@kunlun/dependencies';
import { computed, defineComponent, PropType } from 'vue';
import { ListItem } from './typing';

export default defineComponent({
  name: 'ListRenderDemo',
  props: {
    showDataSource: {
      type: Array as PropType<ActiveRecord[]>
    }
  },
  setup(props) {
    const list = computed<ListItem[]>(() => {
      return (
        props.showDataSource?.map((data, index) => {
          const item: ListItem = {
            key: data.__draftId || uniqueKeyGenerator(),
            data,
            index
          };
          return item;
        }) || []
      );
    });

    return {
      list
    };
  }
});
</script>
ListItemDemo.vue
<template>
  <div class="list-item-demo">
    <div class="title">
      <slot name="title" />
    </div>
    <div class="content">
      <slot />
    </div>
    <div class="footer">
      <slot name="footer" />
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'ListItemDemo'
});
</script>

在一个画廊(GALLERY)的DSL模板中,我们可以这样使用这三个插槽:

<view type="GALLERY">
    <template slot="gallery" widget="ListRenderDemo">
        <element widget="ListItemDemo">
            <template slot="default">
                <field data="id" />
            </template>
            <template slot="title">
                <field data="name" />
            </template>
            <template slot="footer">
                <field data="isEnabled" />
            </template>
        </element>
    </template>
</view>

PS:从默认提供的画廊视图(GALLERY)布局来看,我们可以通过slot="gallery"插槽完全替换下面的子标签,并指定widget="ListRenderDemo"来使用我们的自定义组件。

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

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

Like (0)
oinone's avataroinone
Previous 2023年6月20日 pm4:07
Next 2023年11月2日 pm1:58

相关推荐

  • TS 结合 Vue 实现动态注册和响应式管理

    基础知识 1: 面向对象 面向对象编程是 JavaScript 中一种重要的编程范式,它帮助开发者通过类和对象组织代码,提高代码的复用性。 2: 装饰器 装饰器在 JavaScript 中是用于包装 class 或方法的高阶函数 为了统一术语,下面的内容会把装饰器讲成注解 在 oinone 平台中,无论是字段还是动作,都是通过 ts + vue 来实现的,ts 中是面向对象的写法,所有的属性、方法建议都在对应的 class 中,如果有通用的属性跟方法,可以放在一个公共的 class 中,然后通过继承来实现,这样便于维护。 <!– FormString.vue –> <template> <div> <p>用户名: {{userName}}</p> <button @click="updateUser({name: '王五'})">修改用户名</button> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ props: { userName: { type: String, default: '' }, userInfo: { type: Object, default: () => ({}) }, updateUser: { type: Function, default: () => () => ({}) } } }); </script> import FormString from './FormString.vue' @SPI.ClassFactory( FormFieldWidget.Token({ viewType: [ViewType.Form, ViewType.Search], ttype: ModelFieldType.String }) ) export class FormCustomStringFieldWidget extends FormFieldWidget { public initialize(props) { super.initialize(props); // 调用父类方法,确保继承的属性和方法正常初始化 this.setComponent(FormString); // 注册 Vue 组件,这样该 Widget 就会渲染 FormString 组件 return this; } public otherInfo = { name:'张三' } @Widget.Reactive() public userInfo = { name:'李四' } @Widget.Reactive() public get userName() { return this.userInfo.name } @Widget.Method() public updateUser userName(user) { this.userInfo = user } public updateOtherUser userName(user) { this.otherUser = user } } 这段代码定义了一个 FormCustomStringFieldWidget 类,用于处理表单中 String 类型字段的展示和交互。该类继承自 FormFieldWidget,并使用了多种注解和特性来实现不同功能。下面是对代码的详细讲解。 SPI 讲解 @SPI.ClassFactory() 无论是自定义字段还是动作,或者是自定义 mask、layout,都会用到@SPI.ClassFactory来注册,@SPI.ClassFactory 是一个注解,它标记了该类是通过工厂模式注册的。 在前端中,所有的注解(装饰器)本质上还是高阶函数,下面是一段伪代码。 const SPI = { ClassFactory(token) { return (targetClass) => {…

    2024年9月21日
    2.2K00
  • Class Component(ts)(v4)

    Class Component 一种使用typescript的class声明组件的方式。 IWidget类型声明 IWidget是平台内所有组件的统一接口定义,也是一个组件定义的最小集。 /** * 组件构造器 */ export type WidgetConstructor<Props extends WidgetProps, Widget extends IWidget<Props>> = Constructor<Widget>; /** * 组件属性 */ export interface WidgetProps { [key: string]: unknown; } /** * 组件 */ export interface IWidget<Props extends WidgetProps = WidgetProps> extends DisposableSupported { /** * 获取当前组件响应式对象 * @return this */ getOperator(); /** * 组件初始化 * @param props 属性 */ initialize(props: Props); /** * 创建子组件 * @param constructor 子组件构造器 * @param slotName 插槽名称 * @param props 属性 * @param specifiedIndex 插入/替换指定索引的子组件 */ createWidget<ChildProps extends WidgetProps = WidgetProps>( constructor: WidgetConstructor<ChildProps, IWidget<ChildProps>>, slotName?: string, props?: ChildProps, specifiedIndex?: number ); } Widget Widget是平台实现的类似于Class Component组件抽象基类,定义了包括渲染、生命周期、provider/inject、watch等相关功能。 export abstract class Widget<Props extends WidgetProps = WidgetProps, R = unknown> implements IWidget<Props> { /** * 添加事件监听 * * @param {string} path 监听的路径 * @param {deep?:boolean;immediate?:boolean} options? * * @example * * @Widget.Watch('formData.name', {deep: true, immediate: true}) * private watchName(name: string) { * … todo * } * */ protected static Watch(path: string, options?: { deep?: boolean; immediate?: boolean }); /** * 可以用来处理不同widget之间的通讯,当被订阅的时候,会将默认值发送出去 * * @param {Symbol} name 唯一标示 * @param {unknown} value? 默认值 * * @example *…

    2023年11月1日
    2.0K00
  • 前端密码加密

    在项目开发中,我们可能会遇到自定义登录页,密码需要加密,或者是数据提交的时候,某个数据需要加密,在平台的前端中,提供了默认的全局加密 API 在 oinone 前端工程使用 // pc端工程使用 import { encrypt } from '@kunlun/dependencies'; // 移动端端工程使用 import { encrypt } from '@kunlun/mobile-dependencies'; // 加密后的密码 const password = encrypt('123456'); 其他工程使用 如果是其他工程,前端没有用到 oinone 这一套,比如小程序,或者是其他工程,可以使用下面的代码记得安装 crypto-js import CryptoJS from 'crypto-js'; const key = CryptoJS.enc.Utf8.parse('1234567890abcdefghijklmnopqrstuv'); const iv = CryptoJS.enc.Utf8.parse('1234567890aabbcc'); export const encrypt = (content: string): string => { if (typeof content === 'string' && content) { const encryptedContent = CryptoJS.AES.encrypt(content, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encryptedContent.ciphertext.toString(); } return ''; };

    2025年3月24日
    94400
  • oio-input 输入框

    代码演示 <oio-input v-model:value="value"></oio-input> API Input 参数 说明 类型 默认值 版本 addonAfter 带标签的 input,设置后置标签 string|slot addonBefore 带标签的 input,设置前置标签 string|slot allowClear 可以点击清除图标删除内容 boolean defaultValue 输入框默认内容 string disabled 是否禁用状态,默认为 false boolean false maxlength 最大长度 number prefix 带有前缀图标的 input slot showCount 是否展示字数 boolean false suffix 带有后缀图标的 input slot type 声明 input 类型,同原生 input 标签的 type 属性,见:MDN(请直接使用 <a-textarea /> 代替 type="textarea")。 string text value(v-model:value) 输入框内容 string Input 事件 事件名称 说明 回调参数 update:value 输入框内容变化时的回调 function(e) pressEnter 按下回车的回调 function(e) Input.Search 代码演示 <oio-input-search v-model:value="value"></oio-input-search> Input.Search 事件 事件名称 说明 回调参数 search 点击搜索或按下回车键时的回调 function(value, event) 其余属性和 Input 一致。

    2023年12月18日
    2.8K00
  • 树型表格全量加载数据如何处理

    阅读该文档的前置条件 【界面设计器】树形表格 1.前端自定义表格组件 import { ActiveRecord, BaseElementWidget, Condition, Entity, SPI, TableWidget, ViewType } from '@kunlun/dependencies'; @SPI.ClassFactory( BaseElementWidget.Token({ type: ViewType.Table, widget: ['demo-tree-table'] }) ) export class TreeTableWidget extends TableWidget { // 默认展开所有层级 protected getTreeExpandAll() { return true; } // 关闭懒加载 protected getTreeLazy(): boolean { return false; } public async $$loadTreeNodes(condition?: Condition, currentRow?: ActiveRecord): Promise<Entity[]> { // 树表加载数据的方法,默认首次只查第一层的数据,这里去掉这个查询条件的参数condition,这样就会查所有层级数据 return super.$$loadTreeNodes(undefined, currentRow); } } 2. 注册layout import { registerLayout, ViewType } from '@kunlun/dependencies'; const install = () => { registerLayout( ` <view type="TABLE"> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="demo-tree-table" slot="table"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" slotSupport="action" /> </element> </view> `, { viewType: ViewType.Table, model: "resource.resourceCity", // 变量,需要替换 actionName: "MenuuiMenu6f6005bdddba468bb2fb814a62fa83c6", // 变量,需要替换 } ); }; install();

    2024年8月17日
    2.0K00

Leave a Reply

Please Login to Comment