主頁 > 前端設計 > 組件庫實戰 | 教你如何設計Web世界中的表單驗證

組件庫實戰 | 教你如何設計Web世界中的表單驗證

2021-09-20 12:12:36 前端設計

封面

教你如何設計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>
        <input
        type="email" class="form-control" id="exampleEmail1"
        v-model="emailRef.val"
        @blur="validateEmail">
        <div class="form-text" v-if="emailRef.error">{{emailRef.message}}</div>
      </div>
      <div class="mb-3">
        <label for="exampleInputPassword1" class="form-label">密碼</label>
        <input type="password" class="form-control" id="exampleInputPassword1">
      </div>
    </form>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, ref } from 'vue'
import 'bootstrap/dist/css/bootstrap.min.css'
import ColumnList, { ColumnProps } from './components/ColumnList.vue'
import GlobalHeader, { UserProps } from './components/GlobalHeader.vue'
const currentUser: UserProps = {
  isLogin: true,
  name: 'Monday'
}
// 判斷是否是郵箱的格式
const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
const testData: ColumnProps[] = [
  {
    id: 1,
    title: 'test1專欄',
    description: '眾所周知, js 是一門弱型別語言,并且規范較少,這就很容易導致在專案上線之前我們很難發現到它的錯誤,等到專案一上線,渾然不覺地,bug就UpUp了,于是,在過去的這兩年,ts悄悄的崛起了, 本專欄將介紹關于ts的一些學習記錄,'
    // avatar: 'https://img0.baidu.com/it/u=3101694723,748884042&fm=26&fmt=auto&gp=0.jpg'
  },
  {
    id: 2,
    title: 'test2專欄',
    description: '眾所周知, js 是一門弱型別語言,并且規范較少,這就很容易導致在專案上線之前我們很難發現到它的錯誤,等到專案一上線,渾然不覺地,bug就UpUp了,于是,在過去的這兩年,ts悄悄的崛起了, 本專欄將介紹關于ts的一些學習記錄,',
    avatar: 'https://img0.baidu.com/it/u=3101694723,748884042&fm=26&fmt=auto&gp=0.jpg'
  }
]

export default defineComponent({
  name: 'App',
  components: {
    GlobalHeader
  },
  setup () {
	// 郵箱驗證部分資料內容
    const emailRef = reactive({
      val: '',
      error: false,
      message: ''
    })
	// 驗證郵箱邏輯
    const validateEmail = () => {
      // .trim 表示去掉兩邊空格
      // 當郵箱為空時
      if (emailRef.val.trim() === '') {
        emailRef.error = true
        emailRef.message = 'can not be empty'
      } 
       // 當郵箱不為空,但它不是有效的郵箱格式時
       else if (!emailReg.test(emailRef.val)) {
        emailRef.error = true
        emailRef.message = 'should be valid email'
      }
    }
    return {
      list: testData,
      user: currentUser,
      emailRef,
      validateEmail

    }
  }
})
</script>

現在,我們來看下具體的顯示效果:

郵箱驗證

好了,現在我們第一步就實作啦!那么接下來,我們是不是就應該來寫 password 的邏輯了呢?

但是啊,如果按照上面這種方式來寫的話,有小伙伴會不會覺得就有點重復操作了呢,一兩個校驗規則還好,如果我們遇到十幾二十個呢?也一樣每一個都這么寫嗎?

答案當然是否定的,那么下一步,我們就要對這個校驗規則,來進行抽象,

3. 抽象驗證規則

繼續,我們現在要來抽象出用戶名和密碼的校驗規則,讓其可擴展性更強,具體形式如下:

<validate-input :rules="" />

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

首先,我們要先把表單組件給抽離出來,那么現在,我們在 vue3 專案下的 src|components 下創建一個檔案,命名為 ValidateInput.vue其具體代碼如下:

<template>
  <div class="validate-input-container pb-3">
      <!-- 手動處理更新和發送事件 -->
      <!-- 使用可選 class,用于動態計算類名 -->
      <input type="text"
        class="form-control"
        :class="{'is-invalid': inputRef.error}"
        v-model="inputRef.val"
        @blur="validateInput"
      >
      <span v-if="inputRef.error" class="invalid-feedback">{{inputRef.message}}</span>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, PropType } from 'vue'
