el-form表单hooks封装和使用,三行代码实现表单

开发后台管理系统,在业务上接触的最多就是表单了,对于使用 Vue 框架进行开发的同学来说,组件库 Element 是肯定会接触的

然而,一旦项目的表单多起来,每个不同的配置,以及多一个字段少一个字段,都要重新写一大段组件代码,显得非常麻烦。或许你会考虑将这些代码封装起来,可是又会发现,封装的表单大多数只在一处地方使用,还不如不封装呢。到底要如何封装,可以让每处使用el-form, 都可以复用相同的组件,减少代码量的同时又具备高度的可定制性?

一般的el-form封装

ini 复制代码
  <MyForm
    defValue={searchDefValue}
    objectArr={searchObj.value}
    inline={true}
    ref={searchRef}
    labelWidth="80px"
    isAsync={false}
    v-slots={{
      append() {
        return (
          <el-form-item>
            <el-button onClick={handleQuery}>
              <Icon icon="ep:search" class="mr-5px" /> 搜索
            </el-button>
            <el-button onClick={resetQuery}>
              <Icon icon="ep:refresh" class="mr-5px" /> 重置
            </el-button>
          </el-form-item>
        )
      }
    }}
  />

这只是代码的template部分,可以看到这样封装的缺点:

  1. 每次需要使用el-form的属性时,还需要组件作为一个中间载体在props中再写一遍
  2. 需要使用el-form的方法,比如表单验证时,还需要先获取组件的ref,然后再通过 searchRef.value && searchRef.value.resetFields() 或者 searchRef.value && searchRef.value.getValue() 这样获取验证和重置,增加了代码的数量

使用组合式函数封装的特点

首先就是代码简洁

主要部分

go 复制代码
const { MyForm, getValue, resetFields } = useForm(
  formConfig,
  formData.value,
  append
)
  1. 相比上面普通的封装,代码简直少了一大半,需要使用验证和重置时,直接使用导出的{ getValue, resetFields }就行,再也不用写一段先获取封装组件ref的代码了
  2. 传入el-form的属性时,再也不用一个个的往myform组件的props里写了
  3. 模版里看着更加简洁,不需要其它无关的属性,属性全部写在formConfig里,模版中只需要一个标签,把所有的逻辑都放在JS中,更加的紧密清晰

像这么长的一个官网表单的例子,去除掉formConfig配置的部分,实际的代码量就这么多

表单的配置

配置方面,和普通封装的数据差不多,唯一不同的是,el-form的属性,写在了formConfig里,el-form-item的属性写在了formConfig.item下面

php 复制代码
const formConfig = reactive({
  labelWidth: '120px', 标签的长度
  itemWidth: '200px',
  inline: false,  行内表单模式
  labelPosition: 'right',  表单域标签的位置
  size: 'default',  表单内组件的尺寸
  item: [
    {
      label: 'Activity name',
      prop: 'name',
      type: 'text',
      rule: [...]  单独的验证规则
    }
    ...
  ]
  rules: [...    表单验证规则,可以自定义校验规则
})

创建el-form标签

我们的useForm因为是ts文件,所以创建节点的时候只能使用createVNode

createVNode的用法:createVNode(标签名, {属性,DOM事件}, 子节点)

首先创建el-form节点

csharp 复制代码
const MyForm = (prop: any, context: any) => {
  if (!formConfig.item.length) return createVNode('')
  props.value = prop
  return createVNode(
    ElForm,
    {
        ref: formRef,
        ...context.attrs,  通过标签传入的属性
        model: formValue,  绑定的表单数据对象
        rules: rules,  表单验证规则
        ...createFormAttrs()
    },
    [createItem(), appendDom()]
  )
}

createFormAttrs方法是用来返回节点的属性,就是formConfig对象的第一层,因为item不是form节点的属性,所以我们使用扩展运算把item属性去掉,以及rules

javascript 复制代码
const createFormAttrs = () => {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { item, rules, ...rest } = formConfig // 返回formConfig中除了item和rules之外的所有属性
    return rest
  }

创建el-form-item节点

这里没什么好说的,跟创建el-form节点一样

php 复制代码
if (!formConfig.item.length) return
return formConfig.item.map((item: any) => {
  return createVNode(
    ElFormItem,
    {
      label: item.label,
      prop: item.prop,
      rules: item.rule
    },
    [createItemInput(item)]
  )
})

创建用户输入的组件

首先有插槽,直接返回插槽,然后设置一个共同的属性defaultAttrs,包含了输入组件都有的 v-model, disabled, 共同的样式

arduino 复制代码
const createItemInput = (item: ObjectForm) => {
    
    if (item.slots) {
      return item.slots(item)
    }
    const defaultAttrs = {
      modelValue: formValue[item.prop],
      'onUpdate:modelValue': (newValue: any) => {
        formValue[item.prop] = newValue
      },
      disabled: item.disabled,
      style: {
        width: item.width ? item.width : formConfig.itemWidth
      }
    }
    ...
})

