基于Vue.js和Element UI框架的自定义表单Form组件

一个基于Vue.js和Element UI框架的自定义表单Form组件。

这个组件非常灵活,支持自定义类名、表单配置项、动态新增表单项、自定义事件绑定等高级功能。

<template>
  <el-form
    class="t-form"
    ref="form"
    :class="className"
    :model="formOpts.formData"
    :rules="formOpts.rules"
    :label-width="formOpts.labelWidth||'120px'"
    :label-position="formOpts.labelPosition||'right'"
    v-bind="$attrs"
    v-on="$listeners"
  >
    <template v-for="(item, index) in fieldList">
      <el-form-item
        v-if="typeof item.isHideItem == 'function' ? item.isHideItem(formOpts.formData) : !item.isHideItem"
        :key="index"
        :prop="item.value"
        :label="item.label"
        :class="[item.className,{'render_label':item.labelRender},{'slot_label':item.slotName},{'render_laber_position':formOpts.labelPosition},{'is_dynamic':isDynamic}]"
        :rules="item.rules"
        :style="getChildWidth(item)"
        v-bind="{...item.formItemBind}"
      >
        <!-- 自定义label -->
        <template #label v-if="item.labelRender">
          <render-comp :createElementFunc="item.labelRender" />
        </template>
        <!-- 自定义输入框插槽 -->
        <template v-if="item.slotName">
          <slot :name="item.slotName"></slot>
        </template>
        <!-- 文本展示值 -->
        <template v-if="item.textShow">
          <span>{{item.textValue||formOpts.formData[item.value]}}</span>
        </template>
        <template v-if="item.isSelfCom">
          <component
            v-if="item.comp==='t-select-table'"
            :is="item.comp"
            :placeholder="item.placeholder||getPlaceholder(item)"
            v-bind="typeof item.bind == 'function' ? item.bind(formOpts.formData) : {clearable:true,filterable:true,...item.bind}"
            :style="{width: item.width||'100%'}"
            v-on="cEvent(item,'t-select-table')"
          />
          <component
            v-else
            :is="item.comp"
            v-model="formOpts.formData[item.value]"
            :placeholder="item.placeholder||getPlaceholder(item)"
            v-bind="typeof item.bind == 'function' ? item.bind(formOpts.formData) : {clearable:true,filterable:true,...item.bind}"
            :style="{width: item.width||'100%'}"
            v-on="cEvent(item)"
          />
        </template>
        <!-- <child-component
          v-if="!item.slotName&&!item.textShow&&!item.isSelfCom"
          v-bind="item"
          :item="item"
          :form="formOpts"
          :value="formOpts.formData[item.value]"
          @handleEvent="handleEvent"
        >
          <template v-for="(index, name) in $slots" v-slot:[name]>
            <slot :name="name" />
          </template>
          <template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
            <slot :name="name" v-bind="data"></slot>
          </template>
        </child-component>-->
        <component
          v-if="!item.slotName&&!item.textShow&&!item.isSelfCom"
          :is="item.comp"
          v-model="formOpts.formData[item.value]"
          :type="item.comp==='el-input'?item.type||'input':item.type||item.bind.type"
          :placeholder="item.placeholder||getPlaceholder(item)"
          @change="handleEvent(item.event, formOpts.formData[item.value],item)"
          v-bind="typeof item.bind == 'function' ? item.bind(formOpts.formData) : {clearable:true,filterable:true,...item.bind}"
          :style="{width: item.width||'100%'}"
          v-on="cEvent(item)"
        >
          <template #prepend v-if="item.prepend">{{ item.prepend }}</template>
          <template #append v-if="item.append">{{ item.append }}</template>
          <template v-if="item.childSlotName">
            <slot :name="item.childSlotName"></slot>
          </template>
          <component
            v-else
            :is="compChildName(item)"
            v-for="(value, key, index) in selectListType(item)"
            :key="index"
            :disabled="value.disabled"
            :label="compChildLabel(item,value)"
            :value="compChildValue(item,value,key)"
          >{{compChildShowLabel(item,value)}}</component>
        </component>
        <i :key="index+'icon'" v-if="isDynamic" class="el-icon-delete" @click="dynamicDel(index)"></i>
      </el-form-item>
    </template>
    <!-- 按钮 -->
    <div class="footer_btn flex-box flex-ver t-margin-top-5">
      <template v-if="formOpts.btnSlotName">
        <slot :name="formOpts.btnSlotName"></slot>
      </template>
      <template v-if="!formOpts.btnSlotName&&formOpts.operatorList&&formOpts.operatorList.length>0">
        <el-button
          v-for="(val,index) in formOpts.operatorList"
          :key="index"
          @click="val.fun(val)"
          v-bind="{
            type:'primary',
            size:'small',
            ...val.bind
          }"
        >{{ val.label }}</el-button>
      </template>
    </div>
  </el-form>
