手搓低代码表单(二)画布区开发

231 阅读4分钟

在上一篇文章中,我们对物料区做了简单的渲染,接下来就需要把组件拖拽到画布区,并且在画布区渲染出组件,代码可以分解为两个动作:

  • 拖拽组件到画布区时,需要把组件的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在我们拖拽时调用,这个函数的作用是克隆组件,然后设置一些默认值,比如spanplaceholder等。

endHandler是在拖拽结束时调用,这个函数的作用是在拖拽结束后将当前组件设置为激活状态。

addComponent是在点击组件时调用,这个函数的作用是将组件添加到画布区。

setActiveFormItem是设置当前激活的组件。

createIdAndKey是创建id和key,以及设置__vModel__

简单来说,当点击左侧物料或者拖拽左侧物料时,通过cloneComponent函数,将数据克隆到画布区的数据源中。

画布区通过组件的scheme渲染出组件

遍历scheme

画布区的组件数据渲染到画布区呢?其实可以通过v-for指令来遍历scheme,然后根据schemetag属性来渲染不同的组件。具体如何操作呢?首先看下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-colel-form-item组件。其中el-form-itemprop属性,这个属性用于表单校验,通过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-inputel-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等,这些属性我们可以通过数据对象的styleclass属性传递给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组件时,可能会用到prependappend插槽,在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属性。

总结

通过上面的分析,我们实现了画布区的渲染,其他的组件的渲染逻辑类似,下回我们继续分享组件编辑区域的实现。

下一期我们将开始编辑区的开发,敬请期待!

如果觉得本文有帮助 记得点赞三连哦 十分感谢!

系列链接:

1. 手搓低代码表单(一)整体设计以及物料区开发