- 手风琴 (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)
本指南探讨了如何使用 TanStack Form 构建表单。您将学习如何使用 <Field /> 组件创建表单,实现 Zod 模式验证,处理错误,并确保可访问性。
演示
我们将从构建以下表单开始。它包含一个简单的文本输入框和一个文本区域。提交时,我们将验证表单数据并显示任何错误。
注意:为了本演示的目的,我们特意禁用了浏览器验证,以展示如何在 TanStack Form 中实现模式验证和表单错误处理。建议在生产代码中添加基础的浏览器验证。
漏洞报告
报告您遇到的错误,帮助我们改进。
<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>方法
此表单利用 TanStack Form 进行强大、无头(headless)的表单处理。我们将使用 <Field /> 组件构建表单,它为您提供了对标记(markup)和样式完全的灵活性。
- 使用 TanStack Form 的
useForm可组合项进行表单状态管理。 form.Field组件结合渲染属性模式(render prop pattern)以实现受控输入。- 用于构建可访问表单的
<Field />组件。 - 使用 Zod 进行客户端验证。
- 实时验证反馈。
结构说明
这是使用 TanStack Form 和 <Field /> 组件构建表单的基础示例。
<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>表单
创建表单架构
我们将从使用 Zod 模式定义表单结构开始。
注意:此示例使用 zod v3 进行模式验证。TanStack Form 通过其验证器 API 与 Zod 及其他标准模式(Standard Schema)验证库无缝集成。
<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>设置表单
使用来自 TanStack Form 的 useForm 可组合项,通过 Zod 验证创建您的表单实例。
<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 支持其他验证模式,您可以在文档中阅读相关内容。
构建表单
现在我们可以使用来自 TanStack Form 的 form.Field 组件和 Field 组件来构建表单了。
<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>完成
就是这样。您现在拥有了一个具备客户端验证功能的完全可访问的表单。
当您提交表单时,onSubmit 函数将接收验证后的表单数据并被调用。如果表单数据无效,TanStack Form 将在每个字段旁边显示错误信息。
验证
客户端验证
TanStack Form 使用 Zod 模式验证您的表单数据。验证会在用户输入时实时发生。
<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>验证模式
TanStack Form 通过 validators 选项支持不同的验证策略
| 模式 | 描述 |
|---|---|
onChange | 在每次更改时触发验证。 |
onBlur | 在失去焦点时触发验证。 |
onSubmit | 在提交时触发验证。 |
<script setup lang="ts">
const form = useForm({
defaultValues: {
title: '',
description: '',
},
validators: {
onSubmit: formSchema,
onChange: formSchema,
onBlur: formSchema,
},
})
</script>显示错误
使用 FieldError 在字段旁边显示错误。为了样式和可访问性:
- 将
: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>使用不同的字段类型
输入框
对于输入字段,在 Input 组件上使用 field.state.value 和 field.handleChange。要显示错误,请将 :aria-invalid 属性添加到 Input 组件,并将 :data-invalid 属性添加到 Field 组件中。
个人资料设置
在下方更新您的个人资料信息。
<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 组件上使用 field.state.value 和 field.handleChange。要显示错误,请将 :aria-invalid 属性添加到 Textarea 组件,并将 :data-invalid 属性添加到 Field 组件中。
个性化
通过告诉我们更多关于您的信息来定制您的体验。
<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 组件上使用 field.state.value 和 field.handleChange。要显示错误,请将 :aria-invalid 属性添加到 SelectTrigger 组件,并将 :data-invalid 属性添加到 Field 组件中。
语言偏好
选择您的首选口语。
<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="(v) => field.handleChange(v as string)"
>
<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 组件上使用 field.state.value 和 field.handleChange。要显示错误,请将 :aria-invalid 属性添加到 Checkbox 组件,并将 :data-invalid 属性添加到 Field 组件中。对于复选框数组,在 form.Field 组件上使用 mode="array" 以及 TanStack Form 的数组助手。记得在 FieldGroup 组件上添加 data-slot="checkbox-group" 以获得正确的样式和间距。
通知
管理您的通知偏好。
<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>单选框组
对于单选组,在 RadioGroup 组件上使用 field.state.value 和 field.handleChange。要显示错误,请将 :aria-invalid 属性添加到 RadioGroupItem 组件,并将 :data-invalid 属性添加到 Field 组件中。
订阅计划
查看每个方案的定价和功能。
<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="(v) => field.handleChange(v as string)"
>
<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。要显示错误,请将 :aria-invalid 属性添加到 Switch 组件,并将 :data-invalid 属性添加到 Field 组件中。
安全设置
管理您的账户安全偏好。
<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>复杂表单
这是一个具有多个字段和验证的更复杂表单示例。
<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="(v) => field.handleChange(v as string)"
>
<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="(v) => field.handleChange(v as string)"
>
<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: boolean | 'indeterminate') => {
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>重置表单
使用 form.reset() 将表单重置为默认值。
<template>
<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>
</template>数组字段
TanStack Form 通过 mode="array" 提供了强大的数组字段管理功能。这允许您动态添加、删除和更新数组项,并提供完整的验证支持。
联系邮箱
管理您的联系电子邮件地址。
<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 个电子邮件地址,可以删除单个地址,并且每个地址都独立验证。
使用 FieldArray
在父字段上使用 mode="array" 来启用数组字段管理。
<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>嵌套字段
使用括号表示法访问单个数组项:fieldName[index].propertyName。此示例使用 InputGroup 在输入框内联显示删除按钮。
<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>添加项
使用 field.pushValue(item) 将项添加到数组字段。当数组达到最大长度时,您可以禁用该按钮。
<template>
<Button
type="button"
variant="outline"
size="sm"
@click="field.pushValue({ address: '' })"
:disabled="field.state.value.length >= 5"
>
Add Email Address
</Button>
</template>移除项
使用 field.removeValue(index) 从数组字段中移除项。您可以仅在有多于一项时才显示移除按钮。
<template>
<InputGroupButton
v-if="field.state.value.length > 1"
@click="field.removeValue(index)"
:aria-label="`Remove email ${index + 1}`"
>
<XIcon />
</InputGroupButton>
</template>数组验证
使用 Zod 的数组方法验证数组字段。
<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>