组件库实战 | 教你如何设计Web世界中的表单验证

简介: 该文章通过实战演练,教授了如何在Web应用中设计和实现表单验证,包括使用Vue.js处理表单输入的验证逻辑、展示错误信息以及通过插槽和组件间通信来增强表单的功能性和用户体验。

封面

💬序言

在实际开发中,我们有一个很经常开发的场景,那就是登录注册。登录注册实际上涉及到的内容是表单验证,因此呢,表单验证也是 web 世界中一个很重要的功能。

那接下里就来了解,在实际的开发中,如何更规范合理地去开发一个表单验证,使其扩展性更强,逻辑更加清晰。

一起来学习⑧~

🗯️一、验证输入框ValidateInput

1. 设计稿抢先知

在了解具体的实现方式之前,我们首先来看原型图。看我们想要实现的表单是怎么样的。如下图所示:

validate原型图

大家可以看到,用我们最熟悉的表单验证就是登录注册操作。其中,整个表单包含四部分。

第一部分是红色框框的内容,红色框框想要做的事情就是,当元素失去焦点时候去触发事件。

第二部分是验证规则,我们不管是在输入用户名还是密码,都需要校验规则来进行校验,比如说不为空,限制输入长度等等内容。

第三部分是当验证没有通过时,需要出现具体的警告。

第四部分就是当所有内容都输入并且要进行提交时,要去验证整个 Form 表单。

2. 简单的实现

我们先来给表单进行一个简单的实现。现在我们在 vue3 项目中的 App.vue 下对整个表单先进行渲染,并且对邮箱的逻辑进行编写。具体代码如下:

<template>
  <div class="container">
    <global-header :user="user"></global-header>
    <form action="">
      <div class="mb-3">
        <label for="exampleInputEmail1" class="form-label">邮箱地址</label>
        <input
        type="email" class="form-control" id="exampleEmail1"
        v-model="emailRef.val"
        @blur="validateEmail">
        <div class="form-text" v-if="emailRef.error">{
  {emailRef.message}}</div>
      </div>
      <div class="mb-3">
        <label for="exampleInputPassword1" class="form-label">密码</label>
        <input type="password" class="form-control" id="exampleInputPassword1">
      </div>
    </form>
  </div>
</template>

<script lang="ts">
import {
     defineComponent, reactive, ref } from 'vue'
import 'bootstrap/dist/css/bootstrap.min.css'
import ColumnList, {
     ColumnProps } from './components/ColumnList.vue'
import GlobalHeader, {
     UserProps } from './components/GlobalHeader.vue'
const currentUser: UserProps = {
    
  isLogin: true,
  name: 'Monday'
}
// 判断是否是邮箱的格式
const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
const testData: ColumnProps[] = [
  {
    
    id: 1,
    title: 'test1专栏',
    description: '众所周知, js 是一门弱类型语言,并且规范较少。这就很容易导致在项目上线之前我们很难发现到它的错误,等到项目一上线,浑然不觉地,bug就UpUp了。于是,在过去的这两年,ts悄悄的崛起了。 本专栏将介绍关于ts的一些学习记录。'
    // avatar: 'https://img0.baidu.com/it/u=3101694723,748884042&fm=26&fmt=auto&gp=0.jpg'
  },
  {
    
    id: 2,
    title: 'test2专栏',
    description: '众所周知, js 是一门弱类型语言,并且规范较少。这就很容易导致在项目上线之前我们很难发现到它的错误,等到项目一上线,浑然不觉地,bug就UpUp了。于是,在过去的这两年,ts悄悄的崛起了。 本专栏将介绍关于ts的一些学习记录。',
    avatar: 'https://img0.baidu.com/it/u=3101694723,748884042&fm=26&fmt=auto&gp=0.jpg'
  }
]

export default defineComponent({
    
  name: 'App',
  components: {
    
    GlobalHeader
  },
  setup () {
    
    // 邮箱验证部分数据内容
    const emailRef = reactive({
    
      val: '',
      error: false,
      message: ''
    })
    // 验证邮箱逻辑
    const validateEmail = () => {
    
      // .trim 表示去掉两边空格
      // 当邮箱为空时
      if (emailRef.val.trim() === '') {
    
        emailRef.error = true
        emailRef.message = 'can not be empty'
      } 
       // 当邮箱不为空,但它不是有效的邮箱格式时
       else if (!emailReg.test(emailRef.val)) {
    
        emailRef.error = true
        emailRef.message = 'should be valid email'
      }
    }
    return {
    
      list: testData,
      user: currentUser,
      emailRef,
      validateEmail

    }
  }
})
</script>

