我们可能会遇到这些需求,如:页面中的一对多字段不是下拉框,而是另一个模型的表单组;页面中的步骤条表单,每一步的表单都需要界面设计器设计,同时这些表单可能属于不同模型。
这时候我们就可以采取页面嵌套的方式,在当前页面中,动态创建一个界面设计器设计的子页面。以一对多字段,动态创建表单子页面举例,以下是代码实现和原理分析。
代码实现
AddSubformWidget 动态添加表单 ts 组件
import {
ModelFieldType,
ViewType,
SPI,
BaseFieldWidget,
Widget,
FormO2MFieldWidget,
ActiveRecord,
CallChaining,
createRuntimeContextByView,
queryViewDslByModelAndName,
uniqueKeyGenerator
} from '@oinone/kunlun-dependencies';
import { MyMetadataViewWidget } from './MyMetadataViewWidget';
import { watch } from 'vue';
import AddSubform from './AddSubform.vue';
@SPI.ClassFactory(
BaseFieldWidget.Token({
viewType: ViewType.Form,
ttype: ModelFieldType.OneToMany,
widget: 'AddSubform'
})
)
export class AddSubformWidget extends FormO2MFieldWidget {
public initialize(props) {
super.initialize(props);
this.setComponent(AddSubform);
return this;
}
@Widget.Reactive()
public myMetadataViewWidget: MyMetadataViewWidget[] = [];
@Widget.Reactive()
public myMetadataViewWidgetKeys: string[] = [];
@Widget.Reactive()
public myMetadataViewWidgetLength = 0;
// region 子视图配置
public get subviewModel() {
return this.getDsl().subviewModel || 'clm.contractcenter.ContractSignatory';
}
public get subviewName() {
return this.getDsl().subviewName || '签署方_FORM_uiViewa9c114903e104800b15e8f3749656b64';
}
// region 添加子视图块
// 按钮添加点击事件
@Widget.Method()
public async onAddSubviewBlock() {
const resView = await queryViewDslByModelAndName(this.subviewModel, this.subviewName);
this.createDynamicSubviewWidget(resView);
}
// 创建子视图块
public async createDynamicSubviewWidget(view, activeRecord: ActiveRecord = {}) {
if (view) {
// 根据视图构建上下文
const runtimeContext = createRuntimeContextByView(
{
type: ViewType.Form,
model: view.model,
modelName: view.modelDefinition.name,
module: view.modelDefinition.module,
moduleName: view.modelDefinition.moduleName,
name: view.name,
dsl: view.template
},
true,
uniqueKeyGenerator(),
this.currentHandle
);
// 取得上下文唯一标识
const runtimeContextHandle = runtimeContext.handle;
const slotKey = `Form_${uniqueKeyGenerator()}`;
// 创建子视图组件
const widget = this.createWidget(new MyMetadataViewWidget(runtimeContextHandle),
slotKey, // 插槽名
{
metadataHandle: runtimeContextHandle,
rootHandle: runtimeContextHandle,
mountedCallChaining: new CallChaining(),
refreshCallChaining: new CallChaining(),
inline: true,
subIndex: this.myMetadataViewWidgetLength
});
widget.initContext(runtimeContext);
// 初始化数据
widget.setData(activeRecord);
this.myMetadataViewWidgetLength++;
this.myMetadataViewWidget.push(widget);
this.myMetadataViewWidgetKeys.push(slotKey);
return widget;
}
}
// region 删除子视图块
// 删除子视图块
@Widget.Method()
public async onDleteSubviewBlock(index: number) {
if (index > -1) {
const deleteWidget = this.myMetadataViewWidget.splice(index, 1);
const deleteKey = this.myMetadataViewWidgetKeys.splice(index, 1);
this.myMetadataViewWidgetLength--;
}
}
// region 初始化子视图
// mountedProcess 时,数据已经拿到,根据数据创建子视图
protected async mountedProcess() {
const resView = await queryViewDslByModelAndName(this.subviewModel, this.subviewName);
if (this.value?.length) {
this.value?.forEach(async (eachValue) => {
this.createDynamicSubviewWidget(resView, eachValue);
});
} else if (!this.myMetadataViewWidgetLength) {
// 默认给一个子视图
this.createDynamicSubviewWidget(resView);
}
}
// 监听子视图数据变化,并更新父视图数据
public mounted(): void {
super.mounted();
this.mountedCallChaining?.callAfter(() => {
// 监听子视图数据变化
watch(
() => this.myMetadataViewWidget.map((widget) => widget.getFormData()),
async (newValues) => {
this.reloadActiveRecords({
...this.formData,
[this.itemName]: newValues
});
},
{ deep: true }
);
});
}
// 字段提交数据方法,提交所有子视图的数据
public async submit() {
const promises = this.myMetadataViewWidget.map(async (widget) => widget.getSubmitData());
const submitValues = await Promise.all(promises);
return {
[this.itemName]: submitValues.map((submitValue) => submitValue?.records || {})
};
}
}
AddSubform.vue 动态添加表单 vue 组件
<template>
<div class="AddSubviewBlockField">
<div class="subview-block" v-for="(key, index) in myMetadataViewWidgetKeys" :key="key">
<oio-icon @click="onDleteSubviewBlock(index)" icon="oinone-shanchu3" size="24" />
<slot :name="key" />
</div>
<div class="add-icon">
<a-divider />
<oio-icon @click="onAddSubviewBlock" icon="oinone-add-circle" size="24" />
</div>
</div>
</template>
<script lang="ts" setup>
import { OioIcon } from '@oinone/kunlun-dependencies';
import { PropType } from 'vue';
const props = defineProps({
myMetadataViewWidgetLength: {
type: Number
},
myMetadataViewWidgetKeys: {
type: Array as PropType<string[]>,
default: () => []
},
onAddSubviewBlock: {
type: Function as PropType<() => void>,
default: () => {}
},
onDleteSubviewBlock: {
type: Function as PropType<(index: number) => void>,
default: () => {}
},
});
</script>
<style lang="scss" scoped>
.AddSubviewBlockField {
display: flex;
flex-direction: column;
gap: 48px;
> .add-icon {
display: flex;
flex-direction: column;
> .ant-divider-horizontal {
margin: 0;
}
> .oio-icon {
cursor: pointer;
align-self: flex-end;
}
}
> .subview-block {
width: 100%;
> .oio-icon {
cursor: pointer;
}
}
}
</style>
MyMetadataViewWidget 数据隔离组件
import {
ActiveRecord,
ActiveRecords,
CallChaining,
FormWidget,
MetadataViewWidget,
queryDslWidget,
Widget
} from '@oinone/kunlun-dependencies';
/**
* 通过视图 handle 查找表单组件
* @param viewHandle
*/
const queryFormWidgetByViewHandle = (viewHandle: string): FormWidget | null => {
const baseViewWidget = Widget.select(viewHandle);
const formWidget = queryDslWidget(baseViewWidget?.getChildrenInstance(), FormWidget);
if (formWidget) {
return formWidget as unknown as FormWidget;
}
return null;
};
export class MyMetadataViewWidget extends MetadataViewWidget {
@Widget.Provide()
public mountedCallChaining: CallChaining | undefined;
@Widget.Provide()
@Widget.Reactive()
public dataSource: ActiveRecord[] = [];
@Widget.Method()
@Widget.Provide()
public reloadDataSource(records: ActiveRecords | undefined) {
if (Array.isArray(records)) {
this.dataSource = records;
} else {
this.dataSource = [records || {}];
}
}
@Widget.Provide()
@Widget.Reactive()
public activeRecords: ActiveRecord[] = [];
@Widget.Method()
@Widget.Provide()
public reloadActiveRecords(records: ActiveRecords | undefined) {
if (Array.isArray(records)) {
this.activeRecords = records;
} else {
this.activeRecords = [records || {}];
}
}
@Widget.Reactive()
@Widget.Provide()
public rootData: ActiveRecord[] | undefined;
@Widget.Method()
@Widget.Provide()
public reloadRootData(records: ActiveRecords | undefined) {
if (Array.isArray(records)) {
this.rootData = records;
} else {
this.rootData = [records || {}];
}
}
public initialize(props): this {
this.mountedCallChaining = props.mountedCallChaining;
this.subIndex = props.subIndex;
super.initialize(props);
return this;
}
protected mounted() {
this.mountedCallChaining?.syncCall();
}
protected async validator() {
const formWidget = queryFormWidgetByViewHandle(this.currentHandle);
const res = await formWidget?.validator();
return res;
}
public getFormData() {
return this.activeRecords?.[0];
}
public async getSubmitData() {
const formWidget = queryFormWidgetByViewHandle(this.currentHandle);
return await formWidget?.submit();
}
protected getModelFields() {
const formWidget = queryFormWidgetByViewHandle(this.currentHandle);
return formWidget?.rootRuntimeContext.getRequestModelFields();
}
public setData(data: Record<string, unknown>) {
if (data) {
this.reloadDataSource(data);
this.reloadActiveRecords(data);
this.reloadRootData(data);
}
}
/**
* 当前子路径索引
*/
@Widget.Reactive()
protected subIndex: string | number | undefined;
/**
* 上级路径
*/
@Widget.Reactive()
@Widget.Inject('path')
protected parentPath: string | undefined;
/**
* 完整路径
*/
@Widget.Reactive()
@Widget.Provide()
public get path() {
const { parentPath, subIndex } = this;
let path = parentPath || '';
return `${path}.metadata[${subIndex || ''}]`;
}
}
原理分析
vue 插槽渲染
我们先从大家都熟悉的 vue 文件入手
<template>
<div class="AddSubviewBlockField">
<div class="subview-block" v-for="(key, index) in myMetadataViewWidgetKeys" :key="key">
<oio-icon @click="onDleteSubviewBlock(index)" icon="oinone-shanchu3" size="24" />
<slot :name="key" />
</div>
<div class="add-icon">
<a-divider />
<oio-icon @click="onAddSubviewBlock" icon="oinone-add-circle" size="24" />
</div>
</div>
</template>
可以看到模版的内容非常简单,上方一个循环,渲染子视图的列表,每个列表有个删除icon;下方一个 添加icon,点击触发添加子视图。
关键点是子视图循环渲染中,用到了插槽接收,也就意味着在 AddSubformWidget
中创建了插槽的内容,下面我们着重来看看这一部分的实现。
widget 创建插槽内容
核心代码,创建插槽
我们先来看看核心创建插槽的代码
// 创建子视图组件
const widget = this.createWidget(
new MyMetadataViewWidget(runtimeContextHandle),
slotKey, // 插槽名称
{
metadataHandle: runtimeContextHandle,
rootHandle: runtimeContextHandle,
mountedCallChaining: new CallChaining(),
refreshCallChaining: new CallChaining(),
inline: true,
subIndex: this.myMetadataViewWidgetLength
}
);
核心就是 this.createWidget
方法,它会为当前 widget 创建一个子组件,即 MyMetadataViewWidget
;第二个参数指定 vue 里接收的插槽名。
如果细心的你去看了
createWidget
的源码,你就会发现它只是:为子组件指定当前组件为父组件,初始化子组件,为当前组件添加子组件罢了。那么插槽是怎么创建出来的呢?
这就涉及到核心buildWidgetComponent
方法,它会把当前 widget 渲染成 vue ,并把this.setComponent(AddSubform)
设置的组件渲染出来。在渲染AddSubform
时,会调用this.resolveChildren()
为它创建插槽,前提是没有默认插槽(默认插槽是根据 DSL 渲染的,也就是说当前组件 DSL 里的子 widgets 必须为空)。
插槽内容
通过上述创建,插槽是有了,但插槽的内容是什么呢?
内容在 MyMetadataViewWidget
这个数据隔离的视图组件里,它继承了 MetadataViewWidget
,会根据上下文视图最终dsl,渲染页面,以下是获取视图,为视图构建上下文的代码。
// 按钮添加点击事件
@Widget.Method()
public async onAddSubviewBlock() {
// 这里根据子视图模型,子视图名称,获取子视图
const resView = await queryViewDslByModelAndName(this.subviewModel, this.subviewName);
this.createDynamicSubviewWidget(resView);
}
// 创建子视图块
public async createDynamicSubviewWidget(view, activeRecord: ActiveRecord = {}) {
if (view) {
// 根据子视图视图构建上下文
const runtimeContext = createRuntimeContextByView(
{
type: ViewType.Form,
model: view.model,
modelName: view.modelDefinition.name,
module: view.modelDefinition.module,
moduleName: view.modelDefinition.moduleName,
name: view.name,
dsl: view.template
},
true,
uniqueKeyGenerator(),
this.currentHandle
);
// 取得上下文唯一标识
const runtimeContextHandle = runtimeContext.handle;
const slotKey = `Form_${uniqueKeyGenerator()}`;
// 创建子视图组件
const widget = this.createWidget(
new MyMetadataViewWidget(runtimeContextHandle),
slotKey, // 插槽名称
{
metadataHandle: runtimeContextHandle,
rootHandle: runtimeContextHandle,
mountedCallChaining: new CallChaining(),
refreshCallChaining: new CallChaining(),
inline: true,
subIndex: this.myMetadataViewWidgetLength
}
);
// 根据上下文,初始化最终 dsl
widget.initContext(runtimeContext);
// 初始化数据
widget.setData(activeRecord);
......
return widget;
}
}
Oinone社区 作者:银时原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/components/21426.html
访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验