tags: Vue3 library Form VeeValidate 創建時間:2024年03月17日

在 Vue3 使用 VeeValidate 讓表單驗證更 easy

為何要使用表單驗證 library VeeValidate #

在前端專案中,常常會需要處理 Form 相關的內容,單純 HTML 原生提供的驗證功能往往難以處理諸多情境:驗證必填一項的 checkbox、客製化錯誤樣式、表單陣列結構資料驗證等,在這種情況下,使用專門處理表單驗證的 library 就能幫助我們加速表單驗證功能的開發,以及處理複雜的表單驗證行為(如非同步驗證),今天介紹 VeeValidate 這套表單驗證的 library 就是一套 Vue 生態系統中,具備諸多功能的表單驗證工具,可以透過它來加速 Form 相關功能開發的速度,讓我們可以有更多的時間處理開發中的商業邏輯。

Vue3 只有一套表單驗證工具嗎?

Vue3 生態系統目前有多套表單驗證工具,較多下載量的有 FormKit、VeeValidate,其中 Formkit 提供含驗證功能的表單組件讓人使用,而 VeeValidate 除了基礎 Form HOC,也提供 Composition API 讓人可以與既有的組件結合使用。

選用 VeeValidate 的優點在哪?

在筆者目前遇到的開發情境中,往往已有既定使用的 component library,這時候若是要引入表單驗證 library,VeeValidate 提供了單純的驗證功能,讓我們可以很方便的將既有 Form component 添加上驗證。

另外截止至 2024.03 VeeValidate 的下載量相比其他 Vue3 生態系統的 Form 驗證工具也具備更高下載量,更多的用戶往往也代表了更完善的社區資源。

圖片出自 npm trends

如何開始在 Vue 專案中使用 VeeValidate #

要在專案中使用 VeeValidate,只需要使用其提供的 Composition API useFormuseField 就可以,以下我們可以從範例看起,下方是一個簡單表單,並添加了 VeeValidate 的驗證:

以下表單包含了一個名為 "name" 的欄位,並設置了相應的驗證規則,確保使用者輸入的名稱不為空。

<script setup>
import { useForm } from 'vee-validate'
import * as yup from 'yup'

// 步驟二之二:表單驗證規則
const validationSchema = yup.object({
name: yup.string().required(),
})

// 步驟二之一:使用 useForm 建立表單驗證 form context,並設置表單初始狀態、驗證規則
const { handleSubmit, defineField, errors } = useForm({
validationSchema,
initialValues: {
name: '',
},
})

// 步驟三:定義表單欄位,從中取得該欄位的 ref 值
const [name] = defineField('name')

// 步驟五:使用從 useForm 取得的 handleSubmit 來建立 submit 處理函式,透過該方法建立的處理函式會在驗證成功後才會執行
const onSubmit = handleSubmit((values, { resetForm }) => {
console.log('success submit')
resetForm()
})
</script>

<template>

<!--步驟一:建立表單-->
<!--步驟六:添加表單 submit 處理器,並不需要 prevent 因為 VeeValidate 會幫我們處理-->
<form @submit="onSubmit">
<div class="form-item">
<p class="form-title">name</p>
<!--步驟四:將欄位 name 的 ref 數值綁定到 input-->
<input v-model="name"></input>
<p class="form-error">{{ errors.name }}</p>
</div>
<div>
<button>submit</button>
</div>
</form>

</template>

我們可以看到使用 VeeValidate 經歷的步驟:

  1. 建立表單
  2. 使用 useForm
    1. 建立表單驗證 form context,並設置表單初始狀態、驗證規則
    2. 定義表單驗證規則
  3. 定義表單欄位,並使用 useForm 提供的 defineField,從中取得該欄位的 ref注意 VeeValidate 規定不能直接更動從 useForm 取得的 values
  4. 將欄位 name 的 ref 數值綁定到 input
  5. 使用從 useForm 取得的 handleSubmit 來建立 submit 處理函式,透過該方法建立的處理函式會在驗證成功後才會執行
  6. 添加表單 submit 處理器,並不需要 prevent 因為 VeeValidate handleSubmit 會幫我們處理