现在,我们来看下具体的显示效果:

邮箱验证

好了,现在我们第一步就实现啦!那么接下来,我们是不是就应该来写 password 的逻辑了呢?

但是啊,如果按照上面这种方式来写的话,有小伙伴会不会觉得就有点重复操作了呢。一两个校验规则还好,如果我们遇到十几二十个呢?也一样每一个都这么写吗?

答案当然是否定的。那么下一步,我们就要对这个校验规则,来进行抽象。

3. 抽象验证规则

继续,我们现在要来抽象出用户名和密码的校验规则,让其可扩展性更强。具体形式如下:

<validate-input :rules="" />

interface RuleProp {
   
    type: 'required' | 'email' | 'range' | ...;
    message: string;
}
export type RulesProp = RuleProp[]

首先,我们要先把表单组件给抽离出来。那么现在,我们在 vue3 项目下的 src|components 下创建一个文件,命名为 ValidateInput.vue其具体代码如下:

<template>
  <div class="validate-input-container pb-3">
      <!-- 手动处理更新和发送事件 -->
      <!-- 使用可选 class,用于动态计算类名 -->
      <input type="text"
        class="form-control"
        :class="{
    'is-invalid': inputRef.error}"
        v-model="inputRef.val"
        @blur="validateInput"
      >
      <span v-if="inputRef.error" class="invalid-feedback">{
  {inputRef.message}}</span>
  </div>
</template>

<script lang="ts">
import {
     defineComponent, reactive, PropType } from 'vue'
// 判断email的正则表达式
const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
// required表示必填值,email表示电子邮件的格式
// message用来展示当出现问题时提示的错误
interface RuleProp {
    
  type: 'required' | 'email';
  message: string;
  validator?: () => boolean;
}

export type RulesProp = RuleProp[]
export default defineComponent({
    
  name: 'ValidateInput',
  props: {
    
    // 用PropType来确定rules的类型,明确里面是RulesProp
    // 这里的rules数据将被父组件 App.vue 给进行动态绑定
    rules: Array as PropType<RulesProp>
  },
  setup(props, context) {
    
    //   输入框的数据
    const inputRef = reactive({
    
      val: '',
      error: false,
      message: ''
    })
    // 验证输入框
    const validateInput = () => {
    
      if (props.rules) {
    
        const allPassed = props.rules.every(rule => {
    
          let passed = true
          inputRef.message = rule.message
          switch (rule.type) {
    
            case 'required':
              passed = (inputRef.val.trim() !== '')
              break
            case 'email':
              passed = emailReg.test(inputRef.val)
              break
            default:
              break
          }
          return passed
        })
        inputRef.error = !allPassed
      }
    }
    return {
    
      inputRef,
      validateInput
    }
  }
})
</script>

<style>

</style>

之后我们将其在 App.vue 下进行注册。具体代码如下:

<template>
  <div class="container">
    <global-header :user="user"></global-header>
    <form action="">
      <div class="mb-3">
        <label class="form-label">邮箱地址</label>
        <validate-input :rules="emailRules"></validate-input>
      </div>
      <div class="mb-3">
        <label for="exampleInputEmail1" class="form-label">邮箱地址</label>
        <input
        type="email" class="form-control" id="exampleEmail1"
        v-model="emailRef.val"
        @blur="validateEmail">
        <div class="form-text" v-if="emailRef.error">{
   {
   emailRef.message}}</div>
      </div>
      <div class="mb-3">
        <label for="exampleInputPassword1" class="form-label">密码</label>
        <input type="password" class="form-control" id="exampleInputPassword1">
      </div>
    </form>
  </div>
</template>

<script lang="ts">
import {
    defineComponent, reactive, ref } from 'vue'
import 'bootstrap/dist/css/bootstrap.min.css'
import ValidateInput, {
    RulesProp } from './components/ValidateInput.vue'
import GlobalHeader, {
    UserProps } from './components/GlobalHeader.vue'
const currentUser: UserProps = {
   
  isLogin: true,
  name: 'Monday'
}
// 判断是否是邮箱的格式
const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/

