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

封面

教你如何设计Web世界中的表单验证

  • 💬序言
  • 🗯️一、验证输入框ValidateInput
    • 1. 设计稿抢先知
    • 2. 简单的实现
    • 3. 抽象验证规则
    • 4. v-model
    • 5. 使用$attrs支持默认属性
  • 💭二、验证表单ValidateForm
    • 1. 组件需求分析
    • 2. 使用插槽 slot
    • 3. 父子组件通讯
  • 👁️‍🗨️四、结束语
  • 💯 往期推荐

💬序言

在实际开发中,我们有一个很经常开发的场景,那就是登录注册。登录注册实际上涉及到的内容是表单验证,因此呢,表单验证也是 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><inputtype="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 = trueemailRef.message = 'can not be empty'} // 当邮箱不为空,但它不是有效的邮箱格式时else if (!emailReg.test(emailRef.val)) {emailRef.error = trueemailRef.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 = trueinputRef.message = rule.messageswitch (rule.type) {case 'required':passed = (inputRef.val.trim() !== '')breakcase 'email':passed = emailReg.test(inputRef.val)breakdefault: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><inputtype="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 = trueemailRef.message = 'can not be empty'} else if (!emailReg.test(emailRef.val)) {emailRef.error = trueemailRef.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>,// 创建一个字符串类型的属性 modelValuemodelValue: String},setup(props, context) {// 输入框的数据const inputRef = reactive({val: props.modelValue || '',error: false,message: ''})// KeyboardEvent 即键盘输入事件const updateValue = (e: KeyboardEvent) => {const targetValue = (e.target as HTMLInputElement).valueinputRef.val = targetValue// 更新值时需要发送事件 update:modelValuecontext.emit('update:modelValue', targetValue)}const validateInput = () => {if (props.rules) {const allPassed = props.rules.every(rule => {let passed = trueinputRef.message = rule.messageswitch (rule.type) {case 'required':passed = (inputRef.val.trim() !== '')breakcase 'email':passed = emailReg.test(inputRef.val)breakdefault: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 = trueemailRef.message = 'can not be empty'} else if (!emailReg.test(emailRef.val)) {emailRef.error = trueemailRef.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,用于动态计算类名 --><inputclass="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).valueinputRef.val = targetValuecontext.emit('update:modelValue', targetValue)}// 验证输入框const validateInput = () => {if (props.rules) {const allPassed = props.rules.every(rule => {let passed = trueinputRef.message = rule.messageswitch (rule.type) {case 'required':passed = (inputRef.val.trim() !== '')breakcase 'email':passed = emailReg.test(inputRef.val)breakdefault: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-inputtype="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-inputtype="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,用于动态计算类名 --><inputclass="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).valueinputRef.val = targetValuecontext.emit('update:modelValue', targetValue)}// 验证输入框const validateInput = () => {if (props.rules) {const allPassed = props.rules.every(rule => {let passed = trueinputRef.message = rule.messageswitch (rule.type) {case 'required':passed = (inputRef.val.trim() !== '')breakcase 'email':passed = emailReg.test(inputRef.val)breakdefault:break}return passed})inputRef.error = !allPassedreturn 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-inputtype="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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307873.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode101. 对称二叉树(两种做法)

一&#xff1a;题目 二&#xff1a;上码 方法一&#xff1a;队列 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int …

微服务架构下的测试策略

源宝导读&#xff1a;最近几年&#xff0c;微服务架构越来越火爆&#xff0c;逐渐被企业所采用。随着软件架构的变化&#xff0c;对应的软件测试策略需要作何调整呢&#xff1f;本文将介绍云客在微服务架构下的测试策略。一、云客测试策略模型策略分析行业内的测试策略是一个先…

leetcode222. 完全二叉树的节点个数(两种做法)

一&#xff1a;题目 二&#xff1a;今天不上菜 上码了 方法一&#xff1a;前序遍历 求解 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr)…

听红宝书译者谈Web视角下的前端开发

Web视角下的前端开发⏳序言⏰一、关于前端开发1. 起源、架构、变迁&#xff08;1&#xff09;起源&#xff08;2&#xff09;架构&#xff08;3&#xff09;变迁2. 前端应用的领域&#xff08;1&#xff09;所面向群体&#xff08;2&#xff09;所面向领域3. 语言、框架、工具4…

在家办公这半年,让我开始热爱生活

距离上次更新公众号已经过去一年多了&#xff0c;感觉自己成了“年更姚”。不过好在这个公众号的读者大多是同事朋友&#xff08;加起来也没有多少好么喂&#xff09;&#xff0c;本身对我也没用太高的期待吧。今天想聊聊远程工作这件事。不过我并不想聊团队成员之间如何远程协…

每天都在红绿灯前面梭行,不如自己来实现个红绿灯?

用js实现一个交通灯&#x1f507;前言&#x1f508;一、需求分析 - 交通灯&#x1f509;二、实现版本1. 版本一&#xff1a;简单粗暴版2. 版本二&#xff1a;数据抽象版3. 版本三&#xff1a;过程抽象版4. 版本四&#xff1a;命令式和声明式&#x1f50a;三、在线online&#x…

Git 常用操作 | 重写 commit 历史

当我们修改完代码&#xff0c;提交了一个 commit&#xff0c;然后发现改错了&#xff0c;怎么修正&#xff1f;这种情况分为两种&#xff1a;修正最近一次提交&#xff0c;和修正历史多个提交。修正最近一次提交如果发现刚刚提交的内容有错误&#xff0c;当场再修改一下再提交一…

leetcode257. 二叉树的所有路径(两种做法)

一:题目 二:上码 1&#xff1a;DFS /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), …

【LeetCode之C#解法】 移动零、爬楼梯

题目官网链接https://leetcode-cn.com/problems/move-zeroes/283. 移动零给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。示例:输入: [0,1,0,3,12]输出: [1,3,12,0,0]说明:必须在原数组上操作&#xff0c;不能拷…

幂等问题 vs 如何判断是否是4的幂

判断是否是4的幂&#x1f939;序言&#x1f6b4;一、需求分析 - 判断是否是4的幂等&#x1f93e;二、实现版本1. 版本一&#xff1a;中规中矩法2. 版本二&#xff1a;按位与3. 版本三&#xff1a;按位与优化4. 版本四&#xff1a;正则匹配法⛹️三、结束语&#x1f93c;往期推荐…

leetcode404. 左叶子之和

一&#xff1a;题目 二:上码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(n…

切西瓜法实现微信抢红包功能

运用切西瓜法和栅栏法实现微信抢红包功能✅前言☑️一、需求分析 - 分红包问题&#x1f199;二、实现版本1. 版本一&#xff1a;切西瓜法2. 版本二&#xff1a;栅栏法✳️三、在线Online&#x1f197;四、结束语&#x1f19a;往期推荐✅前言 在现实生活中&#xff0c;非常常见…

如何给扑克洗牌才能更公平?

如何给扑克牌洗牌才能更显公平性&#x1f4fb;前言一、&#x1f399;️需求分析 - 洗牌问题二、&#x1f4bf;实现版本1. 版本一&#xff1a;常规思维2. 版本二&#xff1a;验证公平性3. 版本三&#xff1a;交换法则三、&#x1f4fa;在线Online四、&#x1f4f9;结束语&#x…

这些年我是怎么自学成架构师的(转自知乎)

近日在知乎上一则关于程序员能否“自学成才”的问题&#xff0c;引发了激烈的讨论&#xff01;各种各样的说法都有&#xff0c;最终变成程序员晒学习经历的帖子。作为十多年.NET技术老兵&#xff0c;纯自学成架构师&#xff0c;也来分享下观点&#xff1a;自学当然有效&#xf…

创新视角下的复盘 | 2021/08/01-2021/09/30

&#x1f5c2;️序言 七月份的时候第一次做月度复盘&#xff0c;发现如果只有每日计划&#xff0c;还是比较零散的。月度复盘可以更直观地看到自己的时间规划和及时纠正当下存在的一些问题。 在8-9月份中&#xff0c;有一半在暑假&#xff0c;一半开始于新学期。 &#x1f4…

TIOBE 9 月榜单:C#上涨1.18,Java 同比下滑3.18

喜欢就关注我们吧&#xff01;TIOBE 已公布 2020 年 9 月的编程语言排行榜。C 近期发展状态不错&#xff0c;依旧在榜单中排第四&#xff0c;但排名比率保持增长&#xff0c;本月为 7.11%。2003 年是 C 的巅峰时期&#xff0c;当年 8 月&#xff0c;它的 TIOBE 排名峰值为 17.5…

储存引擎InnoDB 索引选择 为何是B+树 而不是 B树 哈希表

一:概述 首先需要澄清的一点是&#xff0c;MySQL 跟 B 树没有直接的关系&#xff0c;真正与 B 树有关系的是 MySQL 的默认存储引擎 InnoDB&#xff0c;MySQL 中存储引擎的主要作用是负责数据的存储和提取&#xff0c;除了 InnoDB 之外&#xff0c;MySQL 中也支持 MyISAM 作为表…

初探react,用react实现一个todoList功能

初探react&#xff0c;用react实现一个todoList功能&#x1f6f0;️前言&#x1f680;一、react基础1. react简介2. 开发环境搭建3. 工程目录文件简介4. react中最基础的JSX语法&#x1f6f8;二、使用react编写TodoList功能1. 页面构思2. React中的响应式设计思想和事件绑定3. …

《五分钟商学院》个人篇学习总结(上)

【商业知识】| 作者 / Edison Zhou这是EdisonTalk的第285篇原创内容商业篇聚焦的是我们与外部的关系&#xff0c;管理篇聚焦的是我们与内部的关系&#xff0c;而个人篇聚焦的则是我们与自己的关系。与自己斗&#xff0c;其乐无穷&#xff0c;本文是个人篇的上半部分学习总结。本…

Git 实用操作 | 撤销 Commit 提交

有的时候&#xff0c;改完代码提交 commit 后发现写得实在太烂了&#xff0c;连自己的都看不下去&#xff0c;与其修改它还不如丢弃重写。怎么操作呢&#xff1f;使用 reset 撤销如果是最近提交的 commit 要丢弃重写可以用 reset 来操作。比如你刚写了一个 commit&#xff1a;写…