组件库实战 | 教你如何设计Web世界中的表单验证
教你如何設(shè)計(jì)Web世界中的表單驗(yàn)證
- 💬序言
- 🗯?一、驗(yàn)證輸入框ValidateInput
- 1. 設(shè)計(jì)稿搶先知
- 2. 簡單的實(shí)現(xiàn)
- 3. 抽象驗(yàn)證規(guī)則
- 4. v-model
- 5. 使用$attrs支持默認(rèn)屬性
- 💭二、驗(yàn)證表單ValidateForm
- 1. 組件需求分析
- 2. 使用插槽 slot
- 3. 父子組件通訊
- 👁??🗨?四、結(jié)束語
- 💯 往期推薦
💬序言
在實(shí)際開發(fā)中,我們有一個(gè)很經(jīng)常開發(fā)的場景,那就是登錄注冊。登錄注冊實(shí)際上涉及到的內(nèi)容是表單驗(yàn)證,因此呢,表單驗(yàn)證也是 web 世界中一個(gè)很重要的功能。
那接下里就來了解,在實(shí)際的開發(fā)中,如何更規(guī)范合理地去開發(fā)一個(gè)表單驗(yàn)證,使其擴(kuò)展性更強(qiáng),邏輯更加清晰。
一起來學(xué)習(xí)⑧~
🗯?一、驗(yàn)證輸入框ValidateInput
1. 設(shè)計(jì)稿搶先知
在了解具體的實(shí)現(xiàn)方式之前,我們首先來看原型圖??次覀兿胍獙?shí)現(xiàn)的表單是怎么樣的。如下圖所示:
大家可以看到,用我們最熟悉的表單驗(yàn)證就是登錄注冊操作。其中,整個(gè)表單包含四部分。
第一部分是紅色框框的內(nèi)容,紅色框框想要做的事情就是,當(dāng)元素失去焦點(diǎn)時(shí)候去觸發(fā)事件。
第二部分是驗(yàn)證規(guī)則,我們不管是在輸入用戶名還是密碼,都需要校驗(yàn)規(guī)則來進(jìn)行校驗(yàn),比如說不為空,限制輸入長度等等內(nèi)容。
第三部分是當(dāng)驗(yàn)證沒有通過時(shí),需要出現(xiàn)具體的警告。
第四部分就是當(dāng)所有內(nèi)容都輸入并且要進(jìn)行提交時(shí),要去驗(yàn)證整個(gè) Form 表單。
2. 簡單的實(shí)現(xiàn)
我們先來給表單進(jìn)行一個(gè)簡單的實(shí)現(xiàn)?,F(xiàn)在我們在 vue3 項(xiàng)目中的 App.vue 下對整個(gè)表單先進(jìn)行渲染,并且對郵箱的邏輯進(jìn)行編寫。具體代碼如下:
<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 是一門弱類型語言,并且規(guī)范較少。這就很容易導(dǎo)致在項(xiàng)目上線之前我們很難發(fā)現(xiàn)到它的錯(cuò)誤,等到項(xiàng)目一上線,渾然不覺地,bug就UpUp了。于是,在過去的這兩年,ts悄悄的崛起了。 本專欄將介紹關(guān)于ts的一些學(xué)習(xí)記錄。'// avatar: 'https://img0.baidu.com/it/u=3101694723,748884042&fm=26&fmt=auto&gp=0.jpg'},{id: 2,title: 'test2專欄',description: '眾所周知, js 是一門弱類型語言,并且規(guī)范較少。這就很容易導(dǎo)致在項(xiàng)目上線之前我們很難發(fā)現(xiàn)到它的錯(cuò)誤,等到項(xiàng)目一上線,渾然不覺地,bug就UpUp了。于是,在過去的這兩年,ts悄悄的崛起了。 本專欄將介紹關(guān)于ts的一些學(xué)習(xí)記錄。',avatar: 'https://img0.baidu.com/it/u=3101694723,748884042&fm=26&fmt=auto&gp=0.jpg'} ]export default defineComponent({name: 'App',components: {GlobalHeader},setup () {// 郵箱驗(yàn)證部分?jǐn)?shù)據(jù)內(nèi)容const emailRef = reactive({val: '',error: false,message: ''})// 驗(yàn)證郵箱邏輯const validateEmail = () => {// .trim 表示去掉兩邊空格// 當(dāng)郵箱為空時(shí)if (emailRef.val.trim() === '') {emailRef.error = trueemailRef.message = 'can not be empty'} // 當(dāng)郵箱不為空,但它不是有效的郵箱格式時(shí)else if (!emailReg.test(emailRef.val)) {emailRef.error = trueemailRef.message = 'should be valid email'}}return {list: testData,user: currentUser,emailRef,validateEmail}} }) </script>現(xiàn)在,我們來看下具體的顯示效果:
好了,現(xiàn)在我們第一步就實(shí)現(xiàn)啦!那么接下來,我們是不是就應(yīng)該來寫 password 的邏輯了呢?
但是啊,如果按照上面這種方式來寫的話,有小伙伴會不會覺得就有點(diǎn)重復(fù)操作了呢。一兩個(gè)校驗(yàn)規(guī)則還好,如果我們遇到十幾二十個(gè)呢?也一樣每一個(gè)都這么寫嗎?
答案當(dāng)然是否定的。那么下一步,我們就要對這個(gè)校驗(yàn)規(guī)則,來進(jìn)行抽象。
3. 抽象驗(yàn)證規(guī)則
繼續(xù),我們現(xiàn)在要來抽象出用戶名和密碼的校驗(yàn)規(guī)則,讓其可擴(kuò)展性更強(qiáng)。具體形式如下:
<validate-input :rules="" />interface RuleProp {type: 'required' | 'email' | 'range' | ...;message: string; } export type RulesProp = RuleProp[]首先,我們要先把表單組件給抽離出來。那么現(xiàn)在,我們在 vue3 項(xiàng)目下的 src|components 下創(chuàng)建一個(gè)文件,命名為 ValidateInput.vue 。其具體代碼如下:
<template><div class="validate-input-container pb-3"><!-- 手動(dòng)處理更新和發(fā)送事件 --><!-- 使用可選 class,用于動(dòng)態(tài)計(jì)算類名 --><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的正則表達(dá)式 const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/ // required表示必填值,email表示電子郵件的格式 // message用來展示當(dāng)出現(xiàn)問題時(shí)提示的錯(cuò)誤 interface RuleProp {type: 'required' | 'email';message: string;validator?: () => boolean; }export type RulesProp = RuleProp[] export default defineComponent({name: 'ValidateInput',props: {// 用PropType來確定rules的類型,明確里面是RulesProp// 這里的rules數(shù)據(jù)將被父組件 App.vue 給進(jìn)行動(dòng)態(tài)綁定rules: Array as PropType<RulesProp>},setup(props, context) {// 輸入框的數(shù)據(jù)const inputRef = reactive({val: '',error: false,message: ''})// 驗(yàn)證輸入框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 下進(jìn)行注冊。具體代碼如下:
<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>現(xiàn)在,我們在瀏覽器來看下它好不好用。具體效果如下:
大家可以看到,經(jīng)過抽離后的驗(yàn)證規(guī)則,也正確的顯示了最終的驗(yàn)證效果。課后呢,大家可以繼續(xù)對 RuleProp 的 type 進(jìn)行擴(kuò)展,比如多多加一個(gè) range 功能等等。
到了這一步,我們對驗(yàn)證規(guī)則已經(jīng)進(jìn)行了簡單的抽離。那接下來要做的事情就是,讓父組件 App.vue 可以獲取到子組件 ValidateInput.vue 中 input 框的值,對其進(jìn)行數(shù)據(jù)綁定。
4. v-model
說到 input ,大家首先想到的可能是 v-model 。我們先來看下 vue2 和 vue3 在雙向綁定方面的區(qū)別:
<!-- 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 以后的結(jié)果 --> <my-component v-model="foo" /> h(Comp, {modelValue: foo,'onUpdate: modelValue': value => (foo = value) })對于 vue2 的雙向綁定來說,主要有以下槽點(diǎn):
- 比較繁瑣,需要新建一個(gè) model 屬性;
- 不管如何,都只能支持一個(gè) v-model ,沒辦法雙向綁定多個(gè)值;
- 寫法比較讓人難以理解。
基于以上 vue2 的幾個(gè)槽點(diǎn),現(xiàn)在我們用 vue3 來對這個(gè)組件的 input 值進(jìn)行綁定,手動(dòng)對其處理更新和事件發(fā)送。
首先我們在子組件 ValidateInput.vue 中進(jìn)行處理,處理數(shù)據(jù)更新和事件發(fā)送。具體代碼如下:
<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>,// 創(chuàng)建一個(gè)字符串類型的屬性 modelValuemodelValue: String},setup(props, context) {// 輸入框的數(shù)據(jù)const inputRef = reactive({val: props.modelValue || '',error: false,message: ''})// KeyboardEvent 即鍵盤輸入事件const updateValue = (e: KeyboardEvent) => {const targetValue = (e.target as HTMLInputElement).valueinputRef.val = targetValue// 更新值時(shí)需要發(fā)送事件 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 中對其進(jìn)行使用,具體代碼如下:
<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 () {// 創(chuàng)建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>現(xiàn)在,我們來看下數(shù)據(jù)的值是否成功被綁定。具體效果如下:
大家可以看到,數(shù)據(jù)已經(jīng)直接的被父組件給獲取到并且也成功的綁定了。
5. 使用$attrs支持默認(rèn)屬性
上面我們基本上完成了整個(gè)組件的基本功能,現(xiàn)在,我們要來給它設(shè)置默認(rèn)屬性,也就是平常我們使用的 placeholder 。如果我們直接在 <validate-input /> 組件中綁定 placeholder ,那么默認(rèn)地,會直接綁定到它的父組件上面去。因此呢,我們要禁止掉這種行為,讓綁定后的 placeholder 給相應(yīng)的放置在 input 元素上。
那這一塊內(nèi)容呢,涉及到的就是 vue3 的 $attrs , $attrs 可以讓組件的根元素不繼承 attribute ,并且可以手動(dòng)決定這些 attribute 賦予給哪個(gè)元素。具體可查看官方文檔:禁用 Attribute 繼承
下面,我們來實(shí)現(xiàn)這一塊的功能。
首先是子組件 ValidateInput.vue ,具體代碼如下:
<template><div class="validate-input-container pb-3"><!-- 手動(dòng)處理更新和發(fā)送事件 --><!-- 使用可選 class,用于動(dòng)態(tài)計(jì)算類名 --><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,那么可以在組件的選項(xiàng)中設(shè)置以下屬性inheritAttrs: false,setup(props, context) {// 輸入框的數(shù)據(jù)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)}// 驗(yàn)證輸入框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 ,實(shí)現(xiàn)了我們想要的效果。
我們現(xiàn)在來看下瀏覽器的顯示結(jié)果:
💭二、驗(yàn)證表單ValidateForm
1. 組件需求分析
ValidateInput 除了基本的功能外,還可以進(jìn)行功能擴(kuò)散。比如,自定義校驗(yàn)、更多事件、更多不同的驗(yàn)證元素。
那么下面,我們要來設(shè)計(jì)整個(gè)驗(yàn)證表單,也就是 ValidateForm 組件,并且將 ValidateInput 給對應(yīng)的使用到其中。
我們先來分析下這個(gè) ValidateForm 都有哪些內(nèi)容。先看下圖:
先看第一部分,我們首先把前面我們封裝的 ValidateInput 給放進(jìn)去,進(jìn)行語義化包裹。
第二部分,我們可以對提交的按鈕進(jìn)行自定義化,比如提交的文字是怎么樣的,提交的按鈕又是怎么樣的。
第三部分,我們需要有一個(gè)確定的事件來觸發(fā)最后的結(jié)果,那么我們就在 ValidateForm 中,獲取最后的結(jié)果。
第四部分,算是一個(gè)隱藏功能,也是這個(gè)組件的一個(gè)難點(diǎn),即獲取每個(gè) ValidateForm 包裹下的 ValidateInput 的驗(yàn)證結(jié)果。
ok,到這里,我們就簡單的對 ValidateForm 進(jìn)行一個(gè)分析,那么下面我們將一步步的來對其進(jìn)行代碼設(shè)計(jì)。
2. 使用插槽 slot
首先,我們要先將提交按鈕,做成動(dòng)態(tài)的。一開始初始化一個(gè)值,之后呢,可以動(dòng)態(tài)的改變按鈕的文字和事件。那這個(gè)要用到的就是 vue 的中具名插槽 。
我們先在 vue3 項(xiàng)目下的 src|components 定義一個(gè)子組件,命名為 ValidateForm.vue 。現(xiàn)在我們來設(shè)計(jì)它,具體代碼如下:
<template><form class="validate-form-container"><slot name="default"></slot><!-- @click.prevent 用來阻止事件的默認(rèn)行為 --><!-- 阻止表單提交,僅執(zhí)行函數(shù)submitForm --><div class="submit-area" @click.prevent="submitForm"><slot name="submit"><!-- 給插槽添加一個(gè)默認(rèn)按鈕 --><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字段里面確定所要發(fā)送事件的名稱emits: ['form-submit'],setup(props, context) {const submitForm = () => {context.emit('form-submit', true)}return {submitForm}}}) </script>繼續(xù),我們在 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: '密碼不能為空' }]// 創(chuàng)建一個(gè)函數(shù)來監(jiān)聽結(jié)果const onFormSubmit = (result: boolean) => {console.log('1234', result)}return {user: currentUser,emailRules,emailVal,passwordVal,passwordRules,onFormSubmit}} }) </script>對于以上代碼,我們來做個(gè)簡單的分析:
- 子組件通過 emits 來確定要發(fā)送給父組件的事件名稱,之后呢,父組件通過 @事件名稱 的方式來進(jìn)行調(diào)用。
- 使用具名插槽slot,來對提交表單部分進(jìn)行動(dòng)態(tài)控制。子組件使用 slot 進(jìn)行初始化,父組件使用 template 進(jìn)行動(dòng)態(tài)修改。
3. 父子組件通訊
上面我們解決了第 1 點(diǎn),組件需求分析中的前三部分。那么現(xiàn)在,我們來看第四點(diǎn),如何在 ValidateForm 中完成所有 ValidateInput 的驗(yàn)證。
我們先來完善父組件 ValidateForm.vue 的功能。具體代碼如下:
<template><form class="validate-form-container"><slot name="default"></slot><!-- @click.prevent 用來阻止事件的默認(rèn)行為 --><!-- 阻止表單提交,僅執(zhí)行函數(shù)submitForm --><div class="submit-area" @click.prevent="submitForm"><slot name="submit"><!-- 給插槽添加一個(gè)默認(rèn)按鈕 --><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 // 創(chuàng)建一個(gè)事件監(jiān)聽器 export const emitter = mitt()export default defineComponent({name: 'ValidateForm',components: {},// 在emits字段里面確定所要發(fā)送事件的名稱// 注意:只能用全部小寫或者駝峰法emits: ['formSubmit'],setup(props, context) {// 用于存放一系列的函數(shù),執(zhí)行以后可以顯示錯(cuò)誤的信息let funcArr: ValidateFunc[] = []const submitForm = () => {const result = funcArr.map(func => func()).every(result => result)// 將formSubmit時(shí)間進(jìn)行發(fā)送context.emit('formSubmit', result)}// func 即需要接收錯(cuò)誤信息const callback = (func?: ValidateFunc) => {if (func) {funcArr.push(func)}}// 監(jiān)聽器就像是一個(gè)收音機(jī)一樣在等待信息emitter.on('form-item-created', callback)onUnmounted(() => {emitter.off('form-item-created', callback)funcArr = []})return {submitForm}}}) </script>在上面的代碼中,我們使用 mitt 庫創(chuàng)建了一個(gè)事件監(jiān)聽器 emitter ,供給它的子組件 ValidateInput.vue 使用。同時(shí),創(chuàng)建了一個(gè) formSubmit 事件,用于給它的父組件 App.vue 使用。
接著我們來完善子組件 ValidateInput.vue 的功能。具體代碼如下:
<template><div class="validate-input-container pb-3"><!-- 手動(dòng)處理更新和發(fā)送事件 --><!-- 使用可選 class,用于動(dòng)態(tài)計(jì)算類名 --><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)}// 驗(yàn)證輸入框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 的值發(fā)送出去,即發(fā)給給 ValidateForm 組件emitter.emit('form-item-created', validateInput)})return {inputRef,validateInput,updateValue}} }) </script>有了 emitter 之后, ValidateInput 就在慢慢地把它的消息傳去給它的老父親,也就是 ValidateForm 。
最后,我們在 App.vue 中進(jìn)行調(diào)用。具體代碼如下:
<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 () {// 用于拿到組件的實(shí)例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: '密碼不能為空' }]// 創(chuàng)建一個(gè)函數(shù)來監(jiān)聽結(jié)果const onFormSubmit = (result: boolean) => {console.log('result', result) // result true}return {user: currentUser,emailRules,emailVal,passwordVal,passwordRules,onFormSubmit,inputRef}} }) </script>這部分呢,我們成功調(diào)用了 formSubmit 事件,并將其進(jìn)行監(jiān)聽。
好了,到此,我們的表單驗(yàn)證組件設(shè)計(jì)就完成啦!不知道大家是否對這種設(shè)計(jì)思想有了一個(gè)新的認(rèn)識呢?
👁??🗨?四、結(jié)束語
在上面的文章中,我們講到了 Web 世界中的表單元素。從驗(yàn)證輸入框 ValidateInut 的抽象驗(yàn)證規(guī)則,對 v-model 進(jìn)行重新設(shè)計(jì),以及使用 $attrs 來支持默認(rèn)屬性。再到 ValidateForm 的使用具名插槽讓提交按鈕高度自定義化,再到最后的 input 之前的父子組件通訊。
整個(gè)過程細(xì)水長流,但也有很多新的設(shè)計(jì)思想值得我們?nèi)タ:蛯W(xué)習(xí)~
到這里,關(guān)于本文的講解就結(jié)束啦~
如果您覺得這篇文章有幫助到您的的話不妨點(diǎn)贊支持一下喲~~😛
💯 往期推薦
👉前端只是切圖仔?來學(xué)學(xué)給開發(fā)人看的UI設(shè)計(jì)
👉緊跟月影大佬的步伐,一起來學(xué)習(xí)如何寫好JS(上)
👉緊跟月影大佬的步伐,一起來學(xué)習(xí)如何寫好JS(下)
👉組件庫實(shí)戰(zhàn) | 用vue3+ts實(shí)現(xiàn)全局Header和列表數(shù)據(jù)渲染ColumnList
總結(jié)
以上是生活随笔為你收集整理的组件库实战 | 教你如何设计Web世界中的表单验证的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 赛尔号捕捉稀有精灵攻略
- 下一篇: 每天都在红绿灯前面梭行,不如自己来实现个