// 判斷email的正則運算式
const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
// required表示必填值,email表示電子郵件的格式
// message用來展示當出現問題時提示的錯誤
interface RuleProp {
  type: 'required' | 'email';
  message: string;
  validator?: () => boolean;
}

export type RulesProp = RuleProp[]
export default defineComponent({
  name: 'ValidateInput',
  props: {
    // 用PropType來確定rules的型別,明確里面是RulesProp
    // 這里的rules資料將被父組件 App.vue 給進行動態系結
    rules: Array as PropType<RulesProp>
  },
  setup(props, context) {
    //   輸入框的資料
    const inputRef = reactive({
      val: '',
      error: false,
      message: ''
    })
    // 驗證輸入框
    const validateInput = () => {
      if (props.rules) {
        const allPassed = props.rules.every(rule => {
          let passed = true
          inputRef.message = rule.message
          switch (rule.type) {
            case 'required':
              passed = (inputRef.val.trim() !== '')
              break
            case 'email':
              passed = emailReg.test(inputRef.val)
              break
            default:
              break
          }
          return passed
        })
        inputRef.error = !allPassed
      }
    }
    return {
      inputRef,
      validateInput
    }
  }
})
</script>

<style>

</style>

之后我們將其在 App.vue 下進行注冊,具體代碼如下:

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

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

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

現在,我們在瀏覽器來看下它好不好用,具體效果如下:

抽象驗證規則

大家可以看到,經過抽離后的驗證規則,也正確的顯示了最終的驗證效果,課后呢,大家可以繼續對 RuleProptype 進行擴展,比如多多加一個 range 功能等等,

到了這一步,我們對驗證規則已經進行了簡單的抽離,那接下來要做的事情就是,讓父組件 App.vue 可以獲取到子組件 ValidateInput.vueinput 框的值,對其進行資料系結,

4. v-model

說到 input ,大家首先想到的可能是 v-model ,我們先來看下 vue2vue3 在雙向系結方面的區別:

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

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

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

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

對于 vue2 的雙向系結來說,主要有以下槽點:

  • 比較繁瑣,需要新建一個 model 屬性;
  • 不管如何,都只能支持一個 v-model ,沒辦法雙向系結多個值;
  • 寫法比較讓人難以理解,

基于以上 vue2 的幾個槽點,現在我們用 vue3 來對這個組件的 input 值進行系結,手動對其處理更新和事件發送,

首先我們在子組件 ValidateInput.vue 中進行處理,處理資料更新和事件發送,具體代碼如下:

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

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

export type RulesProp = RuleProp[]
export default defineComponent({
  name: 'ValidateInput',
  props: {
    rules: Array as PropType<RulesProp>,
    // 創建一個字串型別的屬性 modelValue
    modelValue: String
  },
  setup(props, context) {
    // 輸入框的資料
    const inputRef = reactive({
      val: props.modelValue || '',
      error: false,
      message: ''
    })
    // KeyboardEvent 即鍵盤輸入事件
    const updateValue = (e: KeyboardEvent) => {
      const targetValue = (e.target as HTMLInputElement).value
      inputRef.val = targetValue
      // 更新值時需要發送事件 update:modelValue
      context.emit('update:modelValue', targetValue)
    }
    const validateInput = () => {
      if (props.rules) {
        const allPassed = props.rules.every(rule => {
          let passed = true
          inputRef.message = rule.message
          switch (rule.type) {
            case 'required':
              passed = (inputRef.val.trim() !== '')
              break
            case 'email':
              passed = emailReg.test(inputRef.val)
              break
            default:
              break
          }
          return passed
        })
        inputRef.error = !allPassed
      }
    }
    return {
      inputRef,
      validateInput,
      updateValue
    }
  }
})
</script>

接下來,我們在 App.vue 中對其進行使用,具體代碼如下:

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

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

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

現在,我們來看下資料的值是否成功被系結,具體效果如下:

v-model系結值