接下来,就是判断item的type,判断是什么输入组件

go 复制代码
const isCheckboxOrRadio = item.type
      ? ['checkbox', 'radio'].includes(item.type)
      : false
    if (item.type === 'switch') {
      return createVNode(ElSwitch, {
        ...defaultAttrs
      })
    } else if (item.type === 'textarea') {
      return createVNode(ElInput, {
        type: 'textarea',
        ...defaultAttrs
      })
    } else if (item.type === 'number') {
      return createVNode(ElInputNumber, {
        ...defaultAttrs,
        'controls-position': 'right',
        'step-strictly': item.strictly
      })
    } else if (isCheckboxOrRadio) {
      const options = item.options?.map((option: any) =>
        createVNode(
          item.type === 'checkbox' ? ElCheckbox : ElRadio,
          { label: option.value },
          [option.label]
        )
      )
      const Component =
        item.type === 'checkbox' ? ElCheckboxGroup : ElRadioGroup
      return createVNode(Component, { ...defaultAttrs }, options)
    } else if (item.type === 'text' || !item.type || item.type === 'password') {
      return createVNode(ElInput, {
        ...defaultAttrs,
        placeholder: item.placeholder
          ? item.placeholder
          : `请输入${item.label}`,
        maxlength: item.maxlength ? item.maxlength : null,
        type: item.type === 'password' ? 'password' : 'text',
        'auto-complete': 'off',
        'show-word-limit': true
      })
    }

下拉框的数据获取

select下拉框的数据,有时候需要从后台获取,如果需要获取的数据比较多,在页面一个个调接口会比较麻烦 所有接口的名称和参数也加入到了formConfig中

python 复制代码
import { getOption } from '@/api'

{
  label: '地址',
  prop: 'address',
  type: 'select',
  apiFun: getOption,
  params: ''
},

然后在组件中遍历一遍有接口的item,获取接口数据塞到options里面

ini 复制代码
const initDefault = async () => {
    if (!formConfig.item.length) return
    for (let i = 0; i < formConfig.item.length; i++) {
      const item = formConfig.item[i]
      if (item.apiFun) {
        item.loading = true
        const res = await item.apiFun(item.params)
        item.loading = false
        item.options = res.data
      }
      
    }
}
initDefault()

useForm完整代码

useForm.ts 复制代码
import {
  ElForm,
  ElFormItem,
  ElSwitch,
  ElInput,
  ElInputNumber,
  ElCheckbox,
  ElCheckboxGroup,
  ElRadioGroup,
  ElRadio,
  ElSelect,
  ElOption,
  ElDatePicker
} from 'element-plus'
import { createVNode, reactive, ref, VNode, Ref } from 'vue'

export interface ObjectForm {
  loading?: boolean
  type?:
    | 'switch'
    | 'textarea'
    | 'number'
    | ''
    | 'text'
    | 'checkbox'
    | 'radio'
    | 'multipleselect'
    | 'select'
    | 'datetime'
    | 'daterange'
    | 'upload'
    | 'group'
    | 'password'
    | string
  rule?: rule[]
  hidden?: boolean
  prop: string
  label: string
  disabled?: boolean
  strictly?: number
  multiple?: boolean
  stepStrictly?: number
  placeholder?: string
  width?: string
  clearable?: boolean
  filterable?: boolean
  defaultProp?: { label: string; value: string }
  maxlength?: number
  options?: Array<{ label: string; value: string | number }>
  valueFormat?: string // 时间值的格式化
  format?: string // 时间显示的格式化
  remoteMethod?: (value: string, callback: (list: any[]) => void) => any
  disabledDate?: (time: any) => boolean
  apiFun?: (params: any) => Promise<any>
  params?: any
  slots?: any
}

export type rule = {
  validator?: any
  required?: boolean
  message?: string
  pattern?: RegExp
  trigger?: 'blur' | 'change' | string
}

type RulesType = Record<string, Array<rule>>

export interface FormConfig {
  itemWidth?: string
  labelWidth?: string
  inline?: boolean
  labelPosition?: string | 'top' | 'left' | 'right'
  disabledAll?: boolean
  rules: RulesType
  item: ObjectForm[]
}

interface FormProps {
  disabledAll?: boolean
  labelWidth?: string
  labelPosition?: string
  itemWidth?: string
  inline?: boolean
}

