前言
最近在迁移旧的 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>提交</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: '此项为必填项' }).email({ message: '必须是有效的电子邮箱' }), password: zod.string().min(1, { message: '此项为必填项' }).min(8, { message: '长度太短' }), }));
function onSubmit(values) { alert(JSON.stringify(values, null, 2));}</script>我们可以随时替换组件中的状态,并通过 slot props 将其传递回来,显示在自定义输入组件中:
<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>用户名</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>编辑 Foo Bar</DialogTitle> <DialogDescription class="sr-only">编辑 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 ? '正在编辑...' : '编辑' }} </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)">删除</button> </div>
<button type="button" @click="push({ id: Date.now(), name: '', url: '' })">添加</button> </FieldArray>
<button>提交</button> </Form></template>
<script setup>// 可以为这些数组字段设置初始值const 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>允许的源 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>总结
我不太喜欢反复查阅这些库的使用方法,因为这类内容通常等到实际用到时再查文档就行,但往往仍需要花费一些时间。这篇文章就像是阅读文档后整理的速查笔记,记录了常用的功能示例和使用心得。