💬序言
在实际开发中,我们有一个很经常开发的场景,那就是登录注册。登录注册实际上涉及到的内容是表单验证,因此呢,表单验证也是 web
世界中一个很重要的功能。
那接下里就来了解,在实际的开发中,如何更规范合理地去开发一个表单验证,使其扩展性更强,逻辑更加清晰。
一起来学习⑧~
🗯️一、验证输入框ValidateInput
1. 设计稿抢先知
在了解具体的实现方式之前,我们首先来看原型图。看我们想要实现的表单是怎么样的。如下图所示:
大家可以看到,用我们最熟悉的表单验证就是登录注册操作。其中,整个表单包含四部分。
第一部分是红色框框的内容,红色框框想要做的事情就是,当元素失去焦点时候去触发事件。
第二部分是验证规则,我们不管是在输入用户名还是密码,都需要校验规则来进行校验,比如说不为空,限制输入长度等等内容。
第三部分是当验证没有通过时,需要出现具体的警告。
第四部分就是当所有内容都输入并且要进行提交时,要去验证整个 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> 复制代码
现在,我们在浏览器来看下它好不好用。具体效果如下:
大家可以看到,经过抽离后的验证规则,也正确的显示了最终的验证效果。课后呢,大家可以继续对 RuleProp
的 type
进行扩展,比如多多加一个 range
功能等等。
到了这一步,我们对验证规则已经进行了简单的抽离。那接下来要做的事情就是,让父组件 App.vue
可以获取到子组件 ValidateInput.vue
中 input
框的值,对其进行数据绑定。
4. v-model
说到 input
,大家首先想到的可能是 v-model
。我们先来看下 vue2
和 vue3
在双向绑定方面的区别:
<!-- 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> 复制代码
现在,我们来看下数据的值是否成功被绑定。具体效果如下:
大家可以看到,数据已经直接的被父组件给获取到并且也成功的绑定了。
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
,实现了我们想要的效果。
我们现在来看下浏览器的显示结果:
💭二、验证表单ValidateForm
1. 组件需求分析
ValidateInput
除了基本的功能外,还可以进行功能扩散。比如,自定义校验、更多事件、更多不同的验证元素。
那么下面,我们要来设计整个验证表单,也就是 ValidateForm
组件,并且将 ValidateInput
给对应的使用到其中。
我们先来分析下这个 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
之前的父子组件通讯。
整个过程细水长流,但也有很多新的设计思想值得我们去楷模和学习~
到这里,关于本文的讲解就结束啦~