export default (
  formConfig: FormConfig,
  formData: { [key: string]: any },
  append?: () => VNode
) => {
  const formValue =
    reactive(JSON.parse(JSON.stringify(formData))) || reactive({})

  const rules = formConfig.rules || {}
  const setRules = (item: ObjectForm) => {
    if (item.rule && !item.hidden) {
      rules[item.prop] = item.rule
    }
  }

  const initDefault = async () => {
    if (!formConfig.item.length) return
    for (let i = 0; i < formConfig.item.length; i++) {
      const item = formConfig.item[i]
      if (item.apiFun) {
        item.loading = true
        const res = await item.apiFun(item.params)
        item.loading = false
        item.options = res.data
      }
      setRules(item)
    }
  }
  initDefault()

  const formRef = ref()

  const resetFields = () => {
    formRef.value.resetFields()
  }

  const getValue = async () => {
    if (Object.keys(rules).length > 0) {
      const res = await formRef.value.validate()
      if (res) {
        return formValue
      }
      return null
    }
    return formValue
  }

  const props: Ref<FormProps> = ref({})

  const MyForm = (prop: any, context: any) => {
    if (!formConfig.item.length) return createVNode('')
    props.value = prop
    return createVNode(
      ElForm,
      {
        ref: formRef,
        ...context.attrs,
        model: formValue,
        rules: rules,
        ...createFormAttrs()
      },
      [createItem(), appendDom()]
    )
  }

  const createFormAttrs = () => {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { item, rules, ...rest } = formConfig
    return rest
  }

  const createItem = () => {
    if (!formConfig.item.length) return
    return formConfig.item.map((item: any) => {
      return createVNode(
        ElFormItem,
        {
          label: item.label,
          prop: item.prop,
          rules: item.rule
        },
        [createItemInput(item)]
      )
    })
  }

  const appendDom = () => {
    return createVNode(ElFormItem, {}, [append && append()])
  }

  const createItemInput = (item: ObjectForm) => {
    if (item.slots) {
      return item.slots(item)
    }
    const defaultAttrs = {
      modelValue: formValue[item.prop],
      'onUpdate:modelValue': (newValue: any) => {
        formValue[item.prop] = newValue
      },
      disabled: item.disabled,
      style: {
        width: item.width ? item.width : formConfig.itemWidth
      }
    }
    const isCheckboxOrRadio = item.type
      ? ['checkbox', 'radio'].includes(item.type)
      : false
    if (item.type === 'switch') {
      return createVNode(ElSwitch, {
        ...defaultAttrs
      })
    } else if (item.type === 'textarea') {
      return createVNode(ElInput, {
        type: 'textarea',
        ...defaultAttrs
      })
    } else if (item.type === 'number') {
      return createVNode(ElInputNumber, {
        ...defaultAttrs,
        'controls-position': 'right',
        'step-strictly': item.strictly
      })
    } else if (isCheckboxOrRadio) {
      const options = item.options?.map((option: any) =>
        createVNode(
          item.type === 'checkbox' ? ElCheckbox : ElRadio,
          { label: option.value },
          [option.label]
        )
      )
      const Component =
        item.type === 'checkbox' ? ElCheckboxGroup : ElRadioGroup
      return createVNode(Component, { ...defaultAttrs }, options)
    } else if (item.type === 'text' || !item.type || item.type === 'password') {
      return createVNode(ElInput, {
        ...defaultAttrs,
        placeholder: item.placeholder
          ? item.placeholder
          : `请输入${item.label}`,
        maxlength: item.maxlength ? item.maxlength : null,
        type: item.type === 'password' ? 'password' : 'text',
        'auto-complete': 'off',
        'show-word-limit': true
      })
    } else if (item.type === 'select') {
      return createVNode(
        ElSelect,
        {
          ...defaultAttrs,
          clearable: item.clearable || true,
          multiple: item.multiple,
          filterable: item.filterable,
          'default-first-option': true,
          'v-loading': item.loading,
          remoteMethod: async (value: any) => {
            if (item.remoteMethod) {
              item.loading = true
              item.remoteMethod(value, (list) => {
                item.options = reactive(list)
                item.loading = false
              })
            }
          }
        },
        item.options?.map((option: any, index: number) => {
          return createVNode(ElOption, {
            key: index,
            label: item.defaultProp
              ? option[item.defaultProp.label]
              : option.label,
            value: item.defaultProp
              ? option[item.defaultProp.value]
              : option.value,
            disabled: option.disabled ? option.disabled : false
          })
        })
      )
    } else if (item.type === 'datetime') {
      return createVNode(ElDatePicker, {
        ...defaultAttrs,
        format: item.format || 'YYYY-MM-DD HH:mm:ss',
        'value-format': item.valueFormat,
        placeholder: '选择日期时间',
        'disabled-date': item.disabledDate
      })
    } else if (item.type === 'daterange') {
      return createVNode(ElDatePicker, {
        type: 'daterange',
        ...defaultAttrs,
        format: item.format || 'YYYY-MM-DD HH:mm:ss',
        'value-format': item.valueFormat,
        'range-separator': '至',
        'start-placeholder': '开始日期',
        'end-placeholder': '结束日期'
      })
    }
  }

  return {
    MyForm,
    getValue,
    resetFields,
    formRef
  }
}