按照上述步驟,即可快速的使用 VeeValidate 在既有表單中建立表單驗證。

VeeValidate 核心的 composition API 為 useFormuseField ,以下將講解兩者分別做了什麼

useForm #

將當前組件視作 Form,並提供該 Form 的驗證功能、及表單驗證的 meta 值,該 API 內部會建立一個 form context,該 context 將注入 inject 該組件的所有後代子組件,子組件可以使用該 form context。

基礎使用方法如下:

import { useForm } from 'vee-validate' const { errors, setErrors, setFieldValue } = useForm({
initialValues: { email: '', password: '', }, })

useField #

提供單一表單欄位的驗證功能,沒有 useForm 時也能獨立使用。但大多數的使用情境是和 useForm 一起,useForm 會自動偵測內部所有使用 useField 的子組件,並使用同一個 form context,在巢狀子組件中很常使用,讓我們不用經歷多層的 propsemit

基礎使用方法如下:

<script setup>
import { useField } from 'vee-validate'
import * as yup from 'yup'

const { value: name, errorMessage } = useField('name', yup.string().min(6))
</script>

<template>

<div>
<input type="text" v-model="name" />
<p>{{ errorMessage }}</p>
</div>

</template>

<style scoped></style>

補充

VeeValidate 主要分成 HOC 組件、 composition API 兩種使用模式,其中 HOC 底層也使用 composition API,HOC 模式適合簡單的表單使用情境,而在實際開發時若是已有一套既有的組件庫,則更適合使用 composition API 模式

如何使用 VeeValidate 搭配 yup 驗證表單 #

剛剛的範例中我們可以看到驗證時使用了 yupyup 是一套定義驗證規則的 npm library,以下將建立一個較為複雜的表單範例,讓我們了解如何使用 yup 配合 VeeValidate:

這個表單包含了以下欄位以及對應的驗證:

  • Text: 使用者需輸入文字,長度在 6 到 18 個字元之間。
  • Count: 使用者需輸入數字,範圍在 1 到 10 之間。
  • Date: 使用者需選擇日期。
  • Select: 使用者需選擇其中一個選項。
  • Multiple Select: 使用者需選擇至少兩個選項。
<script setup>
import { useForm } from 'vee-validate'
import * as yup from 'yup'

const { errors, defineField, handleSubmit } = useForm({
initialValues: { text: '', count: 0, date: '', select: '', multipleSelect: [] },
validationSchema: yup.object({
text: yup.string().min(6).max(18).required(),
count: yup.number().min(1).max(10).required(),
date: yup.date().required(),
select: yup.string().required(),
multipleSelect: yup.array().of(yup.string()).min(2).required(),
}),
})

const [text] = defineField('text')
const [count] = defineField('count')
const [date] = defineField('date')
const [select] = defineField('select')
const [multipleSelect] = defineField('multipleSelect')

const onSubmit = handleSubmit((data, { resetForm }) => {
console.log('success', data)
resetForm()
})
</script>

<template>

<form @submit="onSubmit">
<div>
<label for="text">text</label>
<input type="text" id="text" v-model="text" />
<p style="color: red">{{ errors.text }}</p>
</div>
<div>
<label for="count">count</label>
<input type="number" name="count" id="count" v-model.number="count" />
<p style="color: red">{{ errors.count }}</p>
</div>
<div>
<label for="date">date</label>
<input type="date" name="date" id="date" v-model="date" />
<p style="color: red">{{ errors.date }}</p>
</div>
<div>
<p>select</p>
<div>
<label for="a">
<input type="radio" name="select" id="a" v-model="select" />
<span>a</span>
</label>
</div>
<div>
<label for="b">
<input type="radio" name="select" id="b" v-model="select" />
<span>b</span>
</label>
</div>
<p style="color: red">{{ errors.select }}</p>
</div>
<div>
<label for="multiple-select"></label>
<select name="multiple-select" id="multiple-select" multiple v-model="multipleSelect">
<option value="aa">aa</option>
<option value="bb">bb</option>
<option value="cc">cc</option>
<option value="dd">dd</option>
</select>
<p style="color: red">{{ errors.multipleSelect }}</p>
</div>
<div>
<button>submit</button>
</div>
</form>

