vue3+element-plus el-form表单组件二次封装(vue3+ts项目)TForm组件新增继承 Element-plus 组件的事件使用及el-input 去除前后空格

2023-03-06 TForm组件新增继承 Element-plus 组件的事件使用及el-input 去除前后空格(type=password 除外)

vue3+element-plus el-form表单组件二次封装(vue3+ts项目)TForm组件新增继承 Element-plus 组件的事件使用及el-input 去除前后空格_第1张图片

一、简介

HTML 一行代码,可实现表单输入框/日期选择/下拉选择/复选框选中等及规则校验功能

 <t-form
    v-model="formOpts.ref"
  	:formOpts="formOpts"
  	:widthSize="2"
  	@handleEvent="handleEvent"
    />
    //注意formOpts.ref(t-form组件实例相当于vue2 ref)需要要v-model接收

二、最终效果

三、参数配置

1、Attributes

参数 说明 类型 是否必须
className 自定义类名 String
labelPosition 改变表单项 label 与输入框的布局方式(默认:right) /top String
widthSize 每行显示几个输入项(默认两项) 最大值 4 Number
isTrim 全局是否开启清除前后空格(comp 为 el-input 且 type 不等于’password’) Boolean true
formOpts 表单配置项 Object
—listTypeInfo 下拉选择数据源(type:'select’有效) Object
—fieldList form 表单每项 list Array
------slotName 自定义表单某一项输入框 slot
------childSlotName 自定义表单某一下拉选择项子组件插槽(el-option) slot
------comp form 表单每一项组件是输入框还是下拉选择等(可使用第三方 UI 如 el-select/el-input 也可以使用自定义组件) String
------bind 表单每一项属性(继承第三方 UI 的 Attributes,如 el-input 中的 clearable 清空功能)默认清空及下拉过滤 Object/function
------type form 表单每一项类型 String
------isTrim 是否不清除前后空格(comp 为 el-input 且 type 不等于’password’) Boolean false
------eventHandle 继承 comp 组件的事件 Object -
------widthSize form 表单某一项所占比例(如果占一整行则设置 1) Number
------width form 表单某一项所占实际宽度 String
------arrLabel type=select-arr 时,每个下拉显示的中文 String
------arrKey type=select-arr 时,每个下拉显示的中文传后台的数字 String
------label form 表单每一项 title String
------labelRender 自定义某一项 title function
------value form 表单每一项传给后台的参数 String
------rules 每一项输入框的表单校验规则 Object/Array
------list 下拉选择数据源(仅仅对 type:'select’有效) String
------event 表单每一项事件标志(handleEvent 事件) String
—formData 表单提交数据(对应 fieldList 每一项的 value 值) Object
—labelWidth label 宽度 String
—rules 规则(可依据 elementUI el-form 配置————对应 formData 的值) Object/Array
—operatorList 操作按钮 list Array

2、Events

事件名 说明 返回值
handleEvent 单个查询条件触发事件 fieldList 中 type/查询条件输入的值/fieldList 中 event 值

3、Methods

事件名 说明 返回值
validate 校验表单数据 valid:boolean; formData:最终表单数据
resetFields 重置表单 -
clearValidate 清空校验 -

四、源码

<template>
  <el-form class="t-form" ref="tform" :class="className" :model="formOpts.formData" :rules="formOpts.rules"
    :label-width="formOpts.labelWidth || '100px'" :label-position="formOpts.labelPosition || 'right'" v-bind="$attrs">
    <template v-for="(item, index) in formOpts.fieldList">
      <el-form-item v-if="!item.isHideItem" :key="index" :prop="item.value" :label="item.label" :class="[
        item.className,
        { render_label: item.labelRender },
        { slot_label: item.slotName },
        { render_laber_position_left: formOpts.labelPosition === 'left' },
      ]" :rules="item.rules" :style="getChildWidth(item)" v-bind="$attrs">
        
        <template #label v-if="item.labelRender">
          <render-comp :render="item.labelRender" :item="item" />
        template>
        
        <template v-if="item.slotName">
          <slot :name="item.slotName">slot>
        template>
        
        <template v-if="item.textShow">
          <span class="text_show">{{
            item.textValue || formOpts.formData[item.value]
          }}span>
        template>
        <component v-if="!item.slotName && !item.textShow && item.comp.includes('date')" :is="item.comp"
          v-model="formOpts.formData[item.value]" :type="item.type"
          :placeholder="item.placeholder || getPlaceholder(item)"
          @change="handleEvent(item.event, formOpts.formData[item.value], item)" v-bind="typeof item.bind == 'function'
            ? item.bind(item)
            : { clearable: true, filterable: true, ...item.bind }
            " :style="{ width: item.width || '100%' }" v-on="cEvent(item)" />
        <component v-if="!item.slotName &&
          !item.textShow &&
          item.comp.includes('tree-select')
          " :is="item.comp" v-model="formOpts.formData[item.value]" :type="item.type"
          :placeholder="item.placeholder || getPlaceholder(item)"
          @change="handleEvent(item.event, formOpts.formData[item.value], item)" v-bind="typeof item.bind == 'function'
            ? item.bind(item)
            : { clearable: true, filterable: true, ...item.bind }
            " :style="{ width: item.width || '100%' }" v-on="cEvent(item)" />
        <component v-if="!item.slotName &&
          !item.textShow &&
          !item.comp.includes('date') &&
          !item.comp.includes('tree-select')
          " :is="item.comp" v-model="formOpts.formData[item.value]" :type="item.type"
          :placeholder="item.placeholder || getPlaceholder(item)"
          @change="handleEvent(item.event, formOpts.formData[item.value], item)" v-bind="typeof item.bind == 'function'
            ? item.bind(item)
            : { 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 :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>
      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)"
          :type="val.type || 'primary'" :icon="val.icon" :size="val.size || 'small'" :disabled="val.disabled">
          {{ val.label }}
        el-button>
      template>
    div>
  el-form>
template>
<script lang="ts">
export default {
  name: 'TForm',
}
script>
<script setup lang="ts">
import RenderComp from './renderComp.vue'
import { ElMessage } from 'element-plus'
import { computed, ref, watch, onMounted, getCurrentInstance } from 'vue'
const props = defineProps({
  // 自定义类名
  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: any) => {
      return value <= 4
    },
  },
  // 全局是否开启清除前后空格
  isTrim: {
    type: Boolean,
    default: true,
  },
})
const cEvent = computed(() => {
  return ({ eventHandle }) => {
    return { ...eventHandle }
  }
})
const selectListType = computed(() => {
  return ({ list }) => {
    if (props.formOpts.listTypeInfo) {
      return props.formOpts.listTypeInfo[list]
    } else {
      return []
    }
  }
})
// 子组件名称
const compChildName = computed(() => {
  return (opt: any) => {
    switch (opt.type) {
      case 'checkbox':
        return 'el-checkbox'
      case 'radio':
        return 'el-radio'
      case 'select-arr':
      case 'select-obj':
        return 'el-option'
    }
  }
})
// 子子组件label
const compChildLabel = computed(() => {
  return (opt: any, value) => {
    switch (opt.type) {
      case 'radio':
      case 'checkbox':
        return value.value
      case 'el-select-multiple':
      case 'select-arr':
        return value[opt.arrLabel || 'label']
      case 'select-obj':
        return value
    }
  }
})
// 子子组件value
const compChildValue = computed(() => {
  return (opt: any, value, key) => {
    switch (opt.type) {
      case 'radio':
      case 'checkbox':
        return value.value
      case 'el-select-multiple':
      case 'select-arr':
        return value[opt.arrKey || 'key']
      case 'select-obj':
        return key
    }
  }
})
// 子子组件文字展示
const compChildShowLabel = computed(() => {
  return (opt: any, value) => {
    switch (opt.type) {
      case 'radio':
      case 'checkbox':
        return value.label
      case 'el-select-multiple':
      case 'select-arr':
        return value[opt.arrLabel || 'label']
      case 'select-obj':
        return value
    }
  }
})
const colSize = ref(props.widthSize)
// 获取ref
const tform: any = ref<HTMLElement | null>(null)
// 获取实例方法
const instance: any = getCurrentInstance()
// 抛出事件
const emits = defineEmits(['update:modelValue', 'handleEvent'])
watch(
  () => props.formOpts.formData,
  (val) => {
    // state.form = initForm(opts, true)
    // 将form实例返回到父级
    emits('update:modelValue', tform.value)
  },
  { deep: true }
)
watch(
  () => props.widthSize,
  (val) => {
    if (val > 4) {
      ElMessage.warning('widthSize值不能大于4!')
      colSize.value = 4
    } else {
      colSize.value = val
    }
  },
  { deep: true }
)
onMounted(() => {
  const entries = Object.entries(tform.value.$.exposed)
  // console.log('111', entries)
  for (const [key, value] of entries) {
    instance.exposed[key] = value
  }
  // console.log(789, instance)
  // 将form实例返回到父级
  emits('update:modelValue', tform.value)
})
// label与输入框的布局方式
const getChildWidth = (item) => {
  if (props.formOpts.labelPosition === 'top') {
    return `flex: 0 1 calc((${100 / (item.widthSize || colSize.value)
      }% - 10px));margin-right:10px;`
  } else {
    return `flex: 0 1 ${100 / (item.widthSize || colSize.value)}%;`
  }
}
// placeholder的显示
const getPlaceholder = (row: any) => {
  // console.log(77, row.date)
  let placeholder
  if (row.comp && typeof row.comp == 'string') {
    if (row.comp.includes('input')) {
      placeholder = '请输入' + row.label
    } else if (row.comp.includes('select') || row.comp.includes('date')) {
      placeholder = '请选择' + row.label
    } else {
      placeholder = row.label
    }
  }
  return placeholder
}
// 查询条件change事件
const handleEvent = (type, val, item) => {
  // 去除前后空格
  if (
    props.isTrim &&
    !item.isTrim &&
    item.comp.includes('el-input') &&
    item.type !== 'password' &&
    item.type !== 'inputNumber'
  ) {
    props.formOpts.formData[item.value] =
      props.formOpts.formData[item.value].trim()
  }
  emits('handleEvent', type, val)
}
// 自定义校验
const selfValidate = () => {
  return new Promise((resolve: any, reject: any) => {
    tform.value.validate((valid: boolean) => {
      if (valid) {
        resolve({
          valid,
          formData: props.formOpts.formData,
        })
      } else {
        // eslint-disable-next-line prefer-promise-reject-errors
        reject({
          valid,
          formData: null,
        })
      }
    })
  })
}
// 暴露方法出去
defineExpose({ ...instance.exposed, selfValidate })
script>