使用

typescript 复制代码
import {
  defineComponent,
  reactive,
  ref
} from 'vue'
import useForm from '@/hooks/useForm'

export default defineComponent({
  props: {},
  emits: [''],
  setup() {
    const validatePass = (_rule: any, value: any, callback: any) => {
      if (!value) {
        callback(new Error('Please input the password'))
      } else {
        callback() // 校验通过,调用 callback 继续校验
      }
    }
    const formConfig = reactive({
      labelWidth: '120px',
      itemWidth: '200px',
      inline: false,
      labelPosition: 'right',
      size: 'default',
      item: [
        {
          label: 'Activity name',
          prop: 'name',
          type: 'text'
        },
        {
          label: 'slots',
          prop: 'slots',
          slots: (scope: any) => {
            return (
              <span>
                <el-button
                  onClick={() => {
                    console.log(scope)
                  }}
                >
                  插槽
                </el-button>
              </span>
            )
          }
        },
        {
          label: 'age',
          prop: 'age',
          rule: [{ required: true, message: 'age is required' }]
        },
        {
          label: 'Activity zone',
          prop: 'region',
          placeholder: 'please select your zone',
          type: 'select',
          options: [
            { label: 'Zone one', value: 'shanghai' },
            { label: 'Zone two', value: 'beijing' }
          ]
        },
        {
          label: '密码',
          prop: 'pass',
          type: 'password'
        },
        {
          label: 'Activity time',
          prop: 'date',
          width: '200px',
          type: 'datetime'
        },
        { label: 'Instant delivery', prop: 'delivery', type: 'switch' },
        {
          label: 'Instant type',
          prop: 'type',
          type: 'checkbox',
          options: [{ label: 'Online activities', value: 'type1' }]
        },
        {
          label: 'Resources',
          prop: 'resource',
          type: 'radio',
          options: [
            { label: 'Sponsor', value: 'type1' },
            { label: 'Venue', value: 'type2' }
          ]
        },
        {
          label: 'Activity form',
          prop: 'desc',
          type: 'textarea'
        }
      ],
      rules: {
        name: [
          {
            required: true,
            message: 'Please input Activity name',
            trigger: 'blur'
          },
          {
            min: 3,
            max: 5,
            message: 'Length should be 3 to 5',
            trigger: 'blur'
          }
        ],
        pass: [{ required: true, validator: validatePass, trigger: 'blur' }]
      }
    })

    const formData = ref({})

    const append = () => {
      return (
        <>
          <el-button type="primary" onClick={onSubmit}>
            Create
          </el-button>
          <el-button onClick={Cancel}>Cancel</el-button>
        </>
      )
    }

    const onSubmit = () => {
      const searchValue: Promise<Record<string, any>> = getValue()
      console.log(searchValue)
      searchValue.then((res) => {
        console.log(res)
      })
    }

    const Cancel = () => {
      resetFields()
      formData.value = {}
    }

    const { MyForm, getValue, resetFields } = useForm(
      formConfig,
      formData.value,
      append
    )
    return () => {
      return (
        <div style="padding: 20px 20px">
          <MyForm />
        </div>
      )
    }
  }
})
相关推荐
一只小阿乐5 小时前
前端web端项目运行的时候没有ip访问地址
vue.js·vue·vue3·web端
计算机学姐5 小时前
基于python+django+vue的旅游网站系统
开发语言·vue.js·python·mysql·django·旅游·web3.py
.ccl6 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码6 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_765347546 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js
辛-夷6 小时前
VUE面试题(单页应用及其首屏加载速度慢的问题)
前端·javascript·vue.js
刘志辉8 小时前
vue传参方法
android·vue.js·flutter
dream_ready8 小时前
linux安装nginx+前端部署vue项目(实际测试react项目也可以)
前端·javascript·vue.js·nginx·react·html5
编写美好前程8 小时前
ruoyi-vue若依前端是如何防止接口重复请求
前端·javascript·vue.js
喵喵酱仔__8 小时前
阻止冒泡事件
前端·javascript·vue.js