</template>

<style scoped></style>

yup 提供了 string、array、number、object、tuple、boolean、date 的驗證,可以依照需求定義,上述的定義驗證分別為:

  • text: yup.string().min(6).max(18).required() 驗證文字、必填、長度 6 - 8 之間
  • count: yup.number().min(1).max(10).required() 驗證數字、必填、位於 1 - 10 之間
  • date: yup.date().required() 驗證日期,可填入 ISO 字串、必填
  • select: yup.string().required() 驗證單選,文字、必填
  • multipleSelect: yup.array().of(yup.string()).min(2).required() 驗證多選,陣列、必填、至少 2 項、且須符合 of 設置的結構

有更多驗證需求可查詢 yup doc yup - npm

如何使用 VeeValidate 驗證多層嵌套表單組件 #

在實際專案時,時常會遇到多層表單組件,這時候要使用 VeeValidate 的 useFormuseField 搭配來驗證,以下為巢狀表單範例,表單主體為 NestForm、深層組件為 NestSubForm

這個表單包含了以下欄位以及對應的驗證:

  • name: 使用者需輸入文字,必填。
  • detail: 是一個巢狀物件,驗證結構如下
    • info: 必填字串。
    • count: 必填數字。

NestForm

<script setup>
import { useForm } from 'vee-validate'
import NextFormSubItem from './NextFormSubItem.vue'
import * as yup from 'yup'

const validationSchema = yup.object({
name: yup.string().required(),
detail: yup.object({
info: yup.string().required(),
count: yup.number().min(1).required(),
}),
})

const { handleSubmit, defineField, errors } = useForm({
validationSchema,
initialValues: {
name: '',
detail: {
info: '',
count: 0,
},
},
})

const [name] = defineField('name')

const onSubmit = handleSubmit((values, { resetForm }) => {
console.log('success')
})
</script>

<template>

<form @submit.prevent="onSubmit">
<div class="form-item">
<p class="form-title">name</p>
<input type="text" v-model="name"></input>
<p class="form-error">{{ errors.name }}</p>
</div>
<NextFormSubItem></NextFormSubItem>
<div>
<button>submit</button>
</div>
</form>

</template>

NestSubForm

<script setup>
import { useField } from 'vee-validate'

const { value: info, errorMessage: infoErrorMessage } = useField('detail.info')
const { value: count, errorMessage: countErrorMessage } = useField('detail.count')
</script>

<template>

<div>
<div class="form-item">
<p class="form-title">detail.info</p>
<input type="text" v-model="info"></input>
<p class="form-error">{{ infoErrorMessage }}</p>
</div>
<div class="form-item">
<p class="form-title">detail.count</p>
<input
type="number"
v-model.number="count"
>

<p class="form-error">{{ countErrorMessage }}</p>
</div>
</div>

</template>

在以上範例中,內層子組件的 useField 會和 useForm 建立的 form context 處在同一 context 中,故在 NestForm 中進行驗證,會針對所有內部的 useFielddefineField 的表單欄位進行驗證。由於 defineField 只能在 useForm 層使用,內部子組件表單欄位需要使用 useField 幫忙設置。

如何使用 VeeValidate 搭配外部 form 組件 #

由於 VeeValidate composition API 的 useFielddefineField 可以讓我們直接設置表單欄位驗證規則,以及取得表單欄位的響應 ref,所以搭配外部組件基本上只需要將 ref 數值傳入即可使用,以下是一個基礎範例

