
教你如何設計Web世界中的表單驗證
- 💬序言
- 🗯?一、驗證輸入框ValidateInput
- 1. 設計稿搶先知
- 2. 簡單的實作
- 3. 抽象驗證規則
- 4. v-model
- 5. 使用$attrs支持默認屬性
- 💭二、驗證表單ValidateForm
- 1. 組件需求分析
- 2. 使用插槽 slot
- 3. 父子組件通訊
- 👁??🗨?四、結束語
- 💯 往期推薦
💬序言
在實際開發中,我們有一個很經常開發的場景,那就是登錄注冊,登錄注冊實際上涉及到的內容是表單驗證,因此呢,表單驗證也是 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 之前的父子組件通訊,
整個程序細水長流,但也有很多新的設計思想值得我們去楷模和學習~
到這里,關于本文的講解就結束啦~
如果您覺得這篇文章有幫助到您的的話不妨點贊支持一下喲~~😛
💯 往期推薦
👉前端只是切圖仔?來學學給開發人看的UI設計
👉緊跟月影大佬的步伐,一起來學習如何寫好JS(上)
👉緊跟月影大佬的步伐,一起來學習如何寫好JS(下)
👉組件庫實戰 | 用vue3+ts實作全域Header和串列資料渲染ColumnList
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/301548.html
標籤:其他
