Validate表单组件的封装

之前一直是直接去使用别人现成的组件库,也没有具体去了解人家的组件是怎么封装的,造轮子才会更好地提高自己,所以尝试开始从封装Form表单组件开始

一:组件需求分析

本次封装组件,主要是摸索封装组件的流程,对于具体需要的方法和属性,只会实现其中部分方法和属性,之后一点一点才进行添加

  • 表单项组件,ValidateInput组件的封装
    • 根据传递不同的type类型有着不同的校验规则
    • 支持V-model,对于封装的自定义组件支持v-model双向绑定也是一个很关键的属性
    • tag根节点的类型
  • form组件,提交事件
    • 验证整体表单项,是否通过
    • 清空表单项的内容

二:技术栈

  • Vue3
  • TS
  • Bootstrap样式库

三:封装Validate-input验证表单项组件

思路

1. 明确属性和事件

  • v-model属性
  • rules属性:根据不同type不同校验规则
  • tag根节点类型

ValidateInput组件

html 复制代码
<template>
  <div class="validate-input-container pb-3">
    <input
      class="form-control"
      :class="{ 'is-invalid': inputRef.error }"
      :value="modelValue"
      @blur="validateInput"
      @input="updateValue"
      v-bind="$attrs"
    />
    <span v-if="inputRef.error" class="invalid-feedback">{{ inputRef.message }}</span>
  </div>
</template>
ts 复制代码
<script setup lang="ts">
//禁用 Attributes 继承
defineOptions({
  inheritAttrs: false
})
//定义传来的一个参数类型
interface RangeProp {
  message: string
  length: number
}
interface RuleProp {
  type: 'required' | 'email' | 'range'
  message?: string
  //至少位数
  min?: RangeProp
  max?: RangeProp
}
//数组类型
export type RulesProp = RuleProp[]
//接收参数
const props = defineProps<{
  rules?: RulesProp
  //自定义组件使用v-model需要用modelValue来接收参数
  modelValue: string
}>()

//定义表单的数据
const inputRef = reactive({
  //如果为空
  val: props.modelValue || '',
  error: false, //表单验证是否通过
  message: '' //错误信息
})
</script>
  • 禁用Attributes继承:透传 attribute"指的是传递给一个组件,却没有被该组件声明为 propsemits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyleid

    ts 复制代码
    <!-- <MyButton> 的模板 -->
    <button>click me</button>

    当父组件使用该组件,并且传入class:

    ts 复制代码
    <MyButton class="large" />

    最终会在根元素出现class='large",<MyButton> 并没有将 class 声明为一个它所接受的 prop,所以 class 被视作透传 attribute,自动透传到了 <MyButton> 的根元素上。
    因此需要禁用Attibutes继承

  • 定义接收参数类型

    • rules可选参数,接收数组
    • modelValue:接收字符串即输入的值

2. v-model属性

在使用一个自定义组件给其添加v-model属性,其自定义组件内部做了两件事

  • 将内部原生的input元素的值绑定到组件内部Prop属性modelValue
  • 当原生input元素触发时候,触发一个携带了新值的 update:modelValue 自定义事件
html 复制代码
          <!-- 自定义组件Validate-input -->
          <Validate-input v-model="loginParams.email" :rules="emailRules" placeholder="请输入邮箱地址"></Validate-input>

Validate-input组件

ts 复制代码
//接收参数
const props = defineProps<{
  rules?: RulesProp
  //自定义组件使用v-model需要用modelValue来接收参数
  modelValue: string
}>()

props接收了,modelValue属性,类型string

html 复制代码
    <input
      class="form-control"
      :class="{ 'is-invalid': inputRef.error }"
      :value="modelValue"
      @blur="validateInput"
      @input="updateValue"
      v-bind="$attrs"
    />

设置了自定义事件用于更新value

ts 复制代码
//手动更新value
const updateValue = (e: Event) => {
  //HTMLInputElement输入元素类型
  let targetValue = (e.target as HTMLInputElement).value
  //更新文本框的值
  inputRef.val = targetValue
  emit('update:modelValue', targetValue)
}

因此实现v-model属性

3. rules属性

  • 接收参数
    • type:校验类型,requied | email | range
    • message
    • min:至少几位
    • max:至多几位

抽象验证逻辑

validate-input组件

ts 复制代码
//数组类型
export type RulesProp = RuleProp[]
//接收参数
const props = defineProps<{
  rules?: RulesProp
  //自定义组件使用v-model需要用modelValue来接收参数
  modelValue: string
}>()

参数接收rules是个数组

ts 复制代码
//定义表单的数据
const inputRef = reactive({
  //如果为空
  val: props.modelValue || '',
  error: false, //表单验证是否通过
  message: '' //错误信息
})

定义表单的数据