大家可以看到,資料已經直接的被父組件給獲取到并且也成功的系結了,

5. 使用$attrs支持默認屬性

上面我們基本上完成了整個組件的基本功能,現在,我們要來給它設定默認屬性,也就是平常我們使用的 placeholder ,如果我們直接在 <validate-input /> 組件中系結 placeholder ,那么默認地,會直接系結到它的父組件上面去,因此呢,我們要禁止掉這種行為,讓系結后的 placeholder 給相應的放置在 input 元素上,

那這一塊內容呢,涉及到的就是 vue3$attrs$attrs 可以讓組件的根元素不繼承 attribute ,并且可以手動決定這些 attribute 賦予給哪個元素,具體可查看官方檔案:禁用 Attribute 繼承

下面,我們來實作這一塊的功能,

首先是子組件 ValidateInput.vue具體代碼如下:

<template>
  <div class="validate-input-container pb-3">
      <!-- 手動處理更新和發送事件 -->
      <!-- 使用可選 class,用于動態計算類名 -->
      <input
        class="form-control"
        :class="{'is-invalid': inputRef.error}"
        :value="inputRef.val"
        @blur="validateInput"
        @input="updateValue"
        v-bind="$attrs"
      >
      <span v-if="inputRef.error" class="invalid-feedback">{{inputRef.message}}</span>
  </div>
</template>

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

export type RulesProp = RuleProp[]
export default defineComponent({
  name: 'ValidateInput',
  props: {
    rules: Array as PropType<RulesProp>,
    modelValue: String
  },
  // 如果不希望組件的根元素繼承attribute,那么可以在組件的選項中設定以下屬性
  inheritAttrs: false,
  setup(props, context) {
    // 輸入框的資料
    const inputRef = reactive({
      val: props.modelValue || '',
      error: false,
      message: ''
    })
    
    // $attrs包裹著傳遞給組件的attribute的鍵值對
    // console.log(context.attrs)
    
    // KeyboardEvent 即鍵盤輸入事件
    const updateValue = (e: KeyboardEvent) => {
      const targetValue = (e.target as HTMLInputElement).value
      inputRef.val = targetValue
      context.emit('update:modelValue', targetValue)
    }
    // 驗證輸入框
    const validateInput = () => {
      if (props.rules) {
        const allPassed = props.rules.every(rule => {
          let passed = true
          inputRef.message = rule.message
          switch (rule.type) {
            case 'required':
              passed = (inputRef.val.trim() !== '')
              break
            case 'email':
              passed = emailReg.test(inputRef.val)
              break
            default:
              break
          }
          return passed
        })
        inputRef.error = !allPassed
      }
    }
    return {
      inputRef,
      validateInput,
      updateValue
    }
  }
})
</script>

之后是父組件 App.vue具體代碼如下:

<template>
  <div class="container">
    <global-header :user="user"></global-header>
    <form action="">
      <div class="mb-3">
        <label class="form-label">郵箱地址</label>
        <!-- 需要讓placeholder給添加到子組件的input元素上去,而不是添加到根元素上 -->
        <validate-input
        :rules="emailRules" v-model="emailVal"
        placeholder="請輸入郵箱地址"
        type="text" />
      </div>
      <div class="mb-3">
        <label class="form-label">密碼</label>
        <validate-input
          type="password"
          placeholder="請輸入密碼"
          :rules="passwordRules"
          v-model="passwordVal" />
      </div>
    </form>
  </div>
</template>

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

export default defineComponent({
  name: 'App',
  components: {
    GlobalHeader,
    ValidateInput
  },
  setup () {
    const emailVal = ref('')
    const emailRules: RulesProp = [
      { type: 'required', message: '電子郵箱不能為空' },
      { type: 'email', message: '請輸入正確的電子郵箱格式' }
    ]
    const passwordVal = ref('')
    const passwordRules: RulesProp = [
      { type: 'required', message: '密碼不能為空' }
    ]
    return {
      user: currentUser,
      emailRules,
      emailVal,
      passwordVal,
      passwordRules
    }
  }
})
</script>

從上面的代碼中我們可以了解到,通過 inheritAttrs: false$attrs ,實作了我們想要的效果,

