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

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

在这里插入图片描述

一、简介

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) /topString
widthSize每行显示几个输入项(默认两项) 最大值 4Number
isTrim全局是否开启清除前后空格(comp 为 el-input 且 type 不等于’password’)Booleantrue
formOpts表单配置项Object
—listTypeInfo下拉选择数据源(type:'select’有效)Object
—fieldListform 表单每项 listArray
------slotName自定义表单某一项输入框slot
------childSlotName自定义表单某一下拉选择项子组件插槽(el-option)slot
------compform 表单每一项组件是输入框还是下拉选择等(可使用第三方 UI 如 el-select/el-input 也可以使用自定义组件)String
------bind表单每一项属性(继承第三方 UI 的 Attributes,如 el-input 中的 clearable 清空功能)默认清空及下拉过滤Object/function
------typeform 表单每一项类型String
------isTrim是否不清除前后空格(comp 为 el-input 且 type 不等于’password’)Booleanfalse
------eventHandle继承 comp 组件的事件Object-
------widthSizeform 表单某一项所占比例(如果占一整行则设置 1)Number
------widthform 表单某一项所占实际宽度String
------arrLabeltype=select-arr 时,每个下拉显示的中文String
------arrKeytype=select-arr 时,每个下拉显示的中文传后台的数字String
------labelform 表单每一项 titleString
------labelRender自定义某一项 titlefunction
------valueform 表单每一项传给后台的参数String
------rules每一项输入框的表单校验规则Object/Array
------list下拉选择数据源(仅仅对 type:'select’有效)String
------event表单每一项事件标志(handleEvent 事件)String
—formData表单提交数据(对应 fieldList 每一项的 value 值)Object
—labelWidthlabel 宽度String
—rules规则(可依据 elementUI el-form 配置————对应 formData 的值)Object/Array
—operatorList操作按钮 listArray

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">
        <!-- 自定义label -->
        <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>
          <!-- <template v-if="!item.comp.includes('date') && !item.childSlotName"> -->
          <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>
          <!-- </template> -->
        </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组件二次封装

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Vue 3中,可以使用 `v-for` 指令和 `provide/inject` API 来实现父组件校验多个子组件表单。 首先,在子组件中定义一个表单对象,并且使用 `provide` API将表单对象提供给父组件,例如: ``` <template> <el-form :model="form"> ... </el-form> </template> <script> import { provide } from 'vue'; export default { data() { return { form: { ... } } }, mounted() { provide('form', this.form); } } </script> ``` 然后,在父组件使用 `v-for` 指令和 `inject` API 获取所有子组件表单对象,例如: ``` <template> <el-form ref="parentForm"> <child-form v-for="(item, index) in items" :key="index"></child-form> ... </el-form> </template> <script> import { inject } from 'vue'; export default { data() { return { items: [...] } }, methods: { validateChildForms() { const childForms = this.items.map((item, index) => { return inject('form', null, true /* throw error if not found */); }); let isValid = true; for (const childForm of childForms) { if (!childForm.validate()) { isValid = false; } } return isValid; } } } </script> ``` 上述代码中,子组件在 `mounted` 钩子函数中使用 `provide` API将表单对象提供给父组件,父组件使用 `v-for` 指令渲染多个子组件,并且使用 `inject` API获取子组件表单对象。然后,在父组件中的 `validateChildForms` 方法中,遍历所有子组件表单对象进行校验。 需要注意的是,这里使用 `inject` API 的第三个参数为 `true`,表示如果没有找到提供的表单对象,则会抛出错误。如果子组件没有提供表单对象,或者表单对象提供的名称不是 `form`,则会抛出错误。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

wocwin

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

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

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

打赏作者

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

抵扣说明:

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

余额充值