前言
近期在遷移舊的 Element Plus 專案是必要找到另一款客戶端表單驗證方案,而在使用 Shadcn Vue 過程中發現 VeeValidate 是一款針對 Vue 製作的無頭表單驗證工具,你可以攜帶自己的 UI 整合,使用原生表單元件也不是問題。
- 無頭表單套件(只管驗證邏輯,不綁定 UI),自備 UI 不受現成元件影響
- 與 Vue 元件和 Composition API 深度整合
- Shadcn Vue 有許多封裝好可現成使用的元件與樣式
- 支援 TypeScript 與 Yup、Zod、Valibot
基礎範例
vee-validate 提供兩種驗證表單的方式:
- Higher-order components (HOC)
- Composition API
Component
透過 Field 和 Form 現成元件(預設渲染原生表單 HTML):
<template> <Form :validation-schema="validationSchema" @submit="onSubmit"> <Field name="email" type="email" /> <ErrorMessage name="email" />
<Field name="password" type="password" /> <ErrorMessage name="password" />
<button>Submit</button> </Form></template>
<script setup>import { Form, Field, ErrorMessage } from 'vee-validate';import { toTypedSchema } from '@vee-validate/zod';import * as zod from 'zod';
const validationSchema = toTypedSchema( zod.object({ email: zod.string().min(1, { message: 'This is required' }).email({ message: 'Must be a valid email' }), password: zod.string().min(1, { message: 'This is required' }).min(8, { message: 'Too short' }), }));
function onSubmit(values) { alert(JSON.stringify(values, null, 2));}</script>
我們可以隨時替換元件中的狀態給實際要表現的元件透過 slot porps 傳遞回來顯示於自製的輸入元件:
<template><Field v-model="name" type="text" name="name" v-slot="{ field }"> <Input v-bind="field"></Field><template>
這也是為什麼 Shadcn Vue 表單欄位 包裝來會是如此:
<template> <FormField v-slot="{ componentField }"> <FormItem> <FormLabel>Username</FormLabel> <FormControl> <Input placeholder="shadcn" v-bind="componentField" /> </FormControl> <FormDescription /> <FormMessage /> </FormItem> </FormField></template>
Composition API
也有情況是需要更靈活的表單控制,像是:非同步設置表單初始值,由於 initialValues
只用於在表單初始化當下設置,無法是響應式數值,所以在「多步驟表單」或「需索取非同步資料表單」很常需要使用 useForm
先定義表單,拿到 setValues
方法用於操縱該表單內的數值,像是用於開啟 Dialog 時初始化數值。
<template> <Dialog :open="open" @update:open="$emit('update:open', $event)"> <DialogContent> <DialogHeader class="mb-2"> <DialogTitle>Edit Foo Bar</DialogTitle> <DialogDescription class="sr-only">Edit Foo Bar</DialogDescription> </DialogHeader> <form class="flex flex-col gap-4" id="testSendForm" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="foo"> <FormItem> <FormLabel>Foo</FormLabel> <FormControl> <Input type="text" placeholder="請輸入 foo" v-bind="componentField" /> </FormControl> <FormMessage /> </FormItem> </FormField>
<FormField v-slot="{ componentField }" name="bar"> <FormItem> <FormLabel>Bar</FormLabel> <FormControl> <Input type="text" placeholder="請輸入 bar" v-bind="componentField" /> </FormControl> <FormMessage /> </FormItem> </FormField> </form>
<DialogFooter> <Button :disabled="isPending" form="testSendForm" @click="onSubmit"> {{ isSendTestMailPending ? 'Editing...' : 'Edit' }} </Button> </DialogFooter> </DialogContent> </Dialog></template>
<script setup lang="ts">
const props = defineProps<{ open: boolean; defaultFormValue: DefaultFormValue}>()const emit = defineEmits(['update:open'])const { open } = toRefs(props)const testSendSchema = z.object({ foo: z.string().min(1, '請輸入 Foo'), bar: z.string().min(1, '請輸入 Bar'), })
type TestSendSchema = z.infer<typeof testSendSchema>
const { handleSubmit, setValues } = useForm<TestSendSchema>({ validationSchema: toTypedSchema(testSendSchema),})
const onSubmit = handleSubmit(async (submittedValue: TestSendSchema) => { await sendTestMail({ ...submittedValue, }) emit('update:open', false)})
// 開啟 Dialog 時初始化表單watch(open, (isOpen) => { if (isOpen && props.defaultFormValue) { setValues({ foo: props.defaultFormValue.foo, bar: props.defaultFormValue.bar, }) }})</script>
複雜案例
多步驟表單
官方有 Multi-step Form Wizard 範例,核心概念其實只是把 validation-schema
換成動態數值,根據 step
切換當下要驗證的東西。直接用 Shadcn Vue 實踐好的成果 最快最省事。
<script setup lang="ts">const currentSchema = computed(() => { return schemas[currentStep.value];});</script>
<template><Form @submit="nextStep" :validation-schema="currentSchema" keep-values></template>
動態新增欄位
假設有一個欄位希望可以自由的改變輸入的內容數量,像是可以是填寫 1 個連結或 5 個連結,讓使用者決定。
<FieldArray />
元件用於管理可重複的陣列欄位,它是一個無渲染元件,這意味著它本身不會渲染任何內容:
<template> <Form @submit="onSubmit" :initial-values="initialValues"> <FieldArray name="links" v-slot="{ fields, push, remove }"> <div v-for="(field, idx) in fields" :key="field.key"> <Field :name="`links[${idx}].url`" type="url" />
<button type="button" @click="remove(idx)">Remove</button> </div>
<button type="button" @click="push({ id: Date.now(), name: '', url: '' })">Add</button> </FieldArray>
<button>Submit</button> </Form></template>
<script setup>// you can set initial values for those array fieldsconst initialValues = { links: [{ id: 1, url: 'https://github.com/logaretm' }],};
function onSubmit(values) { alert(JSON.stringify(values, null, 2));}</script>
或是 composition API 實踐方式也有:
<template> <FormField name="ips"> <FormItem> <FormLabel>Allow Source IP</FormLabel> <div class="space-y-2"> <div v-for="(field, index) in ips" :key="field.key" class="flex items-center gap-2"> <FormField v-slot="{ componentField }" :name="`ips[${index}].value`"> <FormItem class="flex-grow"> <div class="flex gap-2"> <FormControl> <Input type="text" v-bind="componentField" placeholder="請輸入 IP 位址" /> </FormControl> <Button type="button" variant="outline" size="icon" @click="removeIp(index)" :disabled="ips.length === 1"> <icon-material-symbols:remove /> </Button> </div> <FormMessage /> </FormItem> </FormField> </div> </div> <Button type="button" variant="outline" size="sm" @click="() => addIp({ value: '' })" :disabled="ips.length >= 5"> <icon-material-symbols:add /> 新增 IP </Button> <FormMessage /> </FormItem> </FormField></template>
<script setup lang="ts">const { fields: ips, push: addIp, remove: removeIp } = useFieldArray<{ value: string }>('ips');
const apiSettingSchema = z.object({ ips: z.array(z.object({ value: z.ipv4({ message: 'IP 格式不正確' }) })).min(1, '至少需要一個 IP 位址'),});</script>
總結
我不是很喜歡翻看這些套件的使用方法,因為這種東西都是用到再翻文件就好,但通常要花一定的時間。這篇文章就像是閱讀文件後的快速筆記,把常見的功能範例和心得記錄下來。