ts 复制代码
//定义事件
const validateInput = () => {
  //定义邮箱的正则
  let emailReg = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/

  //如果传过来的有验证的话
  if (props.rules) {
    //every方法  遍历全部数组只有全部满足条件才会返回true, 只要有一个false停止遍历
    let allRulePassed = props.rules.every(rule => {
      let passed = true
      //消息赋值 类型断言
      inputRef.message = rule.message as string
      //switch选择对应type类型错误进行校验
      switch (rule.type) {
        case 'required':
          if (inputRef.val.trim() === '') {
            passed = false
          }
          break

        case 'email':
          if (!emailReg.test(inputRef.val)) {
            passed = false
          }
          break
        case 'range':
          passed = validateRange(rule.min, rule.max)
          break
      }
      return passed
    })
    //allRulePassed为false表示通过
    //所以Input.error表示错误应该为true
    inputRef.error = !allRulePassed
    return allRulePassed
  }
  return true
}

定义校验事件

  • 满足全部rules校验才可以通过,因此用到es6数组方法,every只有全部项都为true才会遍历全部返回true,只要有一个项结果为false,就会停止遍历

  • message校验信息赋值给inputRef.message

  • 利用swicth case语句,选择对应的type类型进行校验

  • 需要充分考虑到所有情况,通过passed为true,未通过为fasle

  • range长度校验,由于情况较多,单独封装一个函数去校验

ts 复制代码
 //校验长度
const validateRange = (min: RangeProp | undefined, max: RangeProp | undefined) => {
  let passed = true
  //1. 如果min 存在 ,max不存在
  if (min && !max) {
    inputRef.message = min.message
    passed = !(inputRef.val.length < min.length)
  }
  //2. min不在, max在
  if (!min && max) {
    inputRef.message = max.message
    passed = !(inputRef.val.length > max.length)
  }
  //3. min在 max在
  if (min && max) {
    //若小于
    if (inputRef.val.length < min.length) {
      passed = false
      inputRef.message = min.message
    }
    //若大于
    if (inputRef.val.length > max.length) {
      passed = false
      inputRef.message = max.message
    }
  }
  return passed
}
  • 模板中,根据passed的值,动态绑定未通过的样式

4. 默认属性

validate-input组件

html 复制代码
  <div class="validate-input-container pb-3">
    <input
      class="form-control"
      :class="{ 'is-invalid': inputRef.error }"
      :value="modelValue"
      @blur="validateInput"
      @input="updateValue"
      v-bind="$attrs"
    />
    <span v-if="inputRef.error" class="invalid-feedback">{{ inputRef.message }}</span>
  </div>

父组件

html 复制代码
          <Validate-input
            v-model="loginParams.password"
            :rules="passwordRules"
            type="password"
            placeholder="请输入密码"
          ></Validate-input>
  • 通过显示绑定attrs,使得自定义组件可以使用默认属性

  • ValidateInput组件中,可以通过$attrs属性将type属性将被传递到组件的根元素上

  • 可以在父组件中灵活地传递任何HTML属性给ValidateInput组件,使它更加通用和可配置

四:封装Validate-Form组件

  • 事件
    • form-submit:点击提交触发的事件,回调参数result是布尔值,表示该表单是否通过了校验

1. form-submit事件

  • 当点击提交时,要去验证该表单是否通过了校验,就需要一个个将表单项进行校验,只要一个没通过就返回false,只有全部通过返回true

收集全部表单项校验函数

  • 可以通过将所有表单项的校验函数都添加到一个数组中,然后最终通过every方法遍历是否全部通过

收集

安装mitt并使用,监听事件

css 复制代码
npm install mitt --save

全局挂载

main.ts

javascript 复制代码
import mitt from 'mitt'
//对外暴露全局事件总线实例
export const bus = mitt()

ValidateForm组件中使用

ts 复制代码
<script setup lang="ts">
import { onUnmounted, reactive } from 'vue'
import { bus } from '@/main'
const emits = defineEmits(['form-submit'])
//点击事件
//定义函数类型
type ValidateFunc = () => boolean
//定义类型
//定义接收的函数数组
const funcArr = reactive<ValidateFunc[]>([])
//测试回调
const callback = (func: ValidateFunc) => {
  //将每个校验函数加入数组
  funcArr.push(func)
}
//订阅事件
bus.on('form-item-created', callback as ValidateFunc)

//提交按钮触发事件
const submitForm = () => {
  const result = funcArr.map(func => func()).every(result => result)

  // 触发提交事件
  //遍历funcArr中的每个校验函数
  emits('form-submit', result)
}
onUnmounted(() => {
  // 移除订阅
  bus.off('form-item-created', callback as ValidateFunc)
})
</script>
  • 定义函数类型,返回值为空
  • 利用泛型定义存放每个表单项校验函数存放的数组
  • 订阅事件,在组件卸载的时候再取消订阅

ValidateInput组件中

  • 组件挂载的时候,就直接触发自定义事件,然后将每一项的校验函数传递给ValidateForm组件