我們現在來看下瀏覽器的顯示結果:

使用$sttrs支持默認屬性

💭二、驗證表單ValidateForm

1. 組件需求分析

ValidateInput 除了基本的功能外,還可以進行功能擴散,比如,自定義校驗、更多事件、更多不同的驗證元素,

那么下面,我們要來設計整個驗證表單,也就是 ValidateForm 組件,并且將 ValidateInput 給對應的使用到其中,

我們先來分析下這個 ValidateForm 都有哪些內容,先看下圖:

ValidateForm分析

先看第一部分,我們首先把前面我們封裝的 ValidateInput 給放進去,進行語意化包裹,

第二部分,我們可以對提交的按鈕進行自定義化,比如提交的文字是怎么樣的,提交的按鈕又是怎么樣的,

第三部分,我們需要有一個確定的事件來觸發最后的結果,那么我們就在 ValidateForm 中,獲取最后的結果,

第四部分,算是一個隱藏功能,也是這個組件的一個難點,即獲取每個 ValidateForm 包裹下的 ValidateInput 的驗證結果,

ok,到這里,我們就簡單的對 ValidateForm 進行一個分析,那么下面我們將一步步的來對其進行代碼設計,

2. 使用插槽 slot

首先,我們要先將提交按鈕,做成動態的,一開始初始化一個值,之后呢,可以動態的改變按鈕的文字和事件,那這個要用到的就是 vue 的中具名插槽

我們先在 vue3 專案下的 src|components 定義一個子組件,命名為 ValidateForm.vue ,現在我們來設計它,具體代碼如下:

<template>
    <form class="validate-form-container">
        <slot name="default"></slot>
        <!-- @click.prevent 用來阻止事件的默認行為 -->
        <!-- 阻止表單提交,僅執行函式submitForm -->
        <div class="submit-area" @click.prevent="submitForm">
            <slot name="submit">
                <!-- 給插槽添加一個默認按鈕 -->
                <button type="submit" class="btn btn-primary">提交</button>
            </slot>
        </div>
    </form>
</template>

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

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

  },
  // 在emits欄位里面確定所要發送事件的名稱
  emits: ['form-submit'],
  setup(props, context) {
    const submitForm = () => {
      context.emit('form-submit', true)
    }
    return {
      submitForm
    }
  }

})
</script>

繼續,我們在 App.vue 中使用子組件 ValidateForm.vue具體代碼如下:

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

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

export default defineComponent({
  name: 'App',
  components: {
    // ColumnList,
    GlobalHeader,
    ValidateInput,
    ValidateForm
  },
  setup () {
    const emailVal = ref('')
    const emailRules: RulesProp = [
      { type: 'required', message: '電子郵箱不能為空' },
      { type: 'email', message: '請輸入正確的電子郵箱格式' }
    ]
    const passwordVal = ref('')
    const passwordRules: RulesProp = [
      { type: 'required', message: '密碼不能為空' }
    ]
    // 創建一個函式來監聽結果
    const onFormSubmit = (result: boolean) => {
      console.log('1234', result)
    }
    return {
      user: currentUser,
      emailRules,
      emailVal,
      passwordVal,
      passwordRules,
      onFormSubmit
    }
  }
})
</script>

對于以上代碼,我們來做個簡單的分析:

  • 子組件通過 emits 來確定要發送給父組件的事件名稱,之后呢,父組件通過 @事件名稱 的方式來進行呼叫,
  • 使用具名插槽slot,來對提交表單部分進行動態控制,子組件使用 slot 進行初始化,父組件使用 template 進行動態修改,

3. 父子組件通訊

上面我們解決了第 1 點,組件需求分析中的前三部分,那么現在,我們來看第四點,如何在 ValidateForm 中完成所有 ValidateInput 的驗證,

我們先來完善父組件 ValidateForm.vue 的功能,具體代碼如下:

<template>
    <form class="validate-form-container">
        <slot name="default"></slot>
        <!-- @click.prevent 用來阻止事件的默認行為 -->
        <!-- 阻止表單提交,僅執行函式submitForm -->
        <div class="submit-area" @click.prevent="submitForm">
            <slot name="submit">
                <!-- 給插槽添加一個默認按鈕 -->
                <button type="submit" class="btn btn-primary">提交</button>
            </slot>
        </div>
    </form>