export default defineComponent({
   
  name: 'App',
  components: {
   
    GlobalHeader,
    ValidateInput
  },
  setup () {
   
    const emailRules: RulesProp = [
      {
    type: 'required', message: '电子邮箱不能为空' },
      {
    type: 'email', message: '请输入正确的电子邮箱格式' }
    ]
    const emailRef = reactive({
   
      val: '',
      error: false,
      message: ''
    })
    const validateEmail = () => {
   
      if (emailRef.val.trim() === '') {
   
        emailRef.error = true
        emailRef.message = 'can not be empty'
      } else if (!emailReg.test(emailRef.val)) {
   
        emailRef.error = true
        emailRef.message = 'should be valid email'
      }
    }
    return {
   
      user: currentUser,
      emailRef,
      validateEmail,
      emailRules
    }
  }
})
</script>

现在,我们在浏览器来看下它好不好用。具体效果如下:

抽象验证规则

大家可以看到,经过抽离后的验证规则,也正确的显示了最终的验证效果。课后呢,大家可以继续对 RuleProptype 进行扩展,比如多多加一个 range 功能等等。

到了这一步,我们对验证规则已经进行了简单的抽离。那接下来要做的事情就是,让父组件 App.vue 可以获取到子组件 ValidateInput.vueinput 框的值,对其进行数据绑定。

4. v-model

说到 input ,大家首先想到的可能是 v-model 。我们先来看下 vue2vue3 在双向绑定方面的区别:

<!-- vue2 原生组件 -->
<input v-model="val">
<input :value="val" @input="val = $event.target.value">

<!-- vue2自定义组件 -->
<my-component v-model="val" />
<my-component :value="val" @input="val = argument[0]" />

<!-- 非同寻常的表单元素 -->
<input type="checkbox" checked="val" @change="">

<!-- vue3 compile 以后的结果 -->
<my-component v-model="foo" />
h(Comp, {
    modelValue: foo,
    'onUpdate: modelValue': value => (foo = value)
})

对于 vue2 的双向绑定来说,主要有以下槽点:

  • 比较繁琐,需要新建一个 model 属性;
  • 不管如何,都只能支持一个 v-model ,没办法双向绑定多个值;
  • 写法比较让人难以理解。

基于以上 vue2 的几个槽点,现在我们用 vue3 来对这个组件的 input 值进行绑定,手动对其处理更新和事件发送。

首先我们在子组件 ValidateInput.vue 中进行处理,处理数据更新和事件发送。具体代码如下:

<template>
  <div class="validate-input-container pb-3">
      <input type="text"
        class="form-control"
        :class="{
    'is-invalid': inputRef.error}"
        :value="inputRef.val"
        @blur="validateInput"
        @input="updateValue"
      >
      <span v-if="inputRef.error" class="invalid-feedback">{
  {inputRef.message}}</span>
  </div>
</template>

<script lang="ts">
import {
     defineComponent, reactive, PropType } from 'vue'
const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
interface RuleProp {
    
  type: 'required' | 'email';
  message: string;
  validator?: () => boolean;
}

export type RulesProp = RuleProp[]
export default defineComponent({
    
  name: 'ValidateInput',
  props: {
    
    rules: Array as PropType<RulesProp>,
    // 创建一个字符串类型的属性 modelValue
    modelValue: String
  },
  setup(props, context) {
    
    // 输入框的数据
    const inputRef = reactive({
    
      val: props.modelValue || '',
      error: false,
      message: ''
    })
    // KeyboardEvent 即键盘输入事件
    const updateValue = (e: KeyboardEvent) => {
    
      const targetValue = (e.target as HTMLInputElement).value
      inputRef.val = targetValue
      // 更新值时需要发送事件 update:modelValue
      context.emit('update:modelValue', targetValue)
    }
    const validateInput = () => {
    
      if (props.rules) {
    
        const allPassed = props.rules.every(rule => {
    
          let passed = true
          inputRef.message = rule.message
          switch (rule.type) {
    
            case 'required':
              passed = (inputRef.val.trim() !== '')
              break
            case 'email':
              passed = emailReg.test(inputRef.val)
              break
            default:
              break
          }
          return passed
        })
        inputRef.error = !allPassed
      }
    }
    return {
    
      inputRef,
      validateInput,
      updateValue
    }
  }
})
</script>

接下来,我们在 App.vue 中对其进行使用,具体代码如下:

<template>
  <div class="container">
    <global-header :user="user"></global-header>
    <form action="">
      <div class="mb-3">
        <label class="form-label">邮箱地址</label>
          <!-- 此处做修改 -->
        <validate-input :rules="emailRules" v-model="emailVal"></validate-input>
        {
  {emailVal}}
      </div>
    </form>
  </div>
</template>