這個表單包含了以下欄位和對應的驗證:

  • name: 必填欄位,輸入使用者的名字。
  • family: 必填欄位,輸入使用者家庭的成員人數,範圍在 1 以上。
  • hobbits: 必填欄位,複選,使用者可以選擇多個興趣,包括游泳、跑步、健走。
  • sex: 必填欄位,單選,使用者可以選擇女性或男性。
  • aboard: 必填欄位,使用者可以勾選是否曾有出國。
  • education: 必填欄位,下拉選單,使用者可以選擇教育程度,包括小學、中學、大學。
  • born: 必填欄位,使用者可以選擇生日日期。
<script setup>
import StyleCheckbox from './StyleCheckbox.vue'
import StyleCheckboxGroup from './StyleCheckboxGroup.vue'
import StyleInputNumber from './StyleInputNumber.vue'
import StyleInputDate from './StyleInputDate.vue'
import StyleRadioGroup from './StyleRadioGroup.vue'
import StyleInput from './StyleInput.vue'
import StyleSelector from './StyleSelector.vue'
import { useForm } from 'vee-validate'
import * as yup from 'yup'

const validationSchema = yup.object({
name: yup.string().required(),
born: yup.date().required(),
family: yup.number().required().min(1),
hobbits: yup.array().min(1).required(),
sex: yup.string().required(),
education: yup.string().required(),
aboard: yup.boolean().required(),
})

const emit = defineEmits(['submitForm'])

const { handleSubmit, defineField, errors } = useForm({
validationSchema,
initialValues: {
name: '',
born: '',
family: 0,
hobbits: [],
sex: '',
education: '',
aboard: false,
},
})

const [name] = defineField('name')
const [hobbits] = defineField('hobbits')
const [aboard] = defineField('aboard')
const [sex] = defineField('sex')
const [family] = defineField('family')
const [education] = defineField('education')
const [born] = defineField('born')

const onSubmit = handleSubmit((value) => emit('submitForm', value))
</script>

<template>

<form @submit.prevent="onSubmit">
<div class="form-item">
<p class="form-title">姓名</p>
<StyleInput v-model="name"></StyleInput>
<p class="form-error">
{{ errors.name }}</p>
</div>
<div class="form-item">
<p class="form-title">家庭成員人數</p>
<StyleInputNumber
v-model="family"
@increase="() => (family += 1)"
@decrease="() => (family -= 1)"
>
</StyleInputNumber>
<p class="form-error">
{{ errors.family }}</p>
</div>
<div class="form-item">
<p class="form-title">興趣</p>
<StyleCheckboxGroup
v-model="hobbits"
:items="[
{ name: '游泳', value: 'swim' },
{ name: '跑步', value: 'run' },
{ name: '健走', value: 'camp' },
]"
></StyleCheckboxGroup>
<p class="form-error">
{{ errors.hobbits }}</p>
</div>
<div class="form-item">
<p class="form-title">性別</p>
<StyleRadioGroup
v-model="sex"
:items="[
{ name: '女', value: 'woman' },
{ name: '男', value: 'man' },
]"
></StyleRadioGroup>
<p class="form-error">
{{ errors.sex }}</p>
</div>
<div class="form-item">
<p class="form-title">是否曾有出國</p>
<StyleCheckbox v-model="aboard"> 是 </StyleCheckbox>
<p class="form-error">
{{ errors.aboard }}</p>
</div>
<div class="form-item">
<p class="form-title">教育程度</p>
<StyleSelector
v-model="education"
:items="[
{ name: '小學', value: '1' },
{ name: '中學', value: '2' },
{ name: '大學', value: '3' },
]"
>
</StyleSelector>
<p class="form-error">
{{ errors.education }}</p>
</div>
<div class="form-item">
<p class="form-title">生日</p>
<StyleInputDate v-model="born"></StyleInputDate>
<p class="form-error">
{{ errors.born }}</p>
</div>
<div class="form-item">
<button type="submit">submit</button>
</div>
</form>

</template>

<style scoped>
.form-item
{
position: relative;
margin-bottom: 12px;
}