</template>

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

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

  },
  // 在emits欄位里面確定所要發送事件的名稱
  // 注意:只能用全部小寫或者駝峰法
  emits: ['formSubmit'],
  setup(props, context) {
    // 用于存放一系列的函式,執行以后可以顯示錯誤的資訊
    let funcArr: ValidateFunc[] = []
    const submitForm = () => {
      const result = funcArr.map(func => func()).every(result => result)
      // 將formSubmit時間進行發送
      context.emit('formSubmit', result)
    }
    // func 即需要接收錯誤資訊
    const callback = (func?: ValidateFunc) => {
      if (func) {
        funcArr.push(func)
      }
    }
    // 監聽器就像是一個收音機一樣在等待資訊
    emitter.on('form-item-created', callback)
    onUnmounted(() => {
      emitter.off('form-item-created', callback)
      funcArr = []
    })
    return {
      submitForm
    }
  }

})
</script>

在上面的代碼中,我們使用 mitt 庫創建了一個事件監聽器 emitter ,供給它的子組件 ValidateInput.vue 使用,同時,創建了一個 formSubmit 事件,用于給它的父組件 App.vue 使用,

接著我們來完善子組件 ValidateInput.vue 的功能,具體代碼如下:

<template>
  <div class="validate-input-container pb-3">
      <!-- 手動處理更新和發送事件 -->
      <!-- 使用可選 class,用于動態計算類名 -->
      <input
        class="form-control"
        :class="{'is-invalid': inputRef.error}"
        :value="inputRef.val"
        @blur="validateInput"
        @input="updateValue"
        v-bind="$attrs"
      >
      <span v-if="inputRef.error" class="invalid-feedback">{{inputRef.message}}</span>
  </div>
</template>

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

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

有了 emitter 之后, ValidateInput 就在慢慢地把它的訊息傳去給它的老父親,也就是 ValidateForm

最后,我們在 App.vue 中進行呼叫,具體代碼如下:

<template>
  <div class="container">
    <global-header :user="user"></global-header>
    <!-- 將 ValidateForm 中的 formSubmit 事件給傳過來到這里使用 -->
    <validate-form @formSubmit="onFormSubmit">
      <div class="mb-3">
        <label class="form-label">郵箱地址</label>
        <validate-input
        :rules="emailRules" v-model="emailVal"
        placeholder="請輸入郵箱地址"
        type="text"
        ref="inputRef" />
      </div>
      <div class="mb-3">
        <label class="form-label">密碼</label>
        <validate-input
          type="password"
          placeholder="請輸入密碼"
          :rules="passwordRules"
          v-model="passwordVal" />
      </div>
      <template #submit>
        <span class="btn btn-danger">Submit</span>
      </template>
    </validate-form>
  </div>
</template>

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

export default defineComponent({
  name: 'App',
  components: {
    // ColumnList,
    GlobalHeader,
    ValidateInput,
    ValidateForm
  },
  setup () {
    // 用于拿到組件的實體
    const inputRef = ref<any>()
    const emailVal = ref('')
    const emailRules: RulesProp = [
      { type: 'required', message: '電子郵箱不能為空' },
      { type: 'email', message: '請輸入正確的電子郵箱格式' }
    ]
    const passwordVal = ref('')
    const passwordRules: RulesProp = [
      { type: 'required', message: '密碼不能為空' }
    ]
    // 創建一個函式來監聽結果
    const onFormSubmit = (result: boolean) => {
      console.log('result', result) // result true
    }
    return {
      user: currentUser,
      emailRules,
      emailVal,
      passwordVal,
      passwordRules,
      onFormSubmit,
      inputRef
    }
  }
})
</script>

這部分呢,我們成功呼叫了 formSubmit 事件,并將其進行監聽,

好了,到此,我們的表單驗證組件設計就完成啦!不知道大家是否對這種設計思想有了一個新的認識呢?

👁??🗨?四、結束語