<script lang="ts">
import {
     defineComponent, reactive, ref } from 'vue'
import 'bootstrap/dist/css/bootstrap.min.css'
import ValidateInput, {
     RulesProp } from './components/ValidateInput.vue'
import GlobalHeader, {
     UserProps } from './components/GlobalHeader.vue'
const currentUser: UserProps = {
    
  isLogin: true,
  name: 'Monday'
}
const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/

export default defineComponent({
    
  name: 'App',
  components: {
    
    GlobalHeader,
    ValidateInput
  },
  setup () {
    
    // 创建emailVal的值
    const emailVal = ref('monday')
    const emailRules: RulesProp = [
      {
     type: 'required', message: '电子邮箱不能为空' },
      {
     type: 'email', message: '请输入正确的电子邮箱格式' }
    ]
    const emailRef = reactive({
    
      val: '',
      error: false,
      message: ''
    })
    const validateEmail = () => {
    
      if (emailRef.val.trim() === '') {
    
        emailRef.error = true
        emailRef.message = 'can not be empty'
      } else if (!emailReg.test(emailRef.val)) {
    
        emailRef.error = true
        emailRef.message = 'should be valid email'
      }
    }
    return {
    
      user: currentUser,
      emailRef,
      validateEmail,
      emailRules,
      emailVal
    }
  }
})
</script>

现在,我们来看下数据的值是否成功被绑定。具体效果如下:

v-model绑定值

大家可以看到,数据已经直接的被父组件给获取到并且也成功的绑定了。

5. 使用$attrs支持默认属性

上面我们基本上完成了整个组件的基本功能,现在,我们要来给它设置默认属性,也就是平常我们使用的 placeholder 。如果我们直接在 <validate-input /> 组件中绑定 placeholder ,那么默认地,会直接绑定到它的父组件上面去。因此呢,我们要禁止掉这种行为,让绑定后的 placeholder 给相应的放置在 input 元素上。

那这一块内容呢,涉及到的就是 vue3$attrs$attrs 可以让组件的根元素不继承 attribute ,并且可以手动决定这些 attribute 赋予给哪个元素。具体可查看官方文档:禁用 Attribute 继承

下面,我们来实现这一块的功能。

首先是子组件 ValidateInput.vue具体代码如下:

<template>
  <div class="validate-input-container pb-3">
      <!-- 手动处理更新和发送事件 -->
      <!-- 使用可选 class,用于动态计算类名 -->
      <input
        class="form-control"
        :class="{
    'is-invalid': inputRef.error}"
        :value="inputRef.val"
        @blur="validateInput"
        @input="updateValue"
        v-bind="$attrs"
      >
      <span v-if="inputRef.error" class="invalid-feedback">{
  {inputRef.message}}</span>
  </div>
</template>

<script lang="ts">
import {
     defineComponent, reactive, PropType } from 'vue'
const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
interface RuleProp {
    
  type: 'required' | 'email';
  message: string;
  validator?: () => boolean;
}

export type RulesProp = RuleProp[]
export default defineComponent({
    
  name: 'ValidateInput',
  props: {
    
    rules: Array as PropType<RulesProp>,
    modelValue: String
  },
  // 如果不希望组件的根元素继承attribute,那么可以在组件的选项中设置以下属性
  inheritAttrs: false,
  setup(props, context) {
    
    // 输入框的数据
    const inputRef = reactive({
    
      val: props.modelValue || '',
      error: false,
      message: ''
    })

    // $attrs包裹着传递给组件的attribute的键值对
    // console.log(context.attrs)

    // KeyboardEvent 即键盘输入事件
    const updateValue = (e: KeyboardEvent) => {
    
      const targetValue = (e.target as HTMLInputElement).value
      inputRef.val = targetValue
      context.emit('update:modelValue', targetValue)
    }
    // 验证输入框
    const validateInput = () => {
    
      if (props.rules) {
    
        const allPassed = props.rules.every(rule => {
    
          let passed = true
          inputRef.message = rule.message
          switch (rule.type) {
    
            case 'required':
              passed = (inputRef.val.trim() !== '')
              break
            case 'email':
              passed = emailReg.test(inputRef.val)
              break
            default:
              break
          }
          return passed
        })
        inputRef.error = !allPassed
      }
    }
    return {
    
      inputRef,
      validateInput,
      updateValue
    }
  }
})
</script>

之后是父组件 App.vue具体代码如下:

<template>
  <div class="container">
    <global-header :user="user"></global-header>
    <form action="">
      <div class="mb-3">
        <label class="form-label">邮箱地址</label>
        <!-- 需要让placeholder给添加到子组件的input元素上去,而不是添加到根元素上 -->
        <validate-input
        :rules="emailRules" v-model="emailVal"
        placeholder="请输入邮箱地址"
        type="text" />
      </div>
      <div class="mb-3">
        <label class="form-label">密码</label>
        <validate-input
          type="password"
          placeholder="请输入密码"
          :rules="passwordRules"
          v-model="passwordVal" />
      </div>
    </form>
  </div>
</template>

<script lang="ts">
import {
     defineComponent, reactive, ref } from 'vue'
import 'bootstrap/dist/css/bootstrap.min.css'
import ValidateInput, {
     RulesProp } from './components/ValidateInput.vue'
import GlobalHeader, {
     UserProps } from './components/GlobalHeader.vue'
const currentUser: UserProps = {
    
  isLogin: true,
  name: 'Monday'
}

export default defineComponent({
    
  name: 'App',
  components: {
    
    GlobalHeader,
    ValidateInput
  },
  setup () {
    
    const emailVal = ref('')
    const emailRules: RulesProp = [
      {
     type: 'required', message: '电子邮箱不能为空' },
      {
     type: 'email', message: '请输入正确的电子邮箱格式' }
    ]
    const passwordVal = ref('')
    const passwordRules: RulesProp = [
      {
     type: 'required', message: '密码不能为空' }
    ]
    return {
    
      user: currentUser,
      emailRules,
      emailVal,
      passwordVal,
      passwordRules
    }
  }
})
</script>

从上面的代码中我们可以了解到,通过 inheritAttrs: false$attrs ,实现了我们想要的效果。

我们现在来看下浏览器的显示结果:

使用$sttrs支持默认属性

💭二、验证表单ValidateForm

1. 组件需求分析

ValidateInput 除了基本的功能外,还可以进行功能扩散。比如,自定义校验、更多事件、更多不同的验证元素。

那么下面,我们要来设计整个验证表单,也就是 ValidateForm 组件,并且将 ValidateInput 给对应的使用到其中。

我们先来分析下这个 ValidateForm 都有哪些内容。先看下图:

ValidateForm分析

先看第一部分,我们首先把前面我们封装的 ValidateInput 给放进去,进行语义化包裹。

第二部分,我们可以对提交的按钮进行自定义化,比如提交的文字是怎么样的,提交的按钮又是怎么样的。

第三部分,我们需要有一个确定的事件来触发最后的结果,那么我们就在 ValidateForm 中,获取最后的结果。

第四部分,算是一个隐藏功能,也是这个组件的一个难点,即获取每个 ValidateForm 包裹下的 ValidateInput 的验证结果。

ok,到这里,我们就简单的对 ValidateForm 进行一个分析,那么下面我们将一步步的来对其进行代码设计。

2. 使用插槽 slot

首先,我们要先将提交按钮,做成动态的。一开始初始化一个值,之后呢,可以动态的改变按钮的文字和事件。那这个要用到的就是 vue 的中具名插槽

我们先在 vue3 项目下的 src|components 定义一个子组件,命名为 ValidateForm.vue 。现在我们来设计它,具体代码如下:

<template>
    <form class="validate-form-container">
        <slot name="default"></slot>
        <!-- @click.prevent 用来阻止事件的默认行为 -->
        <!-- 阻止表单提交,仅执行函数submitForm -->
        <div class="submit-area" @click.prevent="submitForm">
            <slot name="submit">
                <!-- 给插槽添加一个默认按钮 -->
                <button type="submit" class="btn btn-primary">提交</button>
            </slot>
        </div>
    </form>
</template>

<script lang="ts">
import {
     defineComponent, onUnmounted } from 'vue'

export default defineComponent({
    
  name: 'ValidateForm',
  components: {
    

  },
  // 在emits字段里面确定所要发送事件的名称
  emits: ['form-submit'],
  setup(props, context) {
    
    const submitForm = () => {
    
      context.emit('form-submit', true)
    }
    return {
    
      submitForm
    }
  }

})
</script>

继续,我们在 App.vue 中使用子组件 ValidateForm.vue具体代码如下:

<template>
  <div class="container">
    <global-header :user="user"></global-header>
    <validate-form @form-submit="onFormSubmit">
      <div class="mb-3">
        <label class="form-label">邮箱地址</label>
        <!-- 需要让placeholder和class给添加到input元素上去,而不是添加到根元素上 -->
        <validate-input
        :rules="emailRules" v-model="emailVal"
        placeholder="请输入邮箱地址"
        type="text" />
      </div>
      <div class="mb-3">
        <label class="form-label">密码</label>
        <validate-input
          type="password"
          placeholder="请输入密码"
          :rules="passwordRules"
          v-model="passwordVal" />
      </div>
      <template #submit>
        <span class="btn btn-danger">Submit</span>
      </template>
    </validate-form>
  </div>
</template>

<script lang="ts">
import {
     defineComponent, reactive, ref } from 'vue'
import 'bootstrap/dist/css/bootstrap.min.css'
// import ColumnList, { ColumnProps } from './components/ColumnList.vue'
import ValidateInput, {
     RulesProp } from './components/ValidateInput.vue'
import ValidateForm from './components/ValidateForm.vue'
import GlobalHeader, {
     UserProps } from './components/GlobalHeader.vue'
const currentUser: UserProps = {
    
  isLogin: true,
  name: 'Monday'
}

export default defineComponent({
    
  name: 'App',
  components: {
    
    // ColumnList,
    GlobalHeader,
    ValidateInput,
    ValidateForm
  },
  setup () {
    
    const emailVal = ref('')
    const emailRules: RulesProp = [
      {
     type: 'required', message: '电子邮箱不能为空' },
      {
     type: 'email', message: '请输入正确的电子邮箱格式' }
    ]
    const passwordVal = ref('')
    const passwordRules: RulesProp = [
      {
     type: 'required', message: '密码不能为空' }
    ]
    // 创建一个函数来监听结果
    const onFormSubmit = (result: boolean) => {
    
      console.log('1234', result)
    }
    return {
    
      user: currentUser,
      emailRules,
      emailVal,
      passwordVal,
      passwordRules,
      onFormSubmit
    }
  }
})
</script>

对于以上代码,我们来做个简单的分析:

  • 子组件通过 emits 来确定要发送给父组件的事件名称,之后呢,父组件通过 @事件名称 的方式来进行调用。
  • 使用具名插槽slot,来对提交表单部分进行动态控制。子组件使用 slot 进行初始化,父组件使用 template 进行动态修改。

3. 父子组件通讯

上面我们解决了第 1 点,组件需求分析中的前三部分。那么现在,我们来看第四点,如何在 ValidateForm 中完成所有 ValidateInput 的验证。

我们先来完善父组件 ValidateForm.vue 的功能。具体代码如下:

<template>
    <form class="validate-form-container">
        <slot name="default"></slot>
        <!-- @click.prevent 用来阻止事件的默认行为 -->
        <!-- 阻止表单提交,仅执行函数submitForm -->
        <div class="submit-area" @click.prevent="submitForm">
            <slot name="submit">
                <!-- 给插槽添加一个默认按钮 -->
                <button type="submit" class="btn btn-primary">提交</button>
            </slot>
        </div>
    </form>
</template>

<script lang="ts">
import {
     defineComponent, onUnmounted } from 'vue'
// 使用 mitt
import mitt from 'mitt'
type ValidateFunc = () => boolean
// 创建一个事件监听器
export const emitter = mitt()

export default defineComponent({
    
  name: 'ValidateForm',
  components: {
    

  },
  // 在emits字段里面确定所要发送事件的名称
  // 注意:只能用全部小写或者驼峰法
  emits: ['formSubmit'],
  setup(props, context) {
    
    // 用于存放一系列的函数,执行以后可以显示错误的信息
    let funcArr: ValidateFunc[] = []
    const submitForm = () => {
    
      const result = funcArr.map(func => func()).every(result => result)
      // 将formSubmit时间进行发送
      context.emit('formSubmit', result)
    }
    // func 即需要接收错误信息
    const callback = (func?: ValidateFunc) => {
    
      if (func) {
    
        funcArr.push(func)
      }
    }
    // 监听器就像是一个收音机一样在等待信息
    emitter.on('form-item-created', callback)
    onUnmounted(() => {
    
      emitter.off('form-item-created', callback)
      funcArr = []
    })
    return {
    
      submitForm
    }
  }

})
</script>

在上面的代码中,我们使用 mitt 库创建了一个事件监听器 emitter ,供给它的子组件 ValidateInput.vue 使用。同时,创建了一个 formSubmit 事件,用于给它的父组件 App.vue 使用。

接着我们来完善子组件 ValidateInput.vue 的功能。具体代码如下:

<template>
  <div class="validate-input-container pb-3">
      <!-- 手动处理更新和发送事件 -->
      <!-- 使用可选 class,用于动态计算类名 -->
      <input
        class="form-control"
        :class="{
    'is-invalid': inputRef.error}"
        :value="inputRef.val"
        @blur="validateInput"
        @input="updateValue"
        v-bind="$attrs"
      >
      <span v-if="inputRef.error" class="invalid-feedback">{
  {inputRef.message}}</span>
  </div>
</template>

<script lang="ts">
import {
     defineComponent, reactive, PropType, onMounted } from 'vue'
import {
     emitter } from './ValidateForm.vue'
const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
interface RuleProp {
    
  type: 'required' | 'email';
  message: string;
  validator?: () => boolean;
}

export type RulesProp = RuleProp[]
export default defineComponent({
    
  name: 'ValidateInput',
  props: {
    
    rules: Array as PropType<RulesProp>,
    modelValue: String
  },
  inheritAttrs: false,
  setup(props, context) {
    
    const inputRef = reactive({
    
      val: props.modelValue || '',
      error: false,
      message: ''
    })
    const updateValue = (e: KeyboardEvent) => {
    
      const targetValue = (e.target as HTMLInputElement).value
      inputRef.val = targetValue
      context.emit('update:modelValue', targetValue)
    }
    // 验证输入框
    const validateInput = () => {
    
      if (props.rules) {
    
        const allPassed = props.rules.every(rule => {
    
          let passed = true
          inputRef.message = rule.message
          switch (rule.type) {
    
            case 'required':
              passed = (inputRef.val.trim() !== '')
              break
            case 'email':
              passed = emailReg.test(inputRef.val)
              break
            default:
              break
          }
          return passed
        })
        inputRef.error = !allPassed
        return allPassed
      }
      return true
    }
    onMounted(() => {
    
      // // 将 input 的值发送出去,即发给给 ValidateForm 组件
      emitter.emit('form-item-created', validateInput)
    })
    return {
    
      inputRef,
      validateInput,
      updateValue
    }
  }
})
</script>

有了 emitter 之后, ValidateInput 就在慢慢地把它的消息传去给它的老父亲,也就是 ValidateForm

最后,我们在 App.vue 中进行调用。具体代码如下:

<template>
  <div class="container">
    <global-header :user="user"></global-header>
    <!-- 将 ValidateForm 中的 formSubmit 事件给传过来到这里使用 -->
    <validate-form @formSubmit="onFormSubmit">
      <div class="mb-3">
        <label class="form-label">邮箱地址</label>
        <validate-input
        :rules="emailRules" v-model="emailVal"
        placeholder="请输入邮箱地址"
        type="text"
        ref="inputRef" />
      </div>
      <div class="mb-3">
        <label class="form-label">密码</label>
        <validate-input
          type="password"
          placeholder="请输入密码"
          :rules="passwordRules"
          v-model="passwordVal" />
      </div>
      <template #submit>
        <span class="btn btn-danger">Submit</span>
      </template>
    </validate-form>
  </div>
</template>

<script lang="ts">
import {
     defineComponent, reactive, ref } from 'vue'
import 'bootstrap/dist/css/bootstrap.min.css'
import ValidateInput, {
     RulesProp } from './components/ValidateInput.vue'
import ValidateForm from './components/ValidateForm.vue'
import GlobalHeader, {
     UserProps } from './components/GlobalHeader.vue'
const currentUser: UserProps = {
    
  isLogin: true,
  name: 'Monday'
}

export default defineComponent({
    
  name: 'App',
  components: {
    
    // ColumnList,
    GlobalHeader,
    ValidateInput,
    ValidateForm
  },
  setup () {
    
    // 用于拿到组件的实例
    const inputRef = ref<any>()
    const emailVal = ref('')
    const emailRules: RulesProp = [
      {
     type: 'required', message: '电子邮箱不能为空' },
      {
     type: 'email', message: '请输入正确的电子邮箱格式' }
    ]
    const passwordVal = ref('')
    const passwordRules: RulesProp = [
      {
     type: 'required', message: '密码不能为空' }
    ]
    // 创建一个函数来监听结果
    const onFormSubmit = (result: boolean) => {
    
      console.log('result', result) // result true
    }
    return {
    
      user: currentUser,
      emailRules,
      emailVal,
      passwordVal,
      passwordRules,
      onFormSubmit,
      inputRef
    }
  }
})
</script>

这部分呢,我们成功调用了 formSubmit 事件,并将其进行监听。