</template>
<script>
import RenderComp from './renderComp'
// import ChildComponent from './ChildComponent'
export default {
  name: 'TForm',
  components: {
    RenderComp
    // ChildComponent
  },
  props: {
    // 自定义类名
    className: {
      type: String
    },
    /** 表单配置项说明
     * formData object 表单提交数据
     * rules object 验证规则
     * fieldList Array 表单渲染数据
     * operatorList Array 操作按钮list
     * listTypeInfo object 下拉选项数据
     * labelWidth  String label宽度
     */
    formOpts: {
      type: Object,
      default: () => ({})
    },
    // 一行显示几个输入项;最大值4
    widthSize: {
      type: Number,
      default: 2,
      validator: (value) => {
        return value <= 4
      }
    },
    // 是否开启动态新增表单项
    isDynamic: {
      type: Boolean,
      default: false
    },
    // 全局是否开启清除前后空格
    isTrim: {
      type: Boolean,
      default: true
    },
    // ref
    refObj: {
      type: Object
    }
  },
  computed: {
    cEvent() {
      return ({ eventHandle }, type) => {
        let event = { ...eventHandle }
        let changeEvent = {}
        Object.keys(event).forEach(v => {
          changeEvent[v] = (e, ids) => {
            if (type === 't-select-table') {
              event[v] && event[v](e, ids, arguments)
            } else {
              if ((typeof e === 'number' && e === 0) || e) {
                event[v] && event[v](e, this.formOpts, arguments)
              } else {
                event[v] && event[v](this.formOpts, arguments)
              }
            }
          }
        })
        return { ...changeEvent }
      }
    },
    selectListType() {
      return ({ list }) => {
        if (this.formOpts.listTypeInfo) {
          return this.formOpts.listTypeInfo[list]
        } else {
          return []
        }
      }
    },
    // 子组件名称
    compChildName() {
      return ({ type }) => {
        switch (type) {
          case 'checkbox':
            return 'el-checkbox'
          case 'radio':
            return 'el-radio'
          case 'select-arr':
          case 'select-obj':
            return 'el-option'
        }
      }
    },
    // 子子组件label
    compChildLabel() {
      return ({ type, arrLabel }, value) => {
        switch (type) {
          case 'radio':
          case 'checkbox':
            return value.value
          case 'el-select-multiple':
          case 'select-arr':
            return value[arrLabel || 'dictLabel']
          case 'select-obj':
            return value
        }
      }
    },
    // 子子组件value
    compChildValue() {
      return ({ type, arrKey }, value, key) => {
        switch (type) {
          case 'radio':
          case 'checkbox':
            return value.value
          case 'el-select-multiple':
          case 'select-arr':
            return value[arrKey || 'dictValue']
          case 'select-obj':
            return key
        }
      }
    },
    // 子子组件文字展示
    compChildShowLabel() {
      return ({ type, arrLabel }, value) => {
        switch (type) {
          case 'radio':
          case 'checkbox':
            return value.label
          case 'el-select-multiple':
          case 'select-arr':
            return value[arrLabel || 'dictLabel']
          case 'select-obj':
            return value
        }
      }
    }
  },
  data() {
    return {
      colSize: this.widthSize,
      fieldList: this.formOpts.fieldList
    }
  },
  watch: {
    'formOpts.formData': {
      handler(val) {
        // 将form实例返回到父级
        this.$emit('update:refObj', this.$refs.form)
      },
      deep: true // 深度监听
    },
    widthSize(val) {
      if (val > 4) {
        this.$message.warning('widthSize值不能大于4!')
        this.colSize = 4
      } else {
        this.colSize = val
      }
    }
  },
  mounted() {
    // 将form实例返回到父级
    this.$emit('update:refObj', this.$refs.form)
  },
  methods: {
    // label与输入框的布局方式
    getChildWidth(item) {
      if (this.formOpts.labelPosition === 'top') {
        return `flex: 0 1 calc((${100 / (item.widthSize || this.colSize)}% - 10px));margin-right:10px;`
      } else {
        return `flex: 0 1 ${100 / (item.widthSize || this.colSize)}%;`
      }
    },
    // 得到placeholder的显示
    getPlaceholder(row) {
      let placeholder
      if (typeof row.comp === 'string' && row.comp) {
        if (row.comp.includes('input')) {
          placeholder = '请输入' + row.label
        } else if (row.comp.includes('select') || row.comp.includes('date') || row.comp.includes('cascader')) {
          placeholder = '请选择' + row.label
        } else {
          placeholder = row.label
        }
      } else {
        placeholder = row.label
      }
      return placeholder
    },
    dynamicDel(index) {
      this.$emit('del', index)
    },
    // 绑定的相关事件
    handleEvent(type, val, item) {
      // console.log('组件', type, val, item)
      // 去除前后空格
      if (this.isTrim && !item.isTrim && item.comp.includes('el-input') && item.type !== 'password' && item.type !== 'inputNumber') {
        this.formOpts.formData[item.value] = this.formOpts.formData[item.value].trim()
      }
      this.$emit('handleEvent', type, val)
    },
    // 校验
    validate() {
      return new Promise((resolve, reject) => {
        this.$refs.form.validate(valid => {
          if (valid) {
            resolve({
              valid,
              formData: this.formOpts.formData
            })
          } else {
            // eslint-disable-next-line prefer-promise-reject-errors
            reject({
              valid,
              formData: null
            })
          }
        })
      })
    },
    // 重置表单
    resetFields() {
      return this.$refs.form.resetFields()
    },
    // 清空校验
    clearValidate() {
      return this.$refs.form.clearValidate()
    }
  }
}
</script>