在上面的文章中,我們講到了 Web 世界中的表單元素,從驗證輸入框 ValidateInut 的抽象驗證規則,對 v-model 進行重新設計,以及使用 $attrs 來支持默認屬性,再到 ValidateForm 的使用具名插槽讓提交按鈕高度自定義化,再到最后的 input 之前的父子組件通訊,

整個程序細水長流,但也有很多新的設計思想值得我們去楷模和學習~

到這里,關于本文的講解就結束啦~

如果您覺得這篇文章有幫助到您的的話不妨點贊支持一下喲~~😛

💯 往期推薦

👉前端只是切圖仔?來學學給開發人看的UI設計

👉緊跟月影大佬的步伐,一起來學習如何寫好JS(上)

👉緊跟月影大佬的步伐,一起來學習如何寫好JS(下)

👉組件庫實戰 | 用vue3+ts實作全域Header和串列資料渲染ColumnList

轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/301548.html

標籤:其他

上一篇:vue系列:Vue核心概念及特性 (一)

下一篇:javascript 陣列排序(三種方法)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • vue移動端上拉加載

    可能做得過于簡單或者比較low,請各位大佬留情,一起探討技術 ......

    uj5u.com 2020-09-10 04:38:07 more
  • 優美網站首頁,頂部多層導航

    一個個人用的瀏覽器首頁,可以把一下常用的網站放在這里,平常打開會比較方便。 第一步,HTML代碼 <script src=https://www.cnblogs.com/szharf/p/"js/jquery-3.4.1.min.js"></script> <div id="navigate"> <ul> <li class="labels labels_1"> ......

    uj5u.com 2020-09-10 04:38:47 more
  • 頁面為要加<!DOCTYPE html>

    最近因為寫一個js函式,需要用到$(window).height(); 由于手寫demo的時候,過于自信,其實對前端方面的認識也不夠體系,用文本檔案直接敲出來的html代碼,第一行沒有加上<!DOCTYPE html> 導致了$(window).height();的結果直接是整個document的高 ......

    uj5u.com 2020-09-10 04:38:52 more
  • WordPress網站程式手動升級要做好資料備份

    WordPress博客網站程式在進行升級前,必須要做好網站資料的備份,這個問題良家佐言是遇見過的;在剛開始接觸WordPress博客程式的時候,因為升級問題和博客網站的修改的一些嘗試,良家佐言是吃盡了苦頭。因為購買的是西部數碼的空間和域名,每當佐言把自己的WordPress博客網站搞到一塌糊涂的時候 ......

    uj5u.com 2020-09-10 04:39:30 more
  • WordPress程式不能升級為5.4.2版本的原因

    WordPress是一款個人博客系統,受到英文博客愛好者和中文博客愛好者的追捧,并逐步演化成一款內容管理系統軟體;它是使用PHP語言和MySQL資料庫開發的,用戶可以在支持PHP和MySQL資料庫的服務器上使用自己的博客。每一次WordPress程式的更新,就會牽動無數WordPress愛好者的心, ......

    uj5u.com 2020-09-10 04:39:49 more
  • 使用CSS3的偽元素進行首字母下沉和首行改變樣式

    網頁中常見的一種效果,首字改變樣式或者首行改變樣式,效果如下圖。 代碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, ......

    uj5u.com 2020-09-10 04:40:09 more
  • 關于a標簽的講解

    什么是a標簽? <a> 標簽定義超鏈接,用于從一個頁面鏈接到另一個頁面。 <a> 元素最重要的屬性是 href 屬性,它指定鏈接的目標。 a標簽的語法格式:<a href=https://www.cnblogs.com/summerxbc/p/"指定要跳轉的目標界面的鏈接">需要展示給用戶看見的內容</a> a標簽 在所有瀏覽器中,鏈接的默認外觀如下: 未被訪問的鏈接帶 ......

    uj5u.com 2020-09-10 04:40:11 more
  • 前端輪播圖

    在需要輪播的頁面是引入swiper.min.js和swiper.min.css swiper.min.js地址: 鏈接:https://pan.baidu.com/s/15Uh516YHa4CV3X-RyjEIWw 提取碼:4aks swiper.min.css地址 鏈接:https://pan.b ......

    uj5u.com 2020-09-10 04:40:13 more
  • 如何設定html中的背景圖片(全屏顯示,且不拉伸)

    1 <style>2 body{background-image:url(https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture); 3 background-size:cover;background ......

    uj5u.com 2020-09-10 04:40:16 more
  • Java學習——HTML詳解(上)

    HTML詳解 初識HTML Hyper Text Markup Language(超文本標記語言) 1 <!--DOCTYPE:告訴瀏覽器我們要使用什么規范--> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <!--meta 描述性的標簽,描述一些 ......

    uj5u.com 2020-09-10 04:40:33 more
最新发布
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 07:59:23 more
  • 生產事故-走近科學之消失的JWT

    入職多年,面對生產環境,盡管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背后,都是寶貴的經驗和教訓,都是專案成員的血淚史。為了更好地防范和遏制今后的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授 ......

    uj5u.com 2023-04-18 07:55:04 more
  • 記錄--Canvas實作打飛字游戲

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 打開游戲界面,看到一個畫面簡潔、卻又富有挑戰性的游戲。螢屏上,有一個白色的矩形框,里面不斷下落著各種單詞,而我需要迅速地輸入這些單詞。如果我輸入的單詞與螢屏上的單詞匹配,那么我就可以獲得得分;如果我輸入的單詞錯誤或者時間過長,那么我就會輸 ......

    uj5u.com 2023-04-04 08:35:30 more
  • 了解 HTTP 看這一篇就夠

    在學習網路之前,了解它的歷史能夠幫助我們明白為何它會發展為如今這個樣子,引發探究網路的興趣。下面的這張圖片就展示了“互聯網”誕生至今的發展歷程。 ......

    uj5u.com 2023-03-16 11:00:15 more
  • 藍牙-低功耗中心設備

    //11.開啟藍牙配接器 openBluetoothAdapter //21.開始搜索藍牙設備 startBluetoothDevicesDiscovery //31.開啟監聽搜索藍牙設備 onBluetoothDeviceFound //30.停止監聽搜索藍牙設備 offBluetoothDevi ......

    uj5u.com 2023-03-15 09:06:45 more
  • canvas畫板(滑鼠和觸摸)

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>canves</title> <style> #canvas { cursor:url(../images/pen.png),crosshair; } #canvasdiv{ bo ......

    uj5u.com 2023-02-15 08:56:31 more
  • 手機端H5 實作自定義拍照界面

    手機端 H5 實作自定義拍照界面也可以使用 MediaDevices API 和 <video> 標簽來實作,和在桌面端做法基本一致。 首先,使用 MediaDevices.getUserMedia() 方法獲取攝像頭媒體流,并將其傳遞給 <video> 標簽進行渲染。 接著,使用 HTML 的 < ......

    uj5u.com 2023-01-12 07:58:22 more
  • 記錄--短視頻滑動播放在 H5 下的實作

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 短視頻已經無數不在了,但是主體還是使用 app 來承載的。本文講述 H5 如何實作 app 的視頻滑動體驗。 無聲勝有聲,一圖頂百辯,且看下圖: 網址鏈接(需在微信或者手Q中瀏覽) 從上圖可以看到,我們主要實作的功能也是本文要講解的有: ......

    uj5u.com 2023-01-04 07:29:05 more
  • 一文讀懂 HTTP/1 HTTP/2 HTTP/3

    從 1989 年萬維網(www)誕生,HTTP(HyperText Transfer Protocol)經歷了眾多版本迭代,WebSocket 也在期間萌芽。1991 年 HTTP0.9 被發明。1996 年出現了 HTTP1.0。2015 年 HTTP2 正式發布。2020 年 HTTP3 或能正... ......

    uj5u.com 2022-12-24 06:56:02 more
  • 【HTML基礎篇002】HTML之form表單超詳解

    ??一、form表單是什么

    ??二、form表單的屬性

    ??三、input中的各種Type屬性值

    ??四、標簽 ......

    uj5u.com 2022-12-18 07:17:06 more