Validate-Form组件

  • 然后接受ValidateInput组件传递过来的校验函数,一个个加入到数组中
  • 在submitForm事件中,遍历数组,不能直接使用every方法,因为只要一个不通过就不进行后面的校验,这是不满足我们的需求的
    • 可以看到下面的错误实例,密码原本也是不通过校验的,但是every方法直接在第一个校验失败结束遍历了
  • 因此先利用map函数,先使得每个校验函数都执行,结束后返回一个新的数组存放校验函数的返回值,再通过every遍历数组
  • 最后触发自定义事件,将结果传递给父组件中

父组件

html 复制代码
<Validate-Form @form-submit="submitForm" ref="validateFormRef">
ts 复制代码
const submitForm = (result: boolean) => {
  console.log(result)
}

提交表单元素后清空表单值

具体逻辑跟校验差不多

Validate-Form组件

ts 复制代码
<script setup lang="ts">
import { onUnmounted, reactive } from 'vue'
import { bus } from '@/main'
const emits = defineEmits(['form-submit'])
//点击事件
//定义函数类型
type ValidateFunc = () => boolean
//定义清空Input函数类型
type clearInputsFunc = () => void
//定义类型
//定义接收的函数数组
const funcArr = reactive<ValidateFunc[]>([])
//定义接收用于清空的函数数组
const clearFuncArr = reactive<clearInputsFunc[]>([])
//测试回调
const callback = (func: ValidateFunc) => {
  //将每个校验函数加入数组
  funcArr.push(func)
}
//事件回调
const clearInputFunc = (func: clearInputsFunc) => {
  clearFuncArr.push(func)
}
//绑定监听事件
bus.on('form-item-created', callback as ValidateFunc)
bus.on('form-item-clear', clearInputFunc as clearInputsFunc)

//提交按钮触发事件
const submitForm = () => {
  const result = funcArr.map(func => func()).every(result => result)

  // 触发提交事件
  //遍历funcArr中的每个校验函数
  emits('form-submit', result)
  //遍历清空函数数组并依次并执行
  //当校验通过时候才会清空input
  if (result) {
    clearFuncArr.map(func => func())
  }
}
onUnmounted(() => {
  // 移除事件监听器
  bus.off('form-item-created', callback as ValidateFunc)
  bus.off('form-item-clear', clearInputFunc as clearInputsFunc)
})
</script>

Validate-input组件

ts 复制代码
onMounted(() => {
  //直接把validateInput校验事件传递过去
  bus.emit('form-item-created', validateInput)
  //直接触发事件先传入每个input的值
  bus.emit('form-item-clear', clearInput)
})

//定义表单的数据
const inputRef = reactive({
  //如果为空
  val: props.modelValue || '',
  error: false, //表单验证是否通过
  message: '' //错误信息
})
  • 把清空表单数据的处理函数收集起来
  • 最后利用map方法依次执行清除即可

五:演示和使用

在 vue template 中添加结构代码

html 复制代码
      <Validate-Form @form-submit="submitForm" ref="validateFormRef">
        <!-- 邮箱地址 -->
        <div class="mb-3">
          <label for="exampleInputEmail1" class="form-label">邮箱地址</label>
          <!-- 自定义组件Validate-input -->
          <Validate-input v-model="loginParams.email" :rules="emailRules" placeholder="请输入邮箱地址"></Validate-input>
        </div>
        <!-- 密码 -->
        <div class="mb-3">
          <label for="exampleInputPassword1" class="form-label">密码</label>
          <Validate-input
            v-model="loginParams.password"
            :rules="passwordRules"
            type="password"
            placeholder="请输入密码"
          ></Validate-input>
        </div>
        <template #submit>
          <button type="submit" class="btn btn-primary btn-block btn-large">提交</button>
        </template>
      </Validate-Form>

在 setup语法糖中添加数据

ts 复制代码
//定义验证类型数据
const emailRules: RulesProp = [
  { type: 'required', message: '电子邮箱地址不能为空' },
  { type: 'email', message: '请输入正确的电子邮箱格式' }
]
//定义密码验证类型数据
const passwordRules: RulesProp = [
  { type: 'required', message: '密码不能为空' },
  {
    type: 'range',
    min: { message: '你的密码至少包括6位,不能含有空格', length: 6 },
    max: {
      message: '你的密码至多15位,不能含有空格',
      length: 15
    }
  }
]

//监听结果
const submitForm = (result: boolean) => {
  console.log(result)
}

Validate-Form属性和事件

  • form-submit
    • 类型:事件
    • 默认:-
    • 说明:回调参数 (result: boolean) => void, result 代表是否通过了验证

Validate-Input属性和事件

  • rules
    • 类型:array
    • 默认:-
    • 说明: 单个输入框的验证类型,可以传入包含特定对象的数组, 详情可见上面示例代码
  • tag
    • 类型:input | textarea
    • 默认:input
    • 说明: 根节点类型
  • v-model
    • 类型: string
    • 默认:-
    • 说明: 支持 v-model,请对响应式数据
相关推荐
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端