在本指南中,我们将研究如何使用 VeeValidate 构建表单。我们将介绍如何使用 <Field /> 组件构建表单、使用 Zod 添加模式验证、错误处理、可访问性等内容。
🌐 In this guide, we will take a look at building forms with VeeValidate. We'll cover building forms with the <Field /> component, adding schema validation using Zod, error handling, accessibility, and more.
::vue-school-link{class="mt-6" lesson="forms-and-form-validation-with-shadcn-vue" placement="top"} 观看一段关于使用 shadcn-vue 构建表单和验证的 Vue School 视频。::
演示
🌐 Demo
我们将要创建以下表单。它有一个简单的文本输入框和一个多行文本框。提交时,我们将验证表单数据并显示任何错误。
🌐 We are going to build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
注意: 为了演示目的,我们故意禁用了浏览器验证,以展示 VeeValidate 中的模式验证和表单错误如何工作。建议在生产代码中添加基本的浏览器验证。
Bug Report
Help us improve by reporting bugs you encounter.
<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>方法
🌐 Approach
这个表单利用 VeeValidate 来实现高性能、灵活的表单处理。我们将使用 <Field /> 组件来构建表单,这使你可以完全自由地控制标记和样式。
🌐 This form leverages VeeValidate for performant, flexible form handling. We'll build our form using the <Field /> component, which gives you complete flexibility over the markup and styling.
- 使用 VeeValidate 的
useForm组合式函数进行表单状态管理。 - VeeValidate 的
<Field />组件,带有用于受控输入和验证的作用域插槽。 - shadcn-vue
<Field />组件用于构建无障碍表单。 - 使用 Zod 和
toTypedSchema的客户端验证。
剖析
🌐 Anatomy
这是一个使用 VeeValidate 的 <Field /> 组件与作用域插槽以及 shadcn-vue <Field /> 组件的表单基本示例。
🌐 Here's a basic example of a form using VeeValidate's <Field /> component with scoped slots and shadcn-vue <Field /> components.
<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>表单
🌐 Form
创建表单模式
🌐 Create a form schema
我们将首先使用 Zod 模式来定义我们表单的形状
🌐 We'll start by defining the shape of our form using a Zod schema
注意: 此示例使用 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>设置表单
🌐 Setup the form
接下来,我们将使用 VeeValidate 中的 useForm 组合函数来创建我们的表单实例。我们还将添加 Zod 模式进行验证。
🌐 Next, we'll use the useForm composable from VeeValidate to create our form instance. We'll also add the Zod schema for validation.
<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>建立表单
🌐 Build the form
我们现在可以使用 VeeValidate 的 <Field /> 组件(带有作用域插槽)和 shadcn-vue 的 <Field /> 组件来构建表单。
🌐 We can now build the form using VeeValidate's <Field /> component with scoped slots and shadcn-vue <Field /> components.
::组件源{name="VeeValidateDemo" title="Form.vue"} ::
完成
🌐 Done
就是这样。你现在有一个完全可访问的表格,并带有客户端验证。
🌐 That's it. You now have a fully accessible form with client-side validation.
当你提交表单时,onSubmit 函数将使用验证过的表单数据被调用。如果表单数据无效,VeeValidate 会在每个字段旁显示错误信息。
🌐 When you submit the form, the onSubmit function will be called with the validated form data. If the form data is invalid, VeeValidate will display the errors next to each field.
验证
🌐 Validation
客户端验证
🌐 Client-side Validation
VeeValidate 使用 Zod 模式验证你的表单数据。定义一个模式并将其传递给 useForm 组合函数的 validationSchema 选项。
🌐 VeeValidate validates your form data using the Zod schema. Define a schema and pass it to the validationSchema option of the useForm composable.
<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>验证模式
🌐 Validation Modes
VeeValidate 通过 Field 组件属性支持不同的验证策略。
🌐 VeeValidate supports different validation strategies through the Field component props.
<VeeField
v-slot="{ field, errors }"
name="title"
:validate-on-input="true"
>
<!-- field content -->
</VeeField>| 属性 | 描述 |
|---|---|
validateOnInput | 验证在输入事件时触发。 |
validateOnChange | 验证在更改事件时触发。 |
validateOnBlur | 验证在失焦事件时触发。 |
validateOnMount | 验证在组件挂载时触发。 |
显示错误
🌐 Displaying Errors
使用 <FieldError /> 在字段旁显示错误。用于样式和可访问性:
🌐 Display errors next to the field using <FieldError />. For styling and accessibility:
- 将
: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>处理不同的字段类型
🌐 Working with Different Field Types
输入
🌐 Input
- 对于输入字段,使用
v-bind="field"将 VeeValidate 的字段对象绑定到输入框。 - 要显示错误,请将
:aria-invalid属性添加到<Input />组件,并将:data-invalid属性添加到 shadcn-vue 的<Field />组件。
Profile Settings
Update your profile information below.
<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 组件及作用域插槽。
🌐 For simple text inputs, use VeeValidate's Field component with scoped slots.
<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>文本区域
🌐 Textarea
- 对于文本区域字段,使用
v-bind="field"将 VeeValidate 的字段对象绑定到文本区域。 - 要显示错误,请将
:aria-invalid属性添加到<Textarea />组件,并将:data-invalid属性添加到 shadcn-vue 的<Field />组件。
Personalization
Customize your experience by telling us more about yourself.
<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 组件并搭配作用域插槽。
🌐 For textarea fields, use VeeValidate's Field component with scoped slots.
<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>选择
🌐 Select
- 对于某些组件,使用
field.value和@update:model-value="field.onChange"以实现正确绑定。 - 要显示错误,请将
:aria-invalid属性添加到<SelectTrigger />组件,并将:data-invalid属性添加到 shadcn-vue 的<Field />组件。
Language Preferences
Select your preferred spoken language.
<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>复选框
🌐 Checkbox
- 对于复选框数组,使用 VeeValidate 的
Field组件并配合自定义处理器来管理数组状态。 - 要显示错误,请将
:aria-invalid属性添加到<Checkbox />组件,并将:data-invalid属性添加到 shadcn-vue<Field />组件。 - 记得将
data-slot="checkbox-group"添加到<FieldGroup />组件中,以获得正确的样式和间距。
Notifications
Manage your notification preferences.
<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>单选按钮组
🌐 Radio Group
- 对于单选按钮组,使用
field.value和@update:model-value="field.onChange"进行正确绑定。 - 要显示错误,请将
:aria-invalid属性添加到<RadioGroupItem />组件,并将:data-invalid属性添加到 shadcn-vue 的<Field />组件。
Subscription Plan
See pricing and features for each plan.
<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 />组件。
Security Settings
Manage your account security preferences.
<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>复杂形式
🌐 Complex Forms
这是一个包含多个字段和验证的更复杂表单的示例。
🌐 Here is an example of a more complex form with multiple fields and validation.
You're almost there!
Choose your subscription plan and billing period.
<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>重置表单
🌐 Resetting the Form
使用 useForm 返回的 resetForm 函数将表单重置为初始值。
🌐 Use the resetForm function returned by useForm to reset the form to its initial values.
<script setup lang="ts">
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
// ...
})
</script>
<template>
<Button type="button" variant="outline" @click="resetForm">
Reset
</Button>
</template>数组字段
🌐 Array Fields
VeeValidate 提供了一个 FieldArray 组件来管理动态数组字段。当你需要动态添加或删除字段时,这非常有用。
🌐 VeeValidate provides a FieldArray component for managing dynamic array fields. This is useful when you need to add or remove fields dynamically.
Contact Emails
Manage your contact email addresses.
<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
🌐 Using FieldArray
使用 FieldArray 组件来管理数组字段。它通过其插槽属性提供 fields、push 和 remove 方法。
🌐 Use the FieldArray component to manage array fields. It provides fields, push, and remove methods through its slot props.
<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>数组字段结构
🌐 Array Field Structure
将你的数组字段用一个 <FieldSet /> 封装,并搭配一个 <FieldLegend /> 和 <FieldDescription />。
🌐 Wrap your array fields in a <FieldSet /> with a <FieldLegend /> and <FieldDescription />.
<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>数组项的字段模式
🌐 Field Pattern for Array Items
遍历 fields 数组,并为每个项目创建字段。确保使用 field.key 作为键。
🌐 Map over the fields array and create fields for each item. Make sure to use field.key as the 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>添加项目
🌐 Adding Items
使用 push 方法向数组中添加新项。
🌐 Use the push method to add new items to the array.
<template>
<Button
type="button"
variant="outline"
size="sm"
:disabled="fields.length >= 5"
@click="push({ address: '' })"
>
Add Email Address
</Button>
</template>移除条目
🌐 Removing Items
使用 remove 方法从数组中移除项。有条件地添加删除按钮。
🌐 Use the remove method to remove items from the array. Add the remove button conditionally.
<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>数组验证
🌐 Array Validation
使用 Zod 的 array 方法来验证数组字段。
🌐 Use Zod's array method to validate array fields.
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.'),
})