<style lang="scss">
.t-form {
  display: flex;
  flex-wrap: wrap;
  .el-form-item {
    display: inline-block;
    width: 50%;
    .el-form-item__content {
      .el-input,
      .el-select,
      .el-date-editor,
      .el-textarea {
        width: 100%;
      }
      .el-input-number {
        .el-input {
          width: inherit;
        }
      }
    }
  }
  .is_dynamic {
    .el-form-item__content {
      display: flex;
      align-items: center;
      .el-icon-delete {
        margin-left: 5px;
        cursor: pointer;
      }
    }
  }
  .t-margin-top-5 {
    margin-top: calc(5px);
  }
  .el-input-number {
    .el-input {
      .el-input__inner {
        text-align: left;
      }
    }
  }
  .render_label {
    .el-form-item__label {
      display: flex;
      align-items: center;
      justify-content: flex-end;
      &::before {
        margin-top: 1px;
      }
    }
  }
  .render_laber_position {
    .el-form-item__label {
      justify-content: flex-start;
    }
  }
  .label_render {
    display: flex;
    align-items: center;
    justify-content: flex-end;
  }
  .slot_label {
    // margin-bottom: 0 !important;
    .el-form-item__content {
      // margin-left: 0 !important;
      label {
        min-width: 108px;
        color: #606266;
        text-align: right;
        margin-right: 12px;
      }
    }
  }
  .footer_btn {
    width: 100%;
  }
}
</style>

