在上一篇文章中,我们对物料区做了简单的渲染,接下来就需要把组件拖拽到画布区,并且在画布区渲染出组件,代码可以分解为两个动作:
- 拖拽组件到画布区时,需要把组件的scheme添加到画布区的数据源中
- 画布区通过组件的scheme渲染出组件
下面分别对这两个步骤展开分析:
拖拽组件到画布区时,需要把组件的scheme添加到画布区
首先回顾下物料区的el-input
的scheme:
const inputSchema = {
__config__: {
label: '单行文本',
labelWidth: null,
showLabel: true,
changeTag: true,
tag: 'el-input',
tagIcon: 'input',
defaultValue: undefined,
required: true,
layout: 'colFormItem',
span: 24,
document: 'https://element.eleme.cn/#/zh-CN/component/input',
// 正则校验规则
regList: [{
pattern: '/^1(3|4|5|7|8|9)\d{9}$/',
message: '手机号格式错误'
}]
},
// 组件的插槽属性
__slot__: {
prepend: '',
append: ''
},
__vModel__: 'mobile',
placeholder: '请输入手机号',
style: { width: '100%' },
clearable: true,
'prefix-icon': 'el-icon-mobile',
'suffix-icon': '',
maxlength: 11,
'show-word-limit': true,
readonly: false,
disabled: false
}
关于字段的解释,我们在之前的文章中介绍过了,这里不再赘述。
在上一篇文章中,使用vuedraggable
实现拖拽,其中draggable
的使用我们不在这里赘述了,大家可以问gpt,这里我们重点说下clone
属性,这个属性是用来克隆组件的,这样我们就不会影响原始数据。end
事件后我们可以添加数据到画布区,代码如下:
export default {
cloneComponent (origin) {
const clone = cloneDeep(origin)
const config = clone.__config__
// 设置span默认值
config.span = this.formConf.span
this.createIdAndKey(clone)
clone.placeholder !== undefined && (clone.placeholder += config.label)
tempActiveData = clone
return tempActiveData
},
endHandler (event) {
if (event.from !== event.to) {
this.activeData = tempActiveData
this.activeId = this.activeData.__config__.formId
}
},
addComponent (item) {
const clone = this.cloneComponent(item)
this.drawingList.push(clone)
this.setActiveFormItem(clone)
},
setActiveFormItem (item) {
this.activeData = item
this.activeId = item.__config__.formId
},
/**
* 创建id和key,以及设置__vModel__
* @param item
* @returns {*}
*/
createIdAndKey (item) {
const config = item.__config__
config.formId = ++this.idGlobal
config.renderKey = `${config.formId}${+new Date()}` // 用于唯一标识每个组件
if(config.layout === 'colFormItem') {
item.__vModel__ = `field${config.formId}`
}
return item
},
}
cloneComponent
在我们拖拽时调用,这个函数的作用是克隆组件,然后设置一些默认值,比如span
,placeholder
等。
endHandler
是在拖拽结束时调用,这个函数的作用是在拖拽结束后将当前组件设置为激活状态。
addComponent
是在点击组件时调用,这个函数的作用是将组件添加到画布区。
setActiveFormItem
是设置当前激活的组件。
createIdAndKey
是创建id和key,以及设置__vModel__
。
简单来说,当点击左侧物料或者拖拽左侧物料时,通过cloneComponent
函数,将数据克隆到画布区的数据源中。
画布区通过组件的scheme渲染出组件
遍历scheme
画布区的组件数据渲染到画布区呢?其实可以通过v-for指令来遍历scheme,然后根据scheme
的tag
属性来渲染不同的组件。具体如何操作呢?首先看下template
部分:
<template>
<div class="center-board">
<el-scrollbar class="center-scrollbar">
<el-row class="center-board-row" :gutter="formConf.gutter">
<el-form
:size="formConf.size"
:label-position="formConf.labelPosition"
:label-width="formConf.labelWidth + 'px'"
:disabled="formConf.disabled"
>
<draggable class="drawing-board" :list="drawingList" :animation="300" group="componentsGroup">
<DraggableItem
v-for="(item, index) in drawingList"
:key="item.renderKey"
:currentItem="item"
:index="index"
:drawingList="drawingList"
:activeId="activeId"
:formConf="formConf"
@activeItem="setActiveFormItem"
@copyItem="drawingItemCopy"
@deleteItem="drawingItemDelete"
></DraggableItem>
</draggable>
<div class="empty-info" v-show="drawingList.length === 0">
从左侧拖入或点选组件进行表单设计
</div>
</el-form>
</el-row>
</el-scrollbar>
</div>
el-form的渲染
el-form
的渲染比较简单,通过formConf
来传递属性,formConf
的定义直接写死即可:
const formConf = {
formRef: 'elForm',
formModel: 'formData',
size: 'medium',
labelPosition: 'right',
labelWidth: 100,
formRules: 'rules',
gutter: 15,
disabled: false,
span: 24,
formBtns: true
}
formConf
的配置都是elementUI
的常见配置,我们继续分析表单项的渲染 ,在schema中,我们定义了一个layout
属性,这个属性表示我们的组件是行内布局还是列布局,如果是列布局,我们需要给组件套一个<el-form-item>
组件,
el-form-item的渲染
继续看模板代码,DraggableItem
组件用来渲染表单项,组件内部代码如下:
export default {
name: "DraggableItem",
props: ["currentItem", "index", "drawingList", "activeId", "formConf"],
render(h) {
const currentItem = this.currentItem
const { activeItem } = this.$listeners
const config = currentItem.__config__;
let className = this.activeId === config.formId ? "drawing-item active-from-item" : "drawing-item";
let labelWidth = config.labelWidth ? `${config.labelWidth}px` : null;
if (config.showLabel === false) labelWidth = "0";
return (
<el-col span={config.span} class={className}
nativeOnClick={event => { activeItem(currentItem); event.stopPropagation() }}>
<el-form-item label={config.showLabel ? config.label : ''} required={config.required} prop={config.__vModel__} label-width={labelWidth} rules={config.regList}>
// <render key={config.renderKey} conf={currentItem} onInput={event => {
// this.$set(config, 'defaultValue', event)
// }}>
// </render>
</el-form-item>
</el-col>
);
}
}
从返回结构上看,和elementUI
的表单组件渲染方式一致,首先从currentItem
中获取到组件的配置信息,然后根据配置信息来渲染el-col
和
el-form-item
组件。其中el-form-item
的prop
属性,这个属性用于表单校验,通过config.__vModel__
来获取到组件的v-model
属性。
el-input的渲染
表单项的渲染最终通过render
组件实现,例如el-input
的渲染,render
组件代码如下:
export default {
//.. other code
render(h) {
return h(this.conf.__config__.tag, dataObject, children)
}
}
this.conf.__config__.tag
是组件的标签,例如el-input
,el-select
等,然后通过h
函数来创建组件实例,通过dataObject
来传递属性,通过children
来传递插槽内容(后面会介绍)。
通过上述代码,我们就可以将一个组件渲染到画布区了,在渲染的时候,我们有几个问题需要解决:
- 数据绑定
- 插槽渲染
- 事件处理
- 表单校验
数据绑定:将组件的props透传给element-ui组件
在调用render函数时,我们的第二个参数是一个数据对象,这我们可以通过这个参数将scheme中的属性透传给element-ui组件。 我们将属性通过attrs属性传递给element-ui组件,如下:
export default {
render(h) {
const dataObject = {
attrs: {
maxlength: 11,
placeholder: "请输入手机号"
},
}
return h(this.conf.__config__.tag, dataObject, children)
}
}
通过attrs我们的大部分属性都可以透传给element-ui组件了,其他的例如style,class等,这些属性我们可以通过数据对象的style
和class
属性传递给element-ui组件。
数据绑定:v-model绑定,事件处理
v-model
是个语法糖,那么我们可以传递value
,并且通过on
属性来传递事件,如下:
export default {
render(h) {
const dataObject = {
attrs: {
maxlength: 11,
placeholder: "请输入手机号"
},
props: {
value: this.__config__.defaultValue
},
on: {
input: (val) => {
this.$set(config, 'defaultValue', event)
}
}
}
return h(this.conf.__config__.tag, dataObject, children)
}
}
通过value
属性传递了v-model
的值,通过on
属性传递了input
事件,在input
事件中,我们修改defaultValue
,这样我们就实现了v-model
与schema的响应。
渲染插槽
在使用el-input
组件时,可能会用到prepend
和append
插槽,在schema中我们的插槽表示如下:
// 组件的插槽属性
__slot__: {
prepend: 'hello',
append: 'world'
},
插槽要通过第三个参数children传递给element-ui组件,我们可以通过如下代码实现:
// 核心代码
const children = []
if (this.conf.__slot__.prepend) {
children.push(h('template', { slot: 'prepend' }, this.conf.__slot__.prepend))
}
if (this.conf.__slot__.append) {
children.push(h('template', { slot: 'append' }, this.conf.__slot__.append))
}
h(this.conf.__config__.tag, dataObject, children)
在上面的代码中我们将slot属性通过h
函数处理为虚拟节点,并插入到children
中,最后通过h
函数渲染到element-ui组件中。
校验表单
在开发过程中,表单的校验离不开几个步骤:
- 校验规则的定义,例如:
{ required: true, message: '请输入手机号', trigger: 'blur' }
<el-form ref="formRef" :model="formData" :rules="rules">
中rules
属性的绑定<el-form-item prop="xxx">
中prop
属性的绑定
校验规则的定义
在我们的schema中,我们定义了一个regList
数组,这个数组中的每一项都相当于我们rules
中的每个属性对应的校验规则,最后我们在渲染的时候会将这个regList
数组转换为rules
中的对象,这里后续我们再展开分析。
prop的绑定我们在el-form-item
组件中通过prop
属性绑定,可以翻看下上面的代码,有提到是通过conf.__vModel__
来绑定prop
属性。
总结
通过上面的分析,我们实现了画布区的渲染,其他的组件的渲染逻辑类似,下回我们继续分享组件编辑区域的实现。
下一期我们将开始编辑区的开发,敬请期待!
如果觉得本文有帮助 记得点赞三连哦 十分感谢!