.form-title {
color: #606266;
font-size: 14px;
margin-bottom: 4px;
}

.form-error {
position: absolute;
color: lightcoral;
font-size: 12px;
bottom: -2px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
}
</style>

如何使用 VeeValidate 對欄位進行非同步驗證 #

使用 VeeValidate 驗證非同步表單方式如下,以下配合 yup 進行撰寫

這個表單包含以下欄位:

  • text: 必填欄位,使用者輸入文字。
  • async-text: 必填欄位,具有異步驗證功能,使用者輸入文字後,會執行異步驗證,確保輸入的文字符合特定條件。
<script setup>
import { useForm } from 'vee-validate'
import * as yup from 'yup'

const { handleSubmit, errors, defineField, meta } = useForm({
initialValues: {
text: '',
asyncText: '',
},
validationSchema: yup.object({
text: yup.string().required(),
asyncText: yup
.string()
.required()
.test('async-text', 'do not pass async validate', fakeApi),
}),
})

const [text] = defineField('text')
const [asyncText] = defineField('asyncText')

const onSubmit = handleSubmit(() => {
console.log('success')
})

function fakeApi(text) {
return new Promise((resolve) => {
setTimeout(() => {
if (!text.includes('good')) {
resolve(false)
}
return resolve(true)
}, 1 * 1000)
})
}
</script>

<template>

<form @submit="onSubmit">
<div>
<label for="text">text</label>
<input type="text" v-model="text" />
<p style="color: red">{{ errors.text }}</p>
</div>
<div>
<label for="async-text">async-text</label>
<input type="text" v-model="asyncText" id="async-text" />
<p style="color: red">{{ errors.asyncText }}</p>
</div>
<div>
<button>submit</button>
</div>
</form>

</template>

<style scoped></style>

如何使用 VeeValidate 驗證陣列型表單欄位 #

這邊可以使用 VeeValidate 提供的專門給 Array 欄位的 useFieldArray,它提供了專門給陣列行表單資料結構的驗證,也提供新增、刪除特定欄位的功能,能滿足大部分對於該種表單資料類型的需求。

這個表單包含以下欄位:

  • name: 廣告名稱,必填欄位,使用者輸入文字。
  • person: 一個動態新增、刪除的欄位列表,每個項目包含以下欄位:
    • name: 審核人員名稱,必填欄位,使用者輸入文字。
    • email: 審核人員電子郵件,必填欄位,使用者輸入有效的電子郵件地址。
<script setup>
import { useForm, useFieldArray } from 'vee-validate'
import * as yup from 'yup'

const { defineField, handleSubmit, errors, resetForm, submitCount } = useForm({
initialValues: {
name: '',
person: [{ email: '', name: '' }],
},
validationSchema: yup.object({
name: yup.string().required(),
person: yup
.array()
.of(
yup.object().shape({
name: yup.string().required(),
email: yup.string().email().required(),
})
)
.min(1),
}),
})

const [name] = defineField('name')
const { remove, push, fields } = useFieldArray('person')

function addPerson() {
push({ name: '', email: '' })
}

function deletePerson(index) {
remove(index)
}

const onSubmit = handleSubmit(() => {
console.log('success')
})
</script>

<template>

<form @submit="onSubmit">
<div>
<label for="name">廣告名稱</label>
<input type="text" v-model="name" />
<p style="color: red">{{ errors.name }}</p>
</div>
<div>
<p>審核人員</p>
<ul>
<li style="border: 1px solid gray" v-for="(field, idx) in fields" :key="field.key">
<input type="text" v-model="field.value.name" />
<p style="color: red" v-if="submitCount > 0">
{{ errors["person[" + idx + "].name"] }}
</p>
<input type="text" v-model="field.value.email" />
<p style="color: red" v-if="submitCount > 0">
{{ errors["person[" + idx + "].email"]}}
</p>
<button @click="() => deletePerson(index)">delete</button>
</li>
</ul>
<button type="button" @click="addPerson">add</button>
</div>
<div>
<button>submit</button>
</div>
</form>

