在上一篇文章中,我们对物料区做了简单的渲染,接下来就需要把组件拖拽到画布区,并且在画布区渲染出组件,代码可以分解为两个动作:
- 拖拽组件到画布区时,需要把组件的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属性。
总结
通过上面的分析,我们实现了画布区的渲染,其他的组件的渲染逻辑类似,下回我们继续分享组件编辑区域的实现。
下一期我们将开始编辑区的开发,敬请期待!
如果觉得本文有帮助 记得点赞三连哦 十分感谢!