- 手风琴 (Accordion)
- 警告 (Alert)
- 警告对话框 (Alert Dialog)
- 宽高比 (Aspect Ratio)
- 头像 (Avatar)
- 徽章 (Badge)
- 面包屑 (Breadcrumb)
- 按钮 (Button)
- 按钮组 (Button Group)
- 日历 (Calendar)
- 卡片 (Card)
- 轮播图 (Carousel)
- 图表 (Chart)
- 复选框 (Checkbox)
- 折叠面板 (Collapsible)
- 组合框 (Combobox)
- 命令面板 (Command)
- 上下文菜单 (Context Menu)
- 数据表格 (Data Table)
- 日期选择器 (Date Picker)
- 对话框 (Dialog)
- 抽屉 (Drawer)
- 下拉菜单 (Dropdown Menu)
- 空状态 (Empty)
- 字段 (Field)
- 表单 (Form)
- 悬停卡片 (Hover Card)
- 输入框 (Input)
- 输入组 (Input Group)
- 验证码输入 (Input OTP)
- 项 (Item)
- 键盘按键 (Kbd)
- 标签 (Label)
- 菜单栏 (Menubar)
- 原生选择器 (Native Select)
- 导航菜单 (Navigation Menu)
- 数字输入框 (Number Field)
- 分页 (Pagination)
- 引脚输入 (Pin Input)
- 气泡卡片 (Popover)
- 进度条 (Progress)
- 单选框组 (Radio Group)
- 范围日历 (Range Calendar)
- 可调整大小 (Resizable)
- 滚动区域 (Scroll Area)
- 选择器 (Select)
- 分隔线 (Separator)
- 侧边栏抽屉 (Sheet)
- 侧边栏 (Sidebar)
- 骨架屏 (Skeleton)
- 滑块 (Slider)
- 轻量提示 (Sonner)
- 加载动画 (Spinner)
- 步骤条 (Stepper)
- 开关 (Switch)
- 表格 (Table)
- 标签页 (Tabs)
- 标签输入 (Tags Input)
- 文本域 (Textarea)
- 吐司提示 (Toast)
- 切换按钮 (Toggle)
- 切换按钮组 (Toggle Group)
- 工具提示 (Tooltip)
- 排版 (Typography)
在本指南中,我们将了解如何使用 VeeValidate 构建表单。我们将涵盖使用 <Field /> 组件构建表单、使用 Zod 添加模式验证、错误处理、可访问性等内容。
演示
我们将构建以下表单。它包含一个简单的文本输入框和一个文本域。提交时,我们将验证表单数据并显示任何错误。
注意:为了演示目的,我们特意禁用了浏览器验证,以展示 VeeValidate 中的模式验证和表单错误是如何工作的。建议在生产代码中添加基本的浏览器验证。
报告错误
通过报告您遇到的错误来帮助我们改进。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import {
InputGroup,
InputGroupAddon,
InputGroupText,
InputGroupTextarea,
} from '@/components/ui/input-group'
const formSchema = toTypedSchema(
z.object({
title: z
.string()
.min(5, 'Bug title must be at least 5 characters.')
.max(32, 'Bug title must be at most 32 characters.'),
description: z
.string()
.min(20, 'Description must be at least 20 characters.')
.max(100, 'Description must be at most 100 characters.'),
}),
)
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
initialValues: {
title: '',
description: '',
},
})
const onSubmit = handleSubmit((data) => {
toast('You submitted the following values:', {
description: h('pre', { class: 'bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4' }, h('code', JSON.stringify(data, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
})
</script>
<template>
<Card class="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Bug Report</CardTitle>
<CardDescription>
Help us improve by reporting bugs you encounter.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-vee-demo" @submit="onSubmit">
<FieldGroup>
<VeeField v-slot="{ field, errors }" name="title">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-vee-demo-title">
Bug Title
</FieldLabel>
<Input
id="form-vee-demo-title"
v-bind="field"
placeholder="Login button not working on mobile"
autocomplete="off"
:aria-invalid="!!errors.length"
/>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
<VeeField v-slot="{ field, errors }" name="description">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-vee-demo-description">
Description
</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="form-vee-demo-description"
v-bind="field"
placeholder="I'm having an issue with the login button on mobile."
:rows="6"
class="min-h-24 resize-none"
:aria-invalid="!!errors.length"
/>
<InputGroupAddon align="block-end">
<InputGroupText class="tabular-nums">
{{ field.value?.length || 0 }}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Include steps to reproduce, expected behavior, and what actually
happened.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
<Button type="submit" form="form-vee-demo">
Submit
</Button>
</Field>
</CardFooter>
</Card>
</template>方法
此表单利用 VeeValidate 进行高性能、灵活的表单处理。我们将使用 <Field /> 组件构建表单,它为您提供了 对标记和样式的完全控制权。
- 使用 VeeValidate 的
useForm可组合函数进行表单状态管理。 - VeeValidate 的
<Field />组件,带有作用域插槽,用于受控验证输入。 - 用于构建可访问表单的 shadcn-vue
<Field />组件。 - 使用 Zod 和
toTypedSchema进行客户端验证。
结构说明
这是一个使用 VeeValidate <Field /> 组件(带作用域插槽)和 shadcn-vue <Field /> 组件的表单基础示例。
<template>
<VeeField v-slot="{ field, errors }" name="title">
<Field :data-invalid="!!errors.length">
<FieldLabel for="title">
Bug Title
</FieldLabel>
<Input
id="title"
v-bind="field"
placeholder="Login button not working on mobile"
autocomplete="off"
:aria-invalid="!!errors.length"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template>表单
创建表单架构
我们将首先使用 Zod 模式定义表单的形状
注意:此示例使用 zod v3 进行模式验证,但您可以将其替换为 VeeValidate 支持的任何其他标准模式验证库。
<script setup lang="ts">
import * as z from 'zod'
const formSchema = z.object({
title: z
.string()
.min(5, 'Bug title must be at least 5 characters.')
.max(32, 'Bug title must be at most 32 characters.'),
description: z
.string()
.min(20, 'Description must be at least 20 characters.')
.max(100, 'Description must be at most 100 characters.'),
})
</script>设置表单
接下来,我们将使用 VeeValidate 中的 useForm 可组合函数来创建表单实例。我们还将添加用于验证的 Zod 模式。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import * as z from 'zod'
const formSchema = z.object({
title: z
.string()
.min(5, 'Bug title must be at least 5 characters.')
.max(32, 'Bug title must be at most 32 characters.'),
description: z
.string()
.min(20, 'Description must be at least 20 characters.')
.max(100, 'Description must be at most 100 characters.'),
})
const { handleSubmit } = useForm({
validationSchema: toTypedSchema(formSchema),
initialValues: {
title: '',
description: '',
},
})
const onSubmit = handleSubmit((values) => {
// Do something with the form values.
console.log(values)
})
</script>
<template>
<form @submit="onSubmit">
<!-- Build the form here -->
</form>
</template>构建表单
现在,我们可以使用 VeeValidate 的 <Field /> 组件(带作用域插槽)和 shadcn-vue <Field /> 组件来构建表单。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import {
InputGroup,
InputGroupAddon,
InputGroupText,
InputGroupTextarea,
} from '@/components/ui/input-group'
const formSchema = toTypedSchema(
z.object({
title: z
.string()
.min(5, 'Bug title must be at least 5 characters.')
.max(32, 'Bug title must be at most 32 characters.'),
description: z
.string()
.min(20, 'Description must be at least 20 characters.')
.max(100, 'Description must be at most 100 characters.'),
}),
)
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
initialValues: {
title: '',
description: '',
},
})
const onSubmit = handleSubmit((data) => {
toast('You submitted the following values:', {
description: h('pre', { class: 'bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4' }, h('code', JSON.stringify(data, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
})
</script>
<template>
<Card class="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Bug Report</CardTitle>
<CardDescription>
Help us improve by reporting bugs you encounter.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-vee-demo" @submit="onSubmit">
<FieldGroup>
<VeeField v-slot="{ field, errors }" name="title">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-vee-demo-title">
Bug Title
</FieldLabel>
<Input
id="form-vee-demo-title"
v-bind="field"
placeholder="Login button not working on mobile"
autocomplete="off"
:aria-invalid="!!errors.length"
/>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
<VeeField v-slot="{ field, errors }" name="description">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-vee-demo-description">
Description
</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="form-vee-demo-description"
v-bind="field"
placeholder="I'm having an issue with the login button on mobile."
:rows="6"
class="min-h-24 resize-none"
:aria-invalid="!!errors.length"
/>
<InputGroupAddon align="block-end">
<InputGroupText class="tabular-nums">
{{ field.value?.length || 0 }}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Include steps to reproduce, expected behavior, and what actually
happened.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
<Button type="submit" form="form-vee-demo">
Submit
</Button>
</Field>
</CardFooter>
</Card>
</template>完成
就是这样。您现在拥有一个具有客户端验证且完全可访问的表单。
当您提交表单时,onSubmit 函数将接收经过验证的表单数据并被调用。如果表单数据无效,VeeValidate 会在每个字段旁边显示错误信息。
验证
客户端验证
VeeValidate 使用 Zod 模式验证您的表单数据。定义一个模式并将其传递给 useForm 可组合函数的 validationSchema 选项。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import * as z from 'zod'
const formSchema = z.object({
title: z.string(),
description: z.string().optional(),
})
const { handleSubmit } = useForm({
validationSchema: toTypedSchema(formSchema),
initialValues: {
title: '',
description: '',
},
})
</script>验证模式
VeeValidate 通过 Field 组件属性支持不同的验证策略。
<VeeField
v-slot="{ field, errors }"
name="title"
:validate-on-input="true"
>
<!-- field content -->
</VeeField>| 属性 | 描述 |
|---|---|
validateOnInput | 在 input 事件上触发验证。 |
validateOnChange | 在 change 事件上触发验证。 |
validateOnBlur | 在 blur 事件上触发验证。 |
validateOnMount | 组件挂载时触发验证。 |
显示错误
使用 <FieldError /> 在字段旁边显示错误。为了样式和可访问性
- 将
:data-invalid属性添加到 shadcn-vue<Field />组件中。 - 将
:aria-invalid属性添加到表单控件中,例如<Input />、<SelectTrigger />、<Checkbox />等。
<template>
<VeeField v-slot="{ field, errors }" name="email">
<Field :data-invalid="!!errors.length">
<FieldLabel for="email">
Email
</FieldLabel>
<Input
id="email"
v-bind="field"
type="email"
:aria-invalid="!!errors.length"
/>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template>处理不同字段类型
输入框
- 对于输入字段,使用
v-bind="field"将 VeeValidate 的字段对象绑定到输入框。 - 要显示错误,请将
:aria-invalid属性添加到<Input />组件,并将:data-invalid属性添加到 shadcn-vue<Field />组件中。
个人资料设置
在下方更新您的个人资料信息。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from '@/components/ui/field'
import { Input } from '@/components/ui/input'
const formSchema = toTypedSchema(
z.object({
username: z
.string()
.min(3, 'Username must be at least 3 characters.')
.max(10, 'Username must be at most 10 characters.')
.regex(
/^\w+$/,
'Username can only contain letters, numbers, and underscores.',
),
}),
)
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
initialValues: {
username: '',
},
})
const onSubmit = handleSubmit((data) => {
toast('You submitted the following values:', {
description: h('pre', { class: 'bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4' }, h('code', JSON.stringify(data, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
})
</script>
<template>
<Card class="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Profile Settings</CardTitle>
<CardDescription>
Update your profile information below.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-vee-input" @submit="onSubmit">
<FieldGroup>
<VeeField v-slot="{ field, errors }" name="username">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-vee-input-username">
Username
</FieldLabel>
<Input
id="form-vee-input-username"
v-bind="field"
:aria-invalid="!!errors.length"
placeholder="shadcn"
autocomplete="username"
/>
<FieldDescription>
This is your public display name. Must be between 3 and 10
characters. Must only contain letters, numbers, and
underscores.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
<Button type="submit" form="form-vee-input">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template>对于简单的文本输入,请使用带有作用域插槽的 VeeValidate Field 组件。
<template>
<VeeField v-slot="{ field, errors }" name="name">
<Field :data-invalid="!!errors.length">
<FieldLabel for="name">
Name
</FieldLabel>
<Input
id="name"
v-bind="field"
placeholder="Enter your name"
:aria-invalid="!!errors.length"
/>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template>文本域
- 对于文本域字段,使用
v-bind="field"将 VeeValidate 的字段对象绑定到文本域。 - 要显示错误,请将
:aria-invalid属性添加到<Textarea />组件,并将:data-invalid属性添加到 shadcn-vue<Field />组件中。
个性化设置
通过告诉我们更多关于您的信息来定制您的体验。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from '@/components/ui/field'
import { Textarea } from '@/components/ui/textarea'
const formSchema = toTypedSchema(
z.object({
about: z
.string()
.min(10, 'Please provide at least 10 characters.')
.max(200, 'Please keep it under 200 characters.'),
}),
)
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
initialValues: {
about: '',
},
})
const onSubmit = handleSubmit((data) => {
toast('You submitted the following values:', {
description: h('pre', { class: 'bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4' }, h('code', JSON.stringify(data, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
})
</script>
<template>
<Card class="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Personalization</CardTitle>
<CardDescription>
Customize your experience by telling us more about yourself.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-vee-textarea" @submit="onSubmit">
<FieldGroup>
<VeeField v-slot="{ field, errors }" name="about">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-vee-textarea-about">
More about you
</FieldLabel>
<Textarea
id="form-vee-textarea-about"
v-bind="field"
:aria-invalid="!!errors.length"
placeholder="I'm a software engineer..."
class="min-h-[120px]"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us
personalize your experience.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
<Button type="submit" form="form-vee-textarea">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template>对于文本域字段,请使用带有作用域插槽的 VeeValidate Field 组件。
<template>
<VeeField v-slot="{ field, errors }" name="about">
<Field :data-invalid="!!errors.length">
<FieldLabel for="about">
More about you
</FieldLabel>
<Textarea
id="about"
v-bind="field"
placeholder="I'm a software engineer..."
class="min-h-[120px]"
:aria-invalid="!!errors.length"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us personalize your experience.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template>选择
- 对于选择组件,使用
field.value和@update:model-value="field.onChange"进行正确的绑定。 - 要显示错误,请将
:aria-invalid属性添加到<SelectTrigger />组件,并将:data-invalid属性添加到 shadcn-vue<Field />组件中。
语言偏好
选择您偏好的语言。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from '@/components/ui/field'
import {
Select,
SelectContent,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
const spokenLanguages = [
{ label: 'English', value: 'en' },
{ label: 'Spanish', value: 'es' },
{ label: 'French', value: 'fr' },
{ label: 'German', value: 'de' },
{ label: 'Italian', value: 'it' },
{ label: 'Chinese', value: 'zh' },
{ label: 'Japanese', value: 'ja' },
] as const
const formSchema = toTypedSchema(
z.object({
language: z
.string()
.min(1, 'Please select your spoken language.')
.refine(val => val !== 'auto', {
message:
'Auto-detection is not allowed. Please select a specific language.',
}),
}),
)
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
initialValues: {
language: '',
},
})
const onSubmit = handleSubmit((data) => {
toast('You submitted the following values:', {
description: h('pre', { class: 'bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4' }, h('code', JSON.stringify(data, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
})
</script>
<template>
<Card class="w-full sm:max-w-lg">
<CardHeader>
<CardTitle>Language Preferences</CardTitle>
<CardDescription>
Select your preferred spoken language.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-vee-select" @submit="onSubmit">
<FieldGroup>
<VeeField v-slot="{ field, errors }" name="language">
<Field
orientation="responsive"
:data-invalid="!!errors.length"
>
<FieldContent>
<FieldLabel for="form-vee-select-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</FieldContent>
<Select
:name="field.name"
:model-value="field.value"
@update:model-value="field.onChange"
>
<SelectTrigger
id="form-vee-select-language"
:aria-invalid="!!errors.length"
class="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">
Auto
</SelectItem>
<SelectSeparator />
<SelectItem
v-for="language in spokenLanguages"
:key="language.value"
:value="language.value"
>
{{ language.label }}
</SelectItem>
</SelectContent>
</Select>
</Field>
</VeeField>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
<Button type="submit" form="form-vee-select">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template><template>
<VeeField v-slot="{ field, errors }" name="language">
<Field orientation="responsive" :data-invalid="!!errors.length">
<FieldContent>
<FieldLabel for="language">
Spoken Language
</FieldLabel>
<FieldDescription>For best results, select the language you speak.</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</FieldContent>
<Select
:model-value="field.value"
@update:model-value="field.onChange"
@blur="field.onBlur"
>
<SelectTrigger
id="language"
class="min-w-[120px]"
:aria-invalid="!!errors.length"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">
Auto
</SelectItem>
<SelectItem value="en">
English
</SelectItem>
</SelectContent>
</Select>
</Field>
</VeeField>
</template>复选框
- 对于复选框数组,使用带有自定义处理程序的 VeeValidate
Field组件来管理数组状态。 - 要显示错误,请将
:aria-invalid属性添加到<Checkbox />组件,并将:data-invalid属性添加到 shadcn-vue<Field />组件中。 - 请记住将
data-slot="checkbox-group"添加到<FieldGroup />组件以获得正确的样式和间距。
通知
管理您的通知偏好。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from '@/components/ui/field'
const tasks = [
{
id: 'push',
label: 'Push notifications',
},
{
id: 'email',
label: 'Email notifications',
},
] as const
const formSchema = toTypedSchema(
z.object({
responses: z.boolean(),
tasks: z
.array(z.string())
.min(1, 'Please select at least one notification type.')
.refine(
value => value.every(task => tasks.some(t => t.id === task)),
{
message: 'Invalid notification type selected.',
},
),
}),
)
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
initialValues: {
responses: true,
tasks: [],
},
})
const onSubmit = handleSubmit((data) => {
toast('You submitted the following values:', {
description: h('pre', { class: 'bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4' }, h('code', JSON.stringify(data, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
})
</script>
<template>
<Card class="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>Manage your notification preferences.</CardDescription>
</CardHeader>
<CardContent>
<form id="form-vee-checkbox" @submit="onSubmit">
<FieldGroup>
<VeeField v-slot="{ field, errors }" name="responses" type="checkbox">
<FieldSet :data-invalid="!!errors.length">
<FieldLegend variant="label">
Responses
</FieldLegend>
<FieldDescription>
Get notified for requests that take time, like research or image
generation.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field orientation="horizontal">
<Checkbox
id="form-vee-checkbox-responses"
:name="field.name"
:model-value="field.value"
disabled
@update:model-value="field.onChange"
/>
<FieldLabel
for="form-vee-checkbox-responses"
class="font-normal"
>
Push notifications
</FieldLabel>
</Field>
</FieldGroup>
<FieldError v-if="errors.length" :errors="errors" />
</FieldSet>
</VeeField>
<FieldSeparator />
<VeeField v-slot="{ field, errors }" name="tasks">
<FieldSet :data-invalid="!!errors.length">
<FieldLegend variant="label">
Tasks
</FieldLegend>
<FieldDescription>
Get notified when tasks you've created have updates.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field
v-for="task in tasks"
:key="task.id"
orientation="horizontal"
:data-invalid="!!errors.length"
>
<Checkbox
:id="`form-vee-checkbox-${task.id}`"
:name="field.name"
:aria-invalid="!!errors.length"
:model-value="field.value?.includes(task.id)"
@update:model-value="
(checked: boolean | 'indeterminate') => {
const newValue = checked
? [...(field.value || []), task.id]
: (field.value || []).filter(
(value: string) => value !== task.id,
);
field.onChange(newValue);
}
"
/>
<FieldLabel
:for="`form-vee-checkbox-${task.id}`"
class="font-normal"
>
{{ task.label }}
</FieldLabel>
</Field>
</FieldGroup>
<FieldError v-if="errors.length" :errors="errors" />
</FieldSet>
</VeeField>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
<Button type="submit" form="form-vee-checkbox">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template><template>
<VeeField v-slot="{ field, errors }" name="tasks">
<FieldSet>
<FieldLegend variant="label">
Tasks
</FieldLegend>
<FieldDescription>Get notified when tasks you've created have updates.</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field
v-for="task in tasks"
:key="task.id"
orientation="horizontal"
:data-invalid="!!errors.length"
>
<Checkbox
:id="`task-${task.id}`"
:model-value="field.value?.includes(task.id) ?? false"
:aria-invalid="!!errors.length"
@update:model-value="(checked | 'indeterminate') => {
const currentTasks = field.value || []
const newValue = checked
? [...currentTasks, task.id]
: currentTasks.filter(id => id !== task.id)
field.onChange(newValue)
}"
/>
<FieldLabel :for="`task-${task.id}`" class="font-normal">
{{ task.label }}
</FieldLabel>
</Field>
</FieldGroup>
<FieldError v-if="errors.length" :errors="errors" />
</FieldSet>
</VeeField>
</template>单选框组
- 对于单选按钮组,使用
field.value和@update:model-value="field.onChange"进行正确的绑定。 - 要显示错误,请将
:aria-invalid属性添加到<RadioGroupItem />组件,并将:data-invalid属性添加到 shadcn-vue<Field />组件中。
订阅计划
查看每个套餐的价格和功能。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FieldTitle,
} from '@/components/ui/field'
import {
RadioGroup,
RadioGroupItem,
} from '@/components/ui/radio-group'
const plans = [
{
id: 'starter',
title: 'Starter (100K tokens/month)',
description: 'For everyday use with basic features.',
},
{
id: 'pro',
title: 'Pro (1M tokens/month)',
description: 'For advanced AI usage with more features.',
},
{
id: 'enterprise',
title: 'Enterprise (Unlimited tokens)',
description: 'For large teams and heavy usage.',
},
] as const
const formSchema = toTypedSchema(
z.object({
plan: z.string().min(1, 'You must select a subscription plan to continue.'),
}),
)
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
initialValues: {
plan: '',
},
})
const onSubmit = handleSubmit((data) => {
toast('You submitted the following values:', {
description: h('pre', { class: 'bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4' }, h('code', JSON.stringify(data, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
})
</script>
<template>
<Card class="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Subscription Plan</CardTitle>
<CardDescription>
See pricing and features for each plan.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-vee-radiogroup" @submit="onSubmit">
<FieldGroup>
<VeeField v-slot="{ field, errors }" name="plan">
<FieldSet :data-invalid="!!errors.length">
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
:name="field.name"
:model-value="field.value"
:aria-invalid="!!errors.length"
@update:model-value="field.onChange"
>
<FieldLabel
v-for="plan in plans"
:key="plan.id"
:for="`form-vee-radiogroup-${plan.id}`"
>
<Field
orientation="horizontal"
:data-invalid="!!errors.length"
>
<FieldContent>
<FieldTitle>{{ plan.title }}</FieldTitle>
<FieldDescription>
{{ plan.description }}
</FieldDescription>
</FieldContent>
<RadioGroupItem
:id="`form-vee-radiogroup-${plan.id}`"
:value="plan.id"
:aria-invalid="!!errors.length"
/>
</Field>
</FieldLabel>
</RadioGroup>
<FieldError v-if="errors.length" :errors="errors" />
</FieldSet>
</VeeField>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
<Button type="submit" form="form-vee-radiogroup">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template><template>
<VeeField v-slot="{ field, errors }" name="plan">
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
:model-value="field.value"
@update:model-value="field.onChange"
>
<FieldLabel v-for="planOption in plans" :key="planOption.id" :for="`plan-${planOption.id}`">
<Field orientation="horizontal" :data-invalid="!!errors.length">
<FieldContent>
<FieldTitle>{{ planOption.title }}</FieldTitle>
<FieldDescription>{{ planOption.description }}</FieldDescription>
</FieldContent>
<RadioGroupItem
:id="`plan-${planOption.id}`"
:value="planOption.id"
:aria-invalid="!!errors.length"
/>
</Field>
</FieldLabel>
</RadioGroup>
<FieldError v-if="errors.length" :errors="errors" />
</FieldSet>
</VeeField>
</template>开关 (Switch)
- 对于开关,使用
:model-value="field.value"和@update:model-value="field.onChange"进行正确的绑定。 - 要显示错误,请将
:aria-invalid属性添加到<Switch />组件,并将:data-invalid属性添加到 shadcn-vue<Field />组件中。
安全设置
管理您的账户安全偏好。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from '@/components/ui/field'
import { Switch } from '@/components/ui/switch'
const formSchema = toTypedSchema(
z.object({
twoFactor: z.boolean().refine(val => val === true, {
message: 'It is highly recommended to enable two-factor authentication.',
}),
}),
)
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
initialValues: {
twoFactor: false,
},
})
const onSubmit = handleSubmit((data) => {
toast('You submitted the following values:', {
description: h('pre', { class: 'bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4' }, h('code', JSON.stringify(data, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
})
</script>
<template>
<Card class="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Security Settings</CardTitle>
<CardDescription>
Manage your account security preferences.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-vee-switch" @submit="onSubmit">
<FieldGroup>
<VeeField v-slot="{ field, errors }" name="twoFactor" type="checkbox">
<Field
orientation="horizontal"
:data-invalid="!!errors.length"
>
<FieldContent>
<FieldLabel for="form-vee-switch-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</FieldContent>
<Switch
id="form-vee-switch-twoFactor"
:name="field.name"
:model-value="field.value"
:aria-invalid="!!errors.length"
@update:model-value="field.onChange"
/>
</Field>
</VeeField>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
<Button type="submit" form="form-vee-switch">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template><template>
<VeeField v-slot="{ field, errors }" name="twoFactor">
<Field orientation="horizontal" :data-invalid="!!errors.length">
<FieldContent>
<FieldLabel for="two-factor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</FieldContent>
<Switch
id="two-factor"
:model-value="field.value"
:aria-invalid="!!errors.length"
@update:model-value="field.onChange"
/>
</Field>
</VeeField>
</template>复杂表单
这是一个具有多个字段和验证的更复杂表单的示例。
即将完成!
选择您的订阅套餐和账单周期。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from '@/components/ui/field'
import {
RadioGroup,
RadioGroupItem,
} from '@/components/ui/radio-group'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
const addons = [
{
id: 'analytics',
title: 'Analytics',
description: 'Advanced analytics and reporting',
},
{
id: 'backup',
title: 'Backup',
description: 'Automated daily backups',
},
{
id: 'support',
title: 'Priority Support',
description: '24/7 premium customer support',
},
] as const
const formSchema = toTypedSchema(
z.object({
plan: z
.string({
required_error: 'Please select a subscription plan',
})
.min(1, 'Please select a subscription plan')
.refine(value => value === 'basic' || value === 'pro', {
message: 'Invalid plan selection. Please choose Basic or Pro',
}),
billingPeriod: z
.string({
required_error: 'Please select a billing period',
})
.min(1, 'Please select a billing period'),
addons: z
.array(z.string())
.min(1, 'Please select at least one add-on')
.max(3, 'You can select up to 3 add-ons')
.refine(
value => value.every(addon => addons.some(a => a.id === addon)),
{
message: 'You selected an invalid add-on',
},
),
emailNotifications: z.boolean(),
}),
)
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
initialValues: {
plan: 'basic',
billingPeriod: '',
addons: [],
emailNotifications: false,
},
})
const onSubmit = handleSubmit((data) => {
toast('You submitted the following values:', {
description: h('pre', { class: 'bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4' }, h('code', JSON.stringify(data, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
})
</script>
<template>
<Card class="w-full max-w-sm">
<CardHeader class="border-b">
<CardTitle>You're almost there!</CardTitle>
<CardDescription>
Choose your subscription plan and billing period.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-vee-complex" @submit="onSubmit">
<FieldGroup>
<VeeField v-slot="{ field, errors }" name="plan">
<FieldSet :data-invalid="!!errors.length">
<FieldLegend variant="label">
Subscription Plan
</FieldLegend>
<FieldDescription>
Choose your subscription plan.
</FieldDescription>
<RadioGroup
:name="field.name"
:model-value="field.value"
:aria-invalid="!!errors.length"
@update:model-value="field.onChange"
>
<FieldLabel for="form-vee-complex-basic">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Basic</FieldTitle>
<FieldDescription>
For individuals and small teams
</FieldDescription>
</FieldContent>
<RadioGroupItem
id="form-vee-complex-basic"
value="basic"
/>
</Field>
</FieldLabel>
<FieldLabel for="form-vee-complex-pro">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Pro</FieldTitle>
<FieldDescription>
For businesses with higher demands
</FieldDescription>
</FieldContent>
<RadioGroupItem
id="form-vee-complex-pro"
value="pro"
/>
</Field>
</FieldLabel>
</RadioGroup>
<FieldError v-if="errors.length" :errors="errors" />
</FieldSet>
</VeeField>
<FieldSeparator />
<VeeField v-slot="{ field, errors }" name="billingPeriod">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-vee-complex-billingPeriod">
Billing Period
</FieldLabel>
<Select
:name="field.name"
:model-value="field.value"
@update:model-value="field.onChange"
>
<SelectTrigger
id="form-vee-complex-billingPeriod"
:aria-invalid="!!errors.length"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="monthly">
Monthly
</SelectItem>
<SelectItem value="yearly">
Yearly
</SelectItem>
</SelectContent>
</Select>
<FieldDescription>
Choose how often you want to be billed.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
<FieldSeparator />
<VeeField v-slot="{ field, errors }" name="addons">
<FieldSet>
<FieldLegend>Add-ons</FieldLegend>
<FieldDescription>
Select additional features you'd like to include.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field
v-for="addon in addons"
:key="addon.id"
orientation="horizontal"
:data-invalid="!!errors.length"
>
<Checkbox
:id="`form-vee-complex-${addon.id}`"
:name="field.name"
:aria-invalid="!!errors.length"
:model-value="field.value?.includes(addon.id)"
@update:model-value="(checked: boolean | 'indeterminate') => {
const newValue = checked
? [...(field.value || []), addon.id]
: (field.value || []).filter((value: string) => value !== addon.id)
field.onChange(newValue)
}"
/>
<FieldContent>
<FieldLabel :for="`form-vee-complex-${addon.id}`">
{{ addon.title }}
</FieldLabel>
<FieldDescription>
{{ addon.description }}
</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
<FieldError v-if="errors.length" :errors="errors" />
</FieldSet>
</VeeField>
<FieldSeparator />
<VeeField
v-slot="{ field, errors }"
name="emailNotifications"
type="checkbox"
>
<Field
orientation="horizontal"
:data-invalid="!!errors.length"
>
<FieldContent>
<FieldLabel for="form-vee-complex-emailNotifications">
Email Notifications
</FieldLabel>
<FieldDescription>
Receive email updates about your subscription
</FieldDescription>
</FieldContent>
<Switch
id="form-vee-complex-emailNotifications"
:name="field.name"
:model-value="field.value"
:aria-invalid="!!errors.length"
@update:model-value="field.onChange"
/>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</FieldGroup>
</form>
</CardContent>
<CardFooter class="border-t">
<Field>
<Button type="submit" form="form-vee-complex">
Save Preferences
</Button>
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
</Field>
</CardFooter>
</Card>
</template>重置表单
使用 useForm 返回的 resetForm 函数将表单重置为其初始值。
<script setup lang="ts">
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
// ...
})
</script>
<template>
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
</template>数组字段
VeeValidate 提供了一个用于管理动态数组字段的 FieldArray 组件。当您需要动态添加或删除字段时,这非常有用。
联系邮箱
管理您的联系邮箱地址。
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { X } from 'lucide-vue-next'
import { useFieldArray, useForm, Field as VeeField } from 'vee-validate'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSet,
} from '@/components/ui/field'
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from '@/components/ui/input-group'
const formSchema = toTypedSchema(
z.object({
emails: z
.array(
z.object({
address: z.string().email('Enter a valid email address.'),
}),
)
.min(1, 'Add at least one email address.')
.max(5, 'You can add up to 5 email addresses.'),
}),
)
const { handleSubmit, resetForm, errors } = useForm({
validationSchema: formSchema,
initialValues: {
emails: [{ address: '' }, { address: '' }],
},
})
const { remove, push, fields } = useFieldArray('emails')
function addEmail() {
push({ address: '' })
}
const onSubmit = handleSubmit((data) => {
toast('You submitted the following values:', {
description: h('pre', { class: 'bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4' }, h('code', JSON.stringify(data, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
})
</script>
<template>
<Card class="w-full sm:max-w-md">
<CardHeader class="border-b">
<CardTitle>Contact Emails</CardTitle>
<CardDescription>Manage your contact email addresses.</CardDescription>
</CardHeader>
<CardContent>
<form id="form-vee-array" @submit="onSubmit">
<FieldSet class="gap-4">
<FieldLegend variant="label">
Email Addresses
</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup class="gap-4">
<VeeField
v-for="(field, index) in fields"
:key="field.key"
v-slot="{ field: fieldProps, errors: fieldErrors }"
:name="`emails[${index}].address`"
>
<Field
orientation="horizontal"
:data-invalid="!!fieldErrors.length"
>
<FieldContent>
<InputGroup>
<InputGroupInput
:id="`form-vee-array-email-${index}`"
v-bind="fieldProps"
:aria-invalid="!!fieldErrors.length"
placeholder="name@example.com"
type="email"
autocomplete="email"
/>
<InputGroupAddon
v-if="fields.length > 1"
align="inline-end"
>
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
:aria-label="`Remove email ${index + 1}`"
@click="remove(index)"
>
<X />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<FieldError v-if="fieldErrors.length" :errors="fieldErrors" />
</FieldContent>
</Field>
</VeeField>
<Button
type="button"
variant="outline"
size="sm"
:disabled="fields.length >= 5"
@click="addEmail"
>
Add Email Address
</Button>
</FieldGroup>
<FieldError v-if="errors.emails" :errors="[errors.emails]" />
</FieldSet>
</form>
</CardContent>
<CardFooter class="border-t">
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
<Button type="submit" form="form-vee-array">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template>使用 FieldArray
使用 FieldArray 组件来管理数组字段。它通过插槽属性提供 fields、push 和 remove 方法。
<script setup lang="ts">
import { FieldArray as VeeFieldArray } from 'vee-validate'
</script>
<template>
<VeeFieldArray v-slot="{ fields, push, remove }" name="emails">
<!-- Array items go here -->
</VeeFieldArray>
</template>数组字段结构
使用带有 <FieldLegend /> 和 <FieldDescription /> 的 <FieldSet /> 包裹您的数组字段。
<template>
<FieldSet class="gap-4">
<FieldLegend variant="label">
Email Addresses
</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup class="gap-4">
<!-- Array items go here -->
</FieldGroup>
</FieldSet>
</template>数组项的字段模式
遍历 fields 数组并为每个项目创建字段。请确保使用 field.key 作为 key。
<template>
<VeeFieldArray v-slot="{ fields, push, remove }" name="emails">
<VeeField
v-for="(field, index) in fields"
:key="field.key"
v-slot="{ field: controllerField, errors }"
:name="`emails[${index}].address`"
>
<Field orientation="horizontal" :data-invalid="!!errors.length">
<FieldContent class="flex-1">
<InputGroup>
<InputGroupInput
:id="`email-${index}`"
v-bind="controllerField"
type="email"
placeholder="name@example.com"
autocomplete="email"
:aria-invalid="!!errors.length"
/>
<!-- Remove button -->
</InputGroup>
<FieldError v-if="errors.length" :errors="errors" />
</FieldContent>
</Field>
</VeeField>
</VeeFieldArray>
</template>添加项目
使用 push 方法向数组添加新项目。
<template>
<Button
type="button"
variant="outline"
size="sm"
:disabled="fields.length >= 5"
@click="push({ address: '' })"
>
Add Email Address
</Button>
</template>删除项目
使用 remove 方法从数组中删除项目。有条件地添加删除按钮。
<template>
<InputGroupAddon v-if="fields.length > 1" align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
:aria-label="`Remove email ${index + 1}`"
@click="remove(index)"
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
</template>数组验证
使用 Zod 的 array 方法来验证数组字段。
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email('Enter a valid email address.'),
}),
)
.min(1, 'Add at least one email address.')
.max(5, 'You can add up to 5 email addresses.'),
})