模板部分(<template>):

  1. 使用el-form组件创建表单,并绑定了表单数据模型(formOpts.formData)、验证规则(formOpts.rules)、标签宽度(formOpts.labelWidth)和标签位置(formOpts.labelPosition)。
  2. 通过v-for循环渲染fieldList中的每个表单项,创建el-form-item组件。
  3. 对于每个表单项,根据条件渲染不同的模板,例如自定义标签(#label)、自定义输入框插槽(slot)、文本展示值(span)或自定义组件。
  4. 支持动态删除表单项,通过dynamicDel方法触发。
  5. 底部操作按钮区域根据配置渲染操作按钮,可以是插槽(slot)或静态按钮(el-button)。

脚本部分(<script>):

  1. 定义了组件TForm,导入了RenderComp组件。
  2. 组件接收多个props,包括自定义类名、表单配置项、动态新增表单项的开关、全局是否开启清除前后空格等。
  3. 计算属性(computed)用于生成事件处理函数、下拉选项数据、子组件名称、子组件的label和value等。
  4. 数据(data)和watch用于处理表单字段列表和宽度大小。
  5. mounted钩子用于将表单实例返回给父组件。
  6. 方法(methods)包括获取子项宽度、获取placeholder、动态删除表单项、处理事件、表单验证、重置表单和清除校验。

样式部分(<style lang="scss">):

  1. 定义了.t-form的基本样式,包括flex布局和表单项的宽度。
  2. 定义了动态表单项的样式,包括删除图标的样式。
  3. 设置了底部操作按钮区域的样式。
  4. 自定义了标签和输入框的布局样式。

这个组件的设计非常灵活,可以根据不同的需求配置表单项,支持自定义验证规则和操作按钮,适用于多种表单场景。

<template>
  <component
    :is="item.comp"
    v-bind="typeof item.bind == 'function' ? bind(form.formData) : {clearable:true,filterable:true,...item.bind}"
    :placeholder="item.placeholder||getPlaceholder(item)"
    :type="item.comp==='el-input'?item.type||'input':item.type||item.bind.type"
    @change="handleEvent(item.event, form.formData[item.value],item)"
    v-on="cEvent(item)"
    v-model="form.formData[item.value]"
  >
    <!-- 前置文本 -->
    <template #prepend v-if="item.prepend">{{ item.prepend }}</template>
    <!-- 后置文本 -->
    <template #append v-if="item.append">{{ item.append }}</template>
    <!-- 子组件自定义插槽 -->
    <template v-if="item.childSlotName">
      <slot :name="item.childSlotName"></slot>
    </template>
    <!-- <child-component v-for="(cOpt, i) in child" :key="i" v-bind="cOpt" :value="cOpt.value" /> -->
    <child-component
      v-else
      :is="compChildName(item)"
      v-for="(value, key, index) in selectListType(item)"
      :key="index"
      :disabled="value.disabled"
      :label="compChildLabel(item,value)"
      :value="compChildValue(item,value,key)"
    >{{compChildShowLabel(item,value)}}</child-component>
  </component>
</template>
<script>
export default {
  name: 'ChildComponent',
  props: {
    dataIndex: {
      type: String,
      default: ''
    },
    form: {
      type: Object,
      default: () => ({})
    },
    item: {
      type: Object,
      default: () => ({})
    },
    bind: {
      type: [Object, Function]
    },
    comp: {
      type: [String, Object],
      default: ''
    },
    childList: {
      type: Array,
      default: () => ([])
    },
    placeholder: {
      type: String
    },
    value: {
      type: [String, Number, Array, Date, Boolean],
      default: ''
    },
    changeEvent: {
      type: String,
      default: 'input'
    }
  },
  computed: {
    cEvent() {
      return ({ eventHandle }) => {
        let event = { ...eventHandle }
        let changeEvent = {}
        Object.keys(event).forEach(v => {
          changeEvent[v] = (e) => {
            if (e) {
              event[v] && event[v](e, this.form, arguments)
            } else {
              event[v] && event[v](this.form, arguments)
            }
          }
        })
        return { ...changeEvent }
      }
    },
    selectListType() {
      return ({ list }) => {
        if (this.form.listTypeInfo) {
          return this.form.listTypeInfo[list]
        } else {
          return []
        }
      }
    },
    // 子组件名称
    compChildName() {
      return ({ type }) => {
        switch (type) {
          case 'checkbox':
            return 'el-checkbox'
          case 'radio':
            return 'el-radio'
          case 'select-arr':
          case 'select-obj':
            return 'el-option'
        }
      }
    },
    // 子子组件label
    compChildLabel() {
      return ({ type, arrLabel }, value) => {
        switch (type) {
          case 'radio':
          case 'checkbox':
            return value.value
          case 'el-select-multiple':
          case 'select-arr':
            return value[arrLabel || 'dictLabel']
          case 'select-obj':
            return value
        }
      }
    },
    // 子子组件value
    compChildValue() {
      return ({ type, arrKey }, value, key) => {
        switch (type) {
          case 'radio':
          case 'checkbox':
            return value.value
          case 'el-select-multiple':
          case 'select-arr':
            return value[arrKey || 'dictValue']
          case 'select-obj':
            return key
        }
      }
    },
    // 子子组件文字展示
    compChildShowLabel() {
      return ({ type, arrLabel }, value) => {
        switch (type) {
          case 'radio':
          case 'checkbox':
            return value.label
          case 'el-select-multiple':
          case 'select-arr':
            return value[arrLabel || 'dictLabel']
          case 'select-obj':
            return value
        }
      }
    }
    // cEvent() {
    //   let event = { ...this.event }
    //   let changeEvent = {}
    //   if (this.changeEvent) {
    //     changeEvent[this.changeEvent] = (v) => {
    //       event[this.changeEvent] && event[this.changeEvent](v, arguments)
    //       this.$emit('change', v, this.dataIndex, arguments)
    //     }
    //   }
    //   return { ...event, ...changeEvent }
    // }
  },
  methods: {
    // 得到placeholder的显示
    getPlaceholder(row) {
      // console.log(222, row, form)
      let placeholder
      if (typeof row.comp === 'string' && row.comp) {
        if (row.comp.includes('input')) {
          placeholder = '请输入' + row.label
        } else if (row.comp.includes('select') || row.comp.includes('date') || row.comp.includes('cascader')) {
          placeholder = '请选择' + row.label
        } else {
          placeholder = row.label
        }
      }
      return placeholder
    },
    // 绑定的相关事件
    handleEvent(type, val, item) {
      // console.log('组件', type, val, item)
      this.$emit('handleEvent', type, val, item)
    }
  }
}
</script>

这段代码是一个Vue组件,名为ChildComponent,它是一个用于渲染表单项的通用组件。这个组件的设计允许它根据传入的item对象动态地渲染不同类型的表单元素,如输入框、选择框、复选框等。下面是对代码的详细解释:

模板部分(<template>):

  1. 使用<component>标签动态地绑定组件,根据item.comp的值来决定渲染哪个Element UI组件。
  2. 绑定了多个属性,包括自定义绑定(bind)、占位符(placeholder)、类型(type)等。
  3. 监听了change事件,并触发handleEvent方法,这个方法将事件类型、值和当前的item对象发送给父组件。
  4. 使用了插槽(slot)来支持前置文本(#prepend)和后置文本(#append)。
  5. 如果item.childSlotName存在,则渲染一个具有该名称的插槽,允许父组件在当前组件内部插入自定义内容。
  6. 通过v-for循环渲染子组件,这些子组件是根据item对象中的selectListType属性动态生成的。

脚本部分(<script>):

  1. 定义了ChildComponent组件,并设置了一系列的props,包括数据索引(dataIndex)、表单数据(form)、当前项(item)、自定义绑定(bind)、组件名称(comp)、子列表(childList)、占位符(placeholder)、值(value)和变更事件(changeEvent)。
  2. 计算属性(computed)用于生成事件处理函数、下拉选项数据、子组件名称、子组件的label和value等。
  3. getPlaceholder方法用于生成表单元素的占位符文本。
  4. handleEvent方法用于处理组件的事件,并将事件类型、值和当前的item对象发送给父组件。

样式部分(<style>):

样式部分在这段代码中没有给出,但通常会包含对组件内部元素的样式定义。

这个组件的设计使得它可以很容易地集成到表单中,并且可以根据不同的配置渲染出不同的表单元素。它的灵活性和可重用性使其成为构建动态表单的有力工具。

<script>
export default {
  name: 'RenderComp',
  props: {
    createElementFunc: Function
  },
  render (h) {
    return this.createElementFunc(h)
  }
}
</script>

这段代码定义了一个名为 RenderComp 的Vue组件,它的主要作用是接收一个函数作为属性,并在组件的渲染过程中调用这个函数来创建和返回虚拟DOM节点。

组件特点:

  1. 单一职责RenderComp 组件专注于根据传入的函数来渲染内容,不做其他操作,这符合单一职责原则。

  2. 高度可定制:由于组件的渲染完全取决于传入的 createElementFunc 函数,这使得它可以非常灵活地用于各种不同的场景。

  3. 使用虚拟DOM:组件使用了Vue的 render 函数和虚拟DOM节点创建函数 h 来构建内容,这是Vue框架的核心特性之一。

组件用法:

在父组件中,你可以这样使用 RenderComp

<template>
  <render-comp :create-element-func="customRenderFunction"></render-comp>
</template>

<script>
import RenderComp from './RenderComp.vue';

export default {
  components: { RenderComp },
  methods: {
    customRenderFunction(createElement) {
      // 使用 createElement 创建虚拟节点
      return createElement('div', 'Hello, World!');
    }
  }
}
</script>

在这个例子中,customRenderFunction 是传递给 RenderComp 的函数,它使用 createElement 函数来创建一个包含文本 “Hello, World!” 的 div 节点。

组件的 props

  • createElementFunc:这是一个函数类型的属性,它期望接收一个函数,这个函数将接收Vue的虚拟DOM创建函数 h 作为参数,并返回一个虚拟DOM节点。

组件的 render 函数:

  • render 函数是Vue组件生命周期的一部分,它定义了组件如何被渲染到页面上。在这个例子中,render 函数直接调用传入的 createElementFunc 函数,并将其返回值作为最终的虚拟DOM节点。

这种组件设计模式在Vue中被称为“函数式组件”,它通常用于创建高度可复用和可定制的UI组件。

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
你可以使用Vue框架结合Element UI组件来实现查询表单。Vue是一个用于构建用户界面的渐进式JavaScript框架,而Element UI是一个基于Vue的组件库,提供了丰富的UI组件和样式。 首先,你需要安装Vue和Element UI。可以通过npm或者yarn来进行安装。 ```shell npm install vue npm install element-ui ``` 接下来,在你的Vue项目中,你需要在入口文件中引入Vue和Element UI,并注册Element UI组件。 ```javascript import Vue from 'vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' Vue.use(ElementUI) ``` 然后,你可以在你的组件中使用Element UI组件来构建查询表单。例如,你可以使用`el-form`和`el-input`组件来创建一个简单的查询表单。 ```html <template> <div> <el-form :model="form" label-width="80px"> <el-form-item label="关键词"> <el-input v-model="form.keyword"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm">查询</el-button> </el-form-item> </el-form> </div> </template> <script> export default { data() { return { form: { keyword: '' } } }, methods: { submitForm() { // 在这里处理表单提交逻辑 } } } </script> ``` 以上代码展示了一个简单的查询表单,包含一个输入框和一个查询按钮。你可以根据自己的需求进行组件的选择和配置,以实现更复杂的查询功能。 希望这个例子能帮助你开始使用Vue框架结合Element UI组件来实现查询表单。如果你有任何其他问题,请随时提问!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风小薇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值