本指南探讨了如何使用 TanStack Form 构建表单。你将学习使用 <Field /> 组件创建表单,使用 Zod 实现模式验证,处理错误,并确保可访问性。
🌐 This guide explores how to build forms using TanStack Form. You'll learn to create forms with <Field /> components, implement schema validation with Zod, handle errors, and ensure accessibility.
演示
🌐 Demo
我们将从构建以下表单开始。它有一个简单的文本输入框和一个多行文本框。提交时,我们将验证表单数据并显示任何错误。
🌐 We'll start by building the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
注意: 为了演示目的,我们故意禁用了浏览器验证,以展示 TanStack Form 中的模式验证和表单错误如何工作。建议在生产代码中添加基本的浏览器验证。
Bug Report
Help us improve by reporting bugs you encounter.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
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 = 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 form = useForm({
defaultValues: {
title: '',
description: '',
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
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(value, null, 2)),
),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
},
})
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</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-tanstack-demo" @submit.prevent="form.handleSubmit">
<FieldGroup>
<form.Field name="title">
<template #default="{ field }">
<Field :data-invalid="isInvalid(field)">
<FieldLabel :for="field.name">
Bug Title
</FieldLabel>
<Input
:id="field.name"
:name="field.name"
:model-value="field.state.value"
:aria-invalid="isInvalid(field)"
placeholder="Login button not working on mobile"
autocomplete="off"
@blur="field.handleBlur"
@input="field.handleChange($event.target.value)"
/>
<FieldError
v-if="isInvalid(field)"
:errors="field.state.meta.errors"
/>
</Field>
</template>
</form.Field>
<form.Field name="description">
<template #default="{ field }">
<Field :data-invalid="isInvalid(field)">
<FieldLabel :for="field.name">
Description
</FieldLabel>
<InputGroup>
<InputGroupTextarea
:id="field.name"
:name="field.name"
:model-value="field.state.value"
placeholder="I'm having an issue with the login button on mobile."
:rows="6"
class="min-h-24 resize-none"
:aria-invalid="isInvalid(field)"
@blur="field.handleBlur"
@input="field.handleChange($event.target.value)"
/>
<InputGroupAddon align="block-end">
<InputGroupText class="tabular-nums">
{{ field.state.value?.length || 0 }}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Include steps to reproduce, expected behavior, and what
actually happened.
</FieldDescription>
<FieldError
v-if="isInvalid(field)"
:errors="field.state.meta.errors"
/>
</Field>
</template>
</form.Field>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>
<Button type="submit" form="form-tanstack-demo">
Submit
</Button>
</Field>
</CardFooter>
</Card>
</template>方法
🌐 Approach
此表单利用 TanStack Form 提供强大的无头表单处理功能。我们将使用 <Field /> 组件来构建表单,这些组件为你提供了对标记和样式的完全灵活性。
🌐 This form leverages TanStack Form for powerful, headless form handling. We'll build our form using the <Field /> components, which give you complete flexibility over the markup and styling.
- 使用 TanStack Form 的
useForm组合式函数进行表单状态管理。 form.Field组件,使用渲染属性模式来控制输入。<Field />组件用于构建可访问的表单。- 使用 Zod 进行客户端验证。
- 实时验证反馈。
剖析
🌐 Anatomy
这是一个使用 TanStack Form 和 <Field /> 组件的表单的基本示例。
🌐 Here's a basic example of a form using TanStack Form with the <Field /> component.
<template>
<form
@submit.prevent="form.handleSubmit"
>
<FieldGroup>
<form.Field
name="title"
#default="{ field }"
>
<Field :data-invalid="isInvalid(field)">
<FieldLabel :for="field.name">Bug Title</FieldLabel>
<Input
:id="field.name"
:name="field.name"
:model-value="field.state.value"
@blur="field.handleBlur"
@input="field.handleChange($event.target.value)"
:aria-invalid="isInvalid(field)"
placeholder="Login button not working on mobile"
autocomplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</Field>
</form.Field>
</FieldGroup>
<Button type="submit">Submit</Button>
</form>
</template>表单
🌐 Form
创建表单模式
🌐 Create a form schema
我们将首先使用 Zod 架构来定义表格的形状。
🌐 We'll start by defining the shape of our form using a Zod schema.
注意: 这个示例使用 zod v3 进行架构验证。TanStack Form 通过其验证器 API 与 Zod 以及其他标准架构验证库无缝集成。
<script setup lang="ts">
import { 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
使用 TanStack Form 的 useForm 可组合函数来创建具有 Zod 验证的表单实例。
🌐 Use the useForm composable from TanStack Form to create your form instance with Zod validation.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import { toast } from 'vue-sonner'
import { z } from 'zod'
const formSchema = z.object({
// ...
})
const form = useForm({
defaultValues: {
title: '',
description: '',
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast.success('Form submitted successfully')
},
})
function isInvalid(field) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</script>
<template>
<form @submit.prevent="form.handleSubmit">
<!-- ... -->
</form>
</template>我们在这里使用 onSubmit 来验证表单数据。TanStack Form 支持其他验证模式,你可以在文档中了解相关内容。
🌐 We are using onSubmit to validate the form data here. TanStack Form supports other validation modes, which you can read about in the documentation.
建立表单
🌐 Build the form
我们现在可以使用 TanStack Form 的 form.Field 组件和 Field 组件来构建表单。
🌐 We can now build the form using the form.Field component from TanStack Form and the Field components.
::组件源
名称: TanStackForm演示 标题: Form.vue
::
完成
🌐 Done
就是这样。你现在有一个完全可访问的表格,并带有客户端验证。
🌐 That's it. You now have a fully accessible form with client-side validation.
当你提交表单时,onSubmit 函数将使用验证过的表单数据被调用。如果表单数据无效,TanStack Form 会在每个字段旁显示错误信息。
🌐 When you submit the form, the onSubmit function will be called with the validated form data. If the form data is invalid, TanStack Form will display the errors next to each field.
验证
🌐 Validation
客户端验证
🌐 Client-side Validation
TanStack Form 使用 Zod 模式验证你的表单数据。验证会在用户输入时实时发生。
🌐 TanStack Form validates your form data using the Zod schema. Validation happens in real-time as the user types.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
const formSchema = z.object({
// ...
})
const form = useForm({
defaultValues: {
title: '',
description: '',
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
</script>验证模式
🌐 Validation Modes
TanStack 表单通过 validators 选项支持不同的验证策略:
🌐 TanStack Form supports different validation strategies through the validators option:
| 模式 | 描述 |
|---|---|
onChange | 验证在每次更改时触发。 |
onBlur | 验证在失去焦点时触发。 |
onSubmit | 验证在提交时触发。 |
<script setup lang="ts">
const form = useForm({
defaultValues: {
title: '',
description: '',
},
validators: {
onSubmit: formSchema,
onChange: formSchema,
onBlur: formSchema,
},
})
</script>显示错误
🌐 Displaying Errors
使用 FieldError 在字段旁显示错误。用于样式和可访问性:
🌐 Display errors next to the field using FieldError. For styling and accessibility:
- 将
:data-invalid属性添加到Field组件中。 - 将
:aria-invalid属性添加到表单控件中,例如Input、SelectTrigger、Checkbox等。
<script setup lang="ts">
function isInvalid(field) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</script>
<template>
<form.Field
name="email"
#default="{ field }"
>
<Field :data-invalid="isInvalid(field)">
<FieldLabel :for="field.name">Email</FieldLabel>
<Input
:id="field.name"
:name="field.name"
:model-value="field.state.value"
@blur="field.handleBlur"
@input="field.handleChange($event.target.value)"
type="email"
:aria-invalid="isInvalid(field)"
/>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</Field>
</form.Field>
</template>处理不同的字段类型
🌐 Working with Different Field Types
输入
🌐 Input
对于输入字段,在 Input 组件上使用 field.state.value 和 field.handleChange。
要显示错误,在 Input 组件上添加 :aria-invalid 属性,在 Field 组件上添加 :data-invalid 属性。
🌐 For input fields, use field.state.value and field.handleChange on the Input component.
To show errors, add the :aria-invalid prop to the Input component and the :data-invalid prop to the Field component.
Profile Settings
Update your profile information below.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
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 = 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 form = useForm({
defaultValues: {
username: '',
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
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(value, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
},
})
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</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-tanstack-input" @submit.prevent="form.handleSubmit">
<FieldGroup>
<form.Field v-slot="{ field }" name="username">
<Field :data-invalid="isInvalid(field)">
<FieldLabel for="form-tanstack-input-username">
Username
</FieldLabel>
<Input
id="form-tanstack-input-username"
:name="field.name"
:model-value="field.state.value"
:aria-invalid="isInvalid(field)"
placeholder="shadcn"
autocomplete="username"
@blur="field.handleBlur"
@input="field.handleChange($event.target.value)"
/>
<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="isInvalid(field)" :errors="field.state.meta.errors" />
</Field>
</form.Field>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>
<Button type="submit" form="form-tanstack-input">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template><template>
<form.Field
name="username"
#default="{ field }"
>
<Field :data-invalid="isInvalid(field)">
<FieldLabel :for="`form-tanstack-input-username`">Username</FieldLabel>
<Input
id="form-tanstack-input-username"
:name="field.name"
:model-value="field.state.value"
@blur="field.handleBlur"
@input="field.handleChange($event.target.value)"
:aria-invalid="isInvalid(field)"
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="isInvalid(field)" :errors="field.state.meta.errors" />
</Field>
</form.Field>
</template>文本区域
🌐 Textarea
对于多行文本字段,在 Textarea 组件上使用 field.state.value 和 field.handleChange。
要显示错误,在 Textarea 组件上添加 :aria-invalid 属性,在 Field 组件上添加 :data-invalid 属性。
🌐 For textarea fields, use field.state.value and field.handleChange on the Textarea component.
To show errors, add the :aria-invalid prop to the Textarea component and the :data-invalid prop to the Field component.
Personalization
Customize your experience by telling us more about yourself.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
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 = z.object({
about: z
.string()
.min(10, 'Please provide at least 10 characters.')
.max(200, 'Please keep it under 200 characters.'),
})
const form = useForm({
defaultValues: {
about: '',
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
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(value, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
},
})
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</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-tanstack-textarea" @submit.prevent="form.handleSubmit">
<FieldGroup>
<form.Field v-slot="{ field }" name="about">
<Field :data-invalid="isInvalid(field)">
<FieldLabel for="form-tanstack-textarea-about">
More about you
</FieldLabel>
<Textarea
id="form-tanstack-textarea-about"
:name="field.name"
:model-value="field.state.value"
:aria-invalid="isInvalid(field)"
placeholder="I'm a software engineer..."
class="min-h-[120px]"
@blur="field.handleBlur"
@input="field.handleChange($event.target.value)"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us
personalize your experience.
</FieldDescription>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</Field>
</form.Field>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>
<Button type="submit" form="form-tanstack-textarea">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template><template>
<form.Field
name="about"
#default="{ field }"
>
<Field :data-invalid="isInvalid(field)">
<FieldLabel :for="`form-tanstack-textarea-about`">
More about you
</FieldLabel>
<Textarea
id="form-tanstack-textarea-about"
:name="field.name"
:model-value="field.state.value"
@blur="field.handleBlur"
@input="field.handleChange($event.target.value)"
:aria-invalid="isInvalid(field)"
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="isInvalid(field)" :errors="field.state.meta.errors" />
</Field>
</form.Field>
</template>选择
🌐 Select
对于特定组件,在 Select 组件上使用 field.state.value 和 field.handleChange。
要显示错误,请在 SelectTrigger 组件上添加 :aria-invalid 属性,并在 Field 组件上添加 :data-invalid 属性。
🌐 For select components, use field.state.value and field.handleChange on the Select component.
To show errors, add the :aria-invalid prop to the SelectTrigger component and the :data-invalid prop to the Field component.
Language Preferences
Select your preferred spoken language.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
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 = 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 form = useForm({
defaultValues: {
language: '',
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
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(value, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
},
})
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</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-tanstack-select" @submit.prevent="form.handleSubmit">
<FieldGroup>
<form.Field v-slot="{ field }" name="language">
<Field orientation="responsive" :data-invalid="isInvalid(field)">
<FieldContent>
<FieldLabel for="form-tanstack-select-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldContent>
<Select
:name="field.name"
:model-value="field.state.value"
@update:model-value="field.handleChange"
>
<SelectTrigger
id="form-tanstack-select-language"
:aria-invalid="isInvalid(field)"
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>
</form.Field>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>
<Button type="submit" form="form-tanstack-select">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template><template>
<form.Field
name="language"
#default="{ field }"
>
<Field orientation="responsive" :data-invalid="isInvalid(field)">
<FieldContent>
<FieldLabel :for="`form-tanstack-select-language`">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldContent>
<Select
:name="field.name"
:model-value="field.state.value"
@update:model-value="field.handleChange"
>
<SelectTrigger
id="form-tanstack-select-language"
:aria-invalid="isInvalid(field)"
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>
</form.Field>
</template>复选框
🌐 Checkbox
对于复选框,在 Checkbox 组件上使用 field.state.value 和 field.handleChange。
要显示错误,在 Checkbox 组件上添加 :aria-invalid 属性,在 Field 组件上添加 :data-invalid 属性。
对于复选框数组,在 form.Field 组件上使用 mode="array" 并结合 TanStack Form 的数组辅助程序。
记得在 FieldGroup 组件上添加 data-slot="checkbox-group" 以确保正确的样式和间距。
🌐 For checkboxes, use field.state.value and field.handleChange on the Checkbox component.
To show errors, add the :aria-invalid prop to the Checkbox component and the :data-invalid prop to the Field component.
For checkbox arrays, use mode="array" on the form.Field component and TanStack Form's array helpers.
Remember to add data-slot="checkbox-group" to the FieldGroup component for proper styling and spacing.
Notifications
Manage your notification preferences.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
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 = 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 form = useForm({
defaultValues: {
responses: true,
tasks: [] as string[],
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
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(value, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
},
})
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</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-tanstack-checkbox" @submit.prevent="form.handleSubmit">
<FieldGroup>
<form.Field v-slot="{ field }" name="responses">
<FieldSet>
<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" :data-invalid="isInvalid(field)">
<Checkbox
id="form-tanstack-checkbox-responses"
:name="field.name"
:model-value="field.state.value"
disabled
@update:model-value="(checked) => field.handleChange(checked === true)"
/>
<FieldLabel
for="form-tanstack-checkbox-responses"
class="font-normal"
>
Push notifications
</FieldLabel>
</Field>
</FieldGroup>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldSet>
</form.Field>
<FieldSeparator />
<form.Field v-slot="{ field }" name="tasks" mode="array">
<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="isInvalid(field)"
>
<Checkbox
:id="`form-tanstack-checkbox-${task.id}`"
:name="field.name"
:aria-invalid="isInvalid(field)"
:model-value="field.state.value.includes(task.id)"
@update:model-value="(checked) => {
if (checked) {
field.pushValue(task.id)
}
else {
const index = field.state.value.indexOf(task.id)
if (index > -1) {
field.removeValue(index)
}
}
}"
/>
<FieldLabel
:for="`form-tanstack-checkbox-${task.id}`"
class="font-normal"
>
{{ task.label }}
</FieldLabel>
</Field>
</FieldGroup>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldSet>
</form.Field>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>
<Button type="submit" form="form-tanstack-checkbox">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template><template>
<form.Field
name="tasks"
mode="array"
#default="{ field }"
>
<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="isInvalid(field)"
>
<Checkbox
:id="`form-tanstack-checkbox-${task.id}`"
:name="field.name"
:aria-invalid="isInvalid(field)"
:model-value="field.state.value.includes(task.id)"
@update:model-value="(checked | 'indeterminate') => {
if (checked) {
field.pushValue(task.id)
} else {
const index = field.state.value.indexOf(task.id)
if (index > -1) {
field.removeValue(index)
}
}
}"
/>
<FieldLabel
:for="`form-tanstack-checkbox-${task.id}`"
class="font-normal"
>
{{ task.label }}
</FieldLabel>
</Field>
</FieldGroup>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldSet>
</form.Field>
</template>单选按钮组
🌐 Radio Group
对于单选按钮组,在 RadioGroup 组件上使用 field.state.value 和 field.handleChange。
要显示错误,在 RadioGroupItem 组件上添加 :aria-invalid 属性,在 Field 组件上添加 :data-invalid 属性。
🌐 For radio groups, use field.state.value and field.handleChange on the RadioGroup component.
To show errors, add the :aria-invalid prop to the RadioGroupItem component and the :data-invalid prop to the Field component.
Subscription Plan
See pricing and features for each plan.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
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 individuals and small teams',
},
{
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 = z.object({
plan: z.string().min(1, 'You must select a subscription plan to continue.'),
})
const form = useForm({
defaultValues: {
plan: '',
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
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(value, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
},
})
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</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-tanstack-radiogroup" @submit.prevent="form.handleSubmit">
<FieldGroup>
<form.Field v-slot="{ field }" name="plan">
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
:name="field.name"
:model-value="field.state.value"
@update:model-value="field.handleChange"
>
<FieldLabel
v-for="plan in plans"
:key="plan.id"
:for="`form-tanstack-radiogroup-${plan.id}`"
>
<Field
orientation="horizontal"
:data-invalid="isInvalid(field)"
>
<FieldContent>
<FieldTitle>{{ plan.title }}</FieldTitle>
<FieldDescription>{{ plan.description }}</FieldDescription>
</FieldContent>
<RadioGroupItem
:id="`form-tanstack-radiogroup-${plan.id}`"
:value="plan.id"
:aria-invalid="isInvalid(field)"
/>
</Field>
</FieldLabel>
</RadioGroup>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldSet>
</form.Field>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>
<Button type="submit" form="form-tanstack-radiogroup">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template><template>
<form.Field
name="plan"
#default="{ field }"
>
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
:name="field.name"
:model-value="field.state.value"
@update:model-value="field.handleChange"
>
<FieldLabel
v-for="plan in plans"
:key="plan.id"
:for="`form-tanstack-radiogroup-${plan.id}`"
>
<Field
orientation="horizontal"
:data-invalid="isInvalid(field)"
>
<FieldContent>
<FieldTitle>{{ plan.title }}</FieldTitle>
<FieldDescription>{{ plan.description }}</FieldDescription>
</FieldContent>
<RadioGroupItem
:value="plan.id"
:id="`form-tanstack-radiogroup-${plan.id}`"
:aria-invalid="isInvalid(field)"
/>
</Field>
</FieldLabel>
</RadioGroup>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldSet>
</form.Field>
</template>切换
🌐 Switch
对于开关,在 Switch 组件上使用 field.state.value 和 field.handleChange。
要显示错误,在 Switch 组件上添加 :aria-invalid 属性,并在 Field 组件上添加 :data-invalid 属性。
🌐 For switches, use field.state.value and field.handleChange on the Switch component.
To show errors, add the :aria-invalid prop to the Switch component and the :data-invalid prop to the Field component.
Security Settings
Manage your account security preferences.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
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 = z.object({
twoFactor: z.boolean().refine(val => val === true, {
message: 'It is highly recommended to enable two-factor authentication.',
}),
})
const form = useForm({
defaultValues: {
twoFactor: false,
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
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(value, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
},
})
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</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-tanstack-switch" @submit.prevent="form.handleSubmit">
<FieldGroup>
<form.Field v-slot="{ field }" name="twoFactor">
<Field orientation="horizontal" :data-invalid="isInvalid(field)">
<FieldContent>
<FieldLabel :for="field.name">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldContent>
<Switch
:id="field.name"
:name="field.name"
:model-value="field.state.value"
:aria-invalid="isInvalid(field)"
@update:model-value="field.handleChange"
/>
</Field>
</form.Field>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>
<Button type="submit" form="form-tanstack-switch">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template><template>
<form.Field
name="twoFactor"
#default="{ field }"
>
<Field orientation="horizontal" :data-invalid="isInvalid(field)">
<FieldContent>
<FieldLabel :for="field.name">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldContent>
<Switch
:id="field.name"
:name="field.name"
:model-value="field.state.value"
@update:model-value="field.handleChange"
:aria-invalid="isInvalid(field)"
/>
</Field>
</form.Field>
</template>复杂形式
🌐 Complex Forms
这是一个包含多个字段和验证的更复杂表单的示例。
🌐 Here is an example of a more complex form with multiple fields and validation.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardFooter } 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 = 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 form = useForm({
defaultValues: {
plan: 'basic',
billingPeriod: 'monthly',
addons: [] as string[],
emailNotifications: false,
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
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(value, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
},
})
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</script>
<template>
<Card class="w-full max-w-sm">
<CardContent>
<form id="subscription-form" @submit.prevent="form.handleSubmit">
<FieldGroup>
<form.Field v-slot="{ field }" name="plan">
<FieldSet>
<FieldLegend>Subscription Plan</FieldLegend>
<FieldDescription>
Choose your subscription plan.
</FieldDescription>
<RadioGroup
:name="field.name"
:model-value="field.state.value"
@update:model-value="field.handleChange"
>
<FieldLabel for="basic">
<Field
orientation="horizontal"
:data-invalid="isInvalid(field)"
>
<FieldContent>
<FieldTitle>Basic</FieldTitle>
<FieldDescription>
For individuals and small teams
</FieldDescription>
</FieldContent>
<RadioGroupItem
id="basic"
value="basic"
:aria-invalid="isInvalid(field)"
/>
</Field>
</FieldLabel>
<FieldLabel for="pro">
<Field
orientation="horizontal"
:data-invalid="isInvalid(field)"
>
<FieldContent>
<FieldTitle>Pro</FieldTitle>
<FieldDescription>
For businesses with higher demands
</FieldDescription>
</FieldContent>
<RadioGroupItem
id="pro"
value="pro"
:aria-invalid="isInvalid(field)"
/>
</Field>
</FieldLabel>
</RadioGroup>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldSet>
</form.Field>
<FieldSeparator />
<form.Field v-slot="{ field }" name="billingPeriod">
<Field :data-invalid="isInvalid(field)">
<FieldLabel :for="field.name">
Billing Period
</FieldLabel>
<Select
:name="field.name"
:model-value="field.state.value"
:aria-invalid="isInvalid(field)"
@update:model-value="field.handleChange"
>
<SelectTrigger :id="field.name">
<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="isInvalid(field)" :errors="field.state.meta.errors" />
</Field>
</form.Field>
<FieldSeparator />
<form.Field v-slot="{ field }" name="addons" mode="array">
<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="isInvalid(field)"
>
<Checkbox
:id="addon.id"
:name="field.name"
:aria-invalid="isInvalid(field)"
:checked="field.state.value.includes(addon.id)"
@update:checked="(checked) => {
if (checked) {
field.pushValue(addon.id)
}
else {
const index = field.state.value.indexOf(addon.id)
if (index > -1) {
field.removeValue(index)
}
}
}"
/>
<FieldContent>
<FieldLabel :for="addon.id">
{{ addon.title }}
</FieldLabel>
<FieldDescription>
{{ addon.description }}
</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldSet>
</form.Field>
<FieldSeparator />
<form.Field v-slot="{ field }" name="emailNotifications">
<Field orientation="horizontal" :data-invalid="isInvalid(field)">
<FieldContent>
<FieldLabel :for="field.name">
Email Notifications
</FieldLabel>
<FieldDescription>
Receive email updates about your subscription
</FieldDescription>
</FieldContent>
<Switch
:id="field.name"
:name="field.name"
:checked="field.state.value"
:aria-invalid="isInvalid(field)"
@update:checked="field.handleChange"
/>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</Field>
</form.Field>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal" class="justify-end">
<Button type="submit" form="subscription-form">
Save Preferences
</Button>
</Field>
</CardFooter>
</Card>
</template>重置表单
🌐 Resetting the Form
使用 form.reset() 将表单重置为默认值。
🌐 Use form.reset() to reset the form to its default values.
<template>
<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>
</template>数组字段
🌐 Array Fields
TanStack Form 提供了强大的数组字段管理功能,使用 mode="array"。这允许你动态地添加、删除和更新数组项,并且支持完整的验证。
🌐 TanStack Form provides powerful array field management with mode="array". This allows you to dynamically add, remove, and update array items with full validation support.
Contact Emails
Manage your contact email addresses.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import { XIcon } from 'lucide-vue-next'
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 = 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 form = useForm({
defaultValues: {
emails: [{ address: '' }],
},
validators: {
onBlur: formSchema,
},
onSubmit: async ({ value }) => {
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(value, null, 2))),
position: 'bottom-right',
class: 'flex flex-col gap-2',
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
},
})
},
})
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
function isSubFieldInvalid(subField: any) {
return subField.state.meta.isTouched && !subField.state.meta.isValid
}
</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-tanstack-array" @submit.prevent="form.handleSubmit">
<form.Field v-slot="{ field }" name="emails" mode="array">
<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">
<form.Field
v-for="(_, index) in field.state.value"
:key="index"
v-slot="{ field: subField }"
:name="`emails[${index}].address`"
>
<Field
orientation="horizontal"
:data-invalid="isSubFieldInvalid(subField)"
>
<FieldContent>
<InputGroup>
<InputGroupInput
:id="`form-tanstack-array-email-${index}`"
:name="subField.name"
:model-value="subField.state.value"
:aria-invalid="isSubFieldInvalid(subField)"
placeholder="name@example.com"
type="email"
autocomplete="email"
@blur="subField.handleBlur"
@input="subField.handleChange"
/>
<InputGroupAddon v-if="field.state.value.length > 1" align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
:aria-label="`Remove email ${index + 1}`"
@click="field.removeValue(index)"
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<FieldError v-if="isSubFieldInvalid(subField)" :errors="subField.state.meta.errors" />
</FieldContent>
</Field>
</form.Field>
<Button
type="button"
variant="outline"
size="sm"
:disabled="field.state.value.length >= 5"
@click="field.pushValue({ address: '' })"
>
Add Email Address
</Button>
</FieldGroup>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</FieldSet>
</form.Field>
</form>
</CardContent>
<CardFooter class="border-t">
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>
<Button type="submit" form="form-tanstack-array">
Save
</Button>
</Field>
</CardFooter>
</Card>
</template>此示例演示了使用数组字段管理多个电子邮件地址。用户最多可以添加5个电子邮件地址,可以删除单个地址,每个地址都会独立验证。
🌐 This example demonstrates managing multiple email addresses with array fields. Users can add up to 5 email addresses, remove individual addresses, and each address is validated independently.
使用 FieldArray
🌐 Using FieldArray
在父字段上使用 mode="array" 以启用数组字段管理。
🌐 Use mode="array" on the parent field to enable array field management.
<template>
<form.Field
name="emails"
mode="array"
#default="{ field }"
>
<FieldSet>
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup>
<template v-for="(_, index) in field.state.value">
<!-- Nested field for each array item -->
</template>
</FieldGroup>
</FieldSet>
</form.Field>
</template>嵌套字段
🌐 Nested Fields
使用括号表示法访问数组中的单个项目:fieldName[index].propertyName。此示例使用 InputGroup 将删除按钮与输入框内联显示。
🌐 Access individual array items using bracket notation: fieldName[index].propertyName. This example uses InputGroup to display the remove button inline with the input.
<template>
<form.Field
:name="`emails[${index}].address`"
#default="{ subField }"
>
<Field orientation="horizontal" :data-invalid="isSubFieldInvalid(subField)">
<FieldContent>
<InputGroup>
<InputGroupInput
:id="`form-tanstack-array-email-${index}`"
:name="subField.name"
:model-value="subField.state.value"
@blur="subField.handleBlur"
@input="subField.handleChange($event.target.value)"
:aria-invalid="isSubFieldInvalid(subField)"
placeholder="name@example.com"
type="email"
/>
<InputGroupAddon v-if="field.state.value.length > 1" align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
@click="field.removeValue(index)"
:aria-label="`Remove email ${index + 1}`"
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<FieldError v-if="isSubFieldInvalid(subField)" :errors="subField.state.meta.errors" />
</FieldContent>
</Field>
</form.Field>
</template>添加项目
🌐 Adding Items
使用 field.pushValue(item) 向数组字段添加项目。当数组达到最大长度时,你可以禁用该按钮。
🌐 Use field.pushValue(item) to add items to an array field. You can disable the button when the array reaches its maximum length.
<template>
<Button
type="button"
variant="outline"
size="sm"
@click="field.pushValue({ address: '' })"
:disabled="field.state.value.length >= 5"
>
Add Email Address
</Button>
</template>移除条目
🌐 Removing Items
使用 field.removeValue(index) 从数组字段中移除项。你可以有条件地仅在有多于一项时显示移除按钮。
🌐 Use field.removeValue(index) to remove items from an array field. You can conditionally show the remove button only when there's more than one item.
<template>
<InputGroupButton
v-if="field.state.value.length > 1"
@click="field.removeValue(index)"
:aria-label="`Remove email ${index + 1}`"
>
<XIcon />
</InputGroupButton>
</template>数组验证
🌐 Array Validation
使用 Zod 的数组方法验证数组字段。
🌐 Validate array fields using Zod's array methods.
<script setup lang="ts">
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.'),
})
</script>