<style lang="scss">
.t-form {
  display: flex;
  flex-wrap: wrap;

  .el-form-item {
    align-items: center;

    .el-form-item__content {

      .el-input,
      .el-select,
      .el-date-editor,
      .el-textarea {
        width: 100%;
      }

      .el-input-number {
        .el-input {
          width: inherit;
        }
      }
    }
  }

  // 左对齐
  .asterisk-left {
    .el-form-item__label {
      margin-left: 5px;
    }
  }

  .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_left {
    .el-form-item__label {
      justify-content: flex-start;
    }
  }

  // 顶部对齐
  &.el-form--label-top {
    .render_label {
      .el-form-item__label {
        justify-content: flex-start;
      }
    }
  }

  .label_render {
    display: flex;
    align-items: center;
    justify-content: flex-end;
  }

  .text_show {
    color: var(--el-text-color-primary);
  }

  .slot_label {

    // margin-bottom: 0 !important;
    .el-form-item__content {

      // margin-left: 0 !important;
      label {
        min-width: 108px;
        color: var(--el-text-color-primary);
        text-align: right;
        margin-right: 12px;
      }
    }
  }

  .flex-box {
    display: -webkit-box;
    display: -webkit-flex;
    display: flex;
  }

  .flex-ver {
    align-items: center;
    justify-content: center;
  }

  .footer_btn {
    width: 100%;
  }
}
style>

五、组件地址

gitHub组件地址

gitee码云组件地址

vue3+ts基于Element-plus再次封装基础组件文档

六、相关文章

基于ElementUi再次封装基础组件文档


vue+element-ui的table组件二次封装

你可能感兴趣的:(vue3.2+ts,element-plus,vitepress,vue3,form表单组件封装,typescript,表单组件)