</template>

<style scoped></style>

注意:目前 useFieldArray 會在使用者為輸入值直接進行初次驗證,所以使用上會看到載入時即有驗證錯誤,這是因為 useFieldArray 無法像 useField 一樣感測用戶輸入的行為,作者也有在 ISSUE 提到,後續可能會施作相關功能補充,目前希望不要在使用者未輸入前展示錯誤的解決方法,我是採用添加 submitCount 判定,不過缺點是這無法在用戶尚未送出表單前對輸入值更動即時驗證。

多步驟表單驗證 #

如果有多步驟表單,通常多步驟表單會處於同一個 Form 內部,依照步驟顯示不同步驟的 Field 內容,此時若是要驗證,每一個步驟組件可以使用 useValidateField 這個 API 來驗證該步驟表單 Field。

這個表單包含以下欄位:

  • name: 名稱,必填欄位,使用者輸入文字。
  • sex: 性別,必填欄位,使用者輸入文字。
  • age: 年齡,必填欄位,使用者輸入數字。

多步驟總表單

<script setup>
import { useForm } from 'vee-validate'
import * as yup from 'yup'
import { ref } from 'vue'
import StepOne from './StepOne.vue'
import StepTwo from './StepTwo.vue'

const currentStep = ref(1)

const { handleSubmit } = useForm({
initialValues: { name: '', sex: '', age: 0 },
validationSchema: yup.object({
name: yup.string().min(1).required(),
age: yup.number().min(1).max(150).required(),
sex: yup.string().required(),
}),
validateOnMount: true,
})

const onSubmit = handleSubmit(
(data, { resetForm }) => {
console.log('success', data)
resetForm()
},
() => {
console.log('error')
}
)
</script>

<template>
<form @submit="onSubmit">
<StepOne v-show="currentStep === 1" @next-step="currentStep += 1"> </StepOne>
<StepTwo v-show="currentStep === 2" @pre-step="currentStep -= 1"> </StepTwo>
</form>
</template>

<style scoped></style>

步驟一組件

<script setup>
import { useField, useValidateField } from 'vee-validate'

const emits = defineEmits(['next-step'])

const validate = useValidateField('name')
const { value: name } = useField('name')

const onNextStep = async () => {
const result = await validate()
if (result.valid) {
emits('next-step')
return
} else console.log(result.errors)
}
</script>

<template>
<div>
<p>step 1</p>
<input v-model="name" type="text" name="name" />
<button @click="onNextStep" type="button">next</button>
</div>
</template>

<style scoped></style>

步驟二組件

<script setup>
import { useField } from 'vee-validate'

const emits = defineEmits('pre-step')

const { value: sex } = useField('sex')
const { value: age } = useField('age')

const onPrevStep = async () => {
emits('pre-step')
}
</script>

<template>
<div>
<p>step 2</p>
<input v-model="sex" type="text" name="sex" />
<input v-model="age" type="number" name="age" />
<button @click="onPrevStep" type="button">pre</button>
<button type="submit">submit</button>
</div>
</template>

<style scoped></style>

結語 #

在前端開發中,表單驗證是一個常見但重要的任務。透過使用 VeeValidate,我們可以更有效地處理表單驗證,加速開發流程並提高應用程式的品質。本文介紹了 VeeValidate 的基本使用方法,包括如何在 Vue 專案中集成和配置,以及如何應對各種驗證需求,包括非同步驗證和陣列型表單欄位的驗證。藉助 VeeValidate 提供的豐富功能,我們可以更輕鬆地開發出具有完善表單驗證功能的前端應用程序,提升用戶體驗和開發效率。有任何問題也歡迎和筆者討論交流歐 ʕ•ᴥ•ʔ。

參考資料 #


tags: Vue3 library Form VeeValidate 創建時間:2024年03月17日


下一篇 ...  初探 Node.js:認識 Node.js