好了,到此,我们的表单验证组件设计就完成啦!不知道大家是否对这种设计思想有了一个新的认识呢?

👁️‍🗨️四、结束语

在上面的文章中,我们讲到了 Web 世界中的表单元素。从验证输入框 ValidateInut 的抽象验证规则,对 v-model 进行重新设计,以及使用 $attrs 来支持默认属性。再到 ValidateForm 的使用具名插槽让提交按钮高度自定义化,再到最后的 input 之前的父子组件通讯。

整个过程细水长流,但也有很多新的设计思想值得我们去楷模和学习~

到这里,关于本文的讲解就结束啦~

如果您觉得这篇文章有帮助到您的的话不妨点赞支持一下哟~~😛

💯 往期推荐

👉前端只是切图仔?来学学给开发人看的UI设计

👉紧跟月影大佬的步伐,一起来学习如何写好JS(上)

👉紧跟月影大佬的步伐,一起来学习如何写好JS(下)

👉组件库实战 | 用vue3+ts实现全局Header和列表数据渲染ColumnList

相关文章
|
1月前
|
移动开发 开发者 HTML5
构建响应式Web界面:Flexbox与Grid的实战应用
【10月更文挑战第22天】随着互联网的普及,用户对Web界面的要求越来越高,不仅需要美观,还要具备良好的响应性和兼容性。为了满足这些需求,Web开发者需要掌握一些高级的布局技术。Flexbox和Grid是现代Web布局的两大法宝,它们分别由CSS3和HTML5引入,能够帮助开发者构建出更加灵活和易于维护的响应式Web界面。本文将深入探讨Flexbox和Grid的实战应用,并通过具体实例来展示它们在构建响应式Web界面中的强大能力。
40 3
|
2月前
|
前端开发 JavaScript Python
Python Web应用中的WebSocket实战:前后端分离时代的实时数据交换
在前后端分离的Web应用开发模式中,如何实现前后端之间的实时数据交换成为了一个重要议题。传统的轮询或长轮询方式在实时性、资源消耗和服务器压力方面存在明显不足,而WebSocket技术的出现则为这一问题提供了优雅的解决方案。本文将通过实战案例,详细介绍如何在Python Web应用中运用WebSocket技术,实现前后端之间的实时数据交换。
94 0
|
1月前
|
设计模式 前端开发 数据库
Python Web开发:Django框架下的全栈开发实战
【10月更文挑战第27天】本文介绍了Django框架在Python Web开发中的应用,涵盖了Django与Flask等框架的比较、项目结构、模型、视图、模板和URL配置等内容,并展示了实际代码示例,帮助读者快速掌握Django全栈开发的核心技术。
148 45
|
1月前
|
前端开发 API 开发者
Python Web开发者必看!AJAX、Fetch API实战技巧,让前后端交互如丝般顺滑!
在Web开发中,前后端的高效交互是提升用户体验的关键。本文通过一个基于Flask框架的博客系统实战案例,详细介绍了如何使用AJAX和Fetch API实现不刷新页面查看评论的功能。从后端路由设置到前端请求处理,全面展示了这两种技术的应用技巧,帮助Python Web开发者提升项目质量和开发效率。
43 1
|
1月前
|
SQL 负载均衡 安全
安全至上:Web应用防火墙技术深度剖析与实战
【10月更文挑战第29天】在数字化时代,Web应用防火墙(WAF)成为保护Web应用免受攻击的关键技术。本文深入解析WAF的工作原理和核心组件,如Envoy和Coraza,并提供实战指南,涵盖动态加载规则、集成威胁情报、高可用性配置等内容,帮助开发者和安全专家构建更安全的Web环境。
55 1
|
1月前
|
安全 数据库 开发者
Python Web开发:Django框架下的全栈开发实战
【10月更文挑战第26天】本文详细介绍了如何在Django框架下进行全栈开发,包括环境安装与配置、创建项目和应用、定义模型类、运行数据库迁移、创建视图和URL映射、编写模板以及启动开发服务器等步骤,并通过示例代码展示了具体实现过程。
51 2
|
3月前
|
JSON Rust 安全
30天拿下Rust之实战Web Server
30天拿下Rust之实战Web Server
74 7
|
2月前
|
移动开发 前端开发 JavaScript
前端开发实战:利用Web Speech API之speechSynthesis实现文字转语音功能
前端开发实战:利用Web Speech API之speechSynthesis实现文字转语音功能
208 0
|
2月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
137 3
|
16天前
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
29 2