TanStack Form 的核心功能是验证的概念。TanStack Form 使验证高度可定制。
这取决于你!`<Field />` 组件接受一些回调函数作为 props,例如 `onChange` 或 `onBlur`。这些回调函数会接收字段的当前值,以及 `fieldAPI` 对象,以便你可以执行验证。如果你发现验证错误,只需将错误消息作为字符串返回,它将可在 `field.state.meta.errors` 中获取。
这里有一个例子
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<label :for="field.name">Age:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
type="number"
@input="(e) => field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<label :for="field.name">Age:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
type="number"
@input="(e) => field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
在上面的示例中,验证在每次按键时进行(`onChange`)。如果相反,我们希望在字段失去焦点时进行验证,我们将上面的代码修改如下:
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onBlur: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<label :for="field.name">Age:</label>
<!-- We always need to implement onChange, so that TanStack Form receives the changes -->
<!-- Listen to the onBlur event on the field -->
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
type="number"
@blur="field.handleBlur"
@input="(e) => field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onBlur: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<label :for="field.name">Age:</label>
<!-- We always need to implement onChange, so that TanStack Form receives the changes -->
<!-- Listen to the onBlur event on the field -->
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
type="number"
@blur="field.handleBlur"
@input="(e) => field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
因此,您可以通过实现所需的回调函数来控制验证何时完成。您甚至可以在不同时间执行不同的验证部分。
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
onBlur: ({ value }) => (value < 0 ? 'Invalid value' : undefined),
}"
>
<template v-slot="{ field }">
<label :for="field.name">Age:</label>
<!-- We always need to implement onChange, so that TanStack Form receives the changes -->
<!-- Listen to the onBlur event on the field -->
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
type="number"
@blur="field.handleBlur"
@input="(e) => field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
onBlur: ({ value }) => (value < 0 ? 'Invalid value' : undefined),
}"
>
<template v-slot="{ field }">
<label :for="field.name">Age:</label>
<!-- We always need to implement onChange, so that TanStack Form receives the changes -->
<!-- Listen to the onBlur event on the field -->
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
type="number"
@blur="field.handleBlur"
@input="(e) => field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
在上面的示例中,我们在同一字段的不同时间验证不同的内容(每次按键时和失去焦点时)。由于 `field.state.meta.errors` 是一个数组,所有相关错误都会在给定时间显示。你也可以使用 `field.state.meta.errorMap` 来根据验证的*时间*(onChange、onBlur 等)获取错误。有关显示错误的更多信息,请参阅下文。
一旦设置好验证,您就可以映射错误数组以在 UI 中显示。
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<!-- ... -->
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<!-- ... -->
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
或者使用 errorMap 属性来访问您正在寻找的特定错误。
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<!-- ... -->
<em role="alert" v-if="field.state.meta.errorMap['onChange']">{{
field.state.meta.errorMap['onChange']
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<!-- ... -->
<em role="alert" v-if="field.state.meta.errorMap['onChange']">{{
field.state.meta.errorMap['onChange']
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
值得一提的是,我们的 `errors` 数组和 `errorMap` 与验证器返回的类型匹配。这意味着:
<form.Field
name="age"
:validators="{
onChange: ({ value }) => (value < 13 ? { isOldEnough: false } : undefined),
}"
>
<template v-slot="{ field }">
<!-- ... -->
<!-- errorMap.onChange is type `{isOldEnough: false} | undefined` -->
<!-- meta.errors is type `Array<{isOldEnough: false} | undefined>` -->
<em v-if="!field.state.meta.errorMap['onChange']?.isOldEnough">The user is not old enough</em>
</template>
</form.Field>
<form.Field
name="age"
:validators="{
onChange: ({ value }) => (value < 13 ? { isOldEnough: false } : undefined),
}"
>
<template v-slot="{ field }">
<!-- ... -->
<!-- errorMap.onChange is type `{isOldEnough: false} | undefined` -->
<!-- meta.errors is type `Array<{isOldEnough: false} | undefined>` -->
<em v-if="!field.state.meta.errorMap['onChange']?.isOldEnough">The user is not old enough</em>
</template>
</form.Field>
如上所示,每个 `<Field>` 都通过 `onChange`、`onBlur` 等回调函数接受其自身的验证规则。也可以通过将类似的回调函数传递给 `useForm()` 函数来在表单级别(而不是逐个字段)定义验证规则。
示例
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
const form = useForm({
defaultValues: {
age: 0,
},
onSubmit: async ({ value }) => {
console.log(value)
},
validators: {
// Add validators to the form the same way you would add them to a field
onChange({ value }) {
if (value.age < 13) {
return 'Must be 13 or older to sign'
}
return undefined
},
},
})
// Subscribe to the form's error map so that updates to it will render
// alternately, you can use `form.Subscribe`
const formErrorMap = form.useStore((state) => state.errorMap)
</script>
<template>
<!-- ... -->
<div v-if="formErrorMap.onChange">
<em role="alert">
There was an error on the form: {{ formErrorMap.onChange }}
</em>
</div>
<!-- ... -->
</template>
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
const form = useForm({
defaultValues: {
age: 0,
},
onSubmit: async ({ value }) => {
console.log(value)
},
validators: {
// Add validators to the form the same way you would add them to a field
onChange({ value }) {
if (value.age < 13) {
return 'Must be 13 or older to sign'
}
return undefined
},
},
})
// Subscribe to the form's error map so that updates to it will render
// alternately, you can use `form.Subscribe`
const formErrorMap = form.useStore((state) => state.errorMap)
</script>
<template>
<!-- ... -->
<div v-if="formErrorMap.onChange">
<em role="alert">
There was an error on the form: {{ formErrorMap.onChange }}
</em>
</div>
<!-- ... -->
</template>
你可以从表单的验证器中为字段设置错误。一个常见的用例是通过调用表单的 `onSubmitAsync` 验证器来在提交时验证所有字段。
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
const form = useForm({
defaultValues: {
age: 0,
socials: [],
details: {
email: '',
},
},
validators: {
// Add validators to the form the same way you would add them to a field
onSubmitAsync({ value }) {
// Validate the value on the server
const hasErrors = await verifyDataOnServer(value)
if (hasErrors) {
return {
form: 'Invalid data', // The `form` key is optional
fields: {
age: 'Must be 13 or older to sign',
// Set errors on nested fields with the field's name
'socials[0].url': 'The provided URL does not exist',
'details.email': 'An email is required',
},
}
}
return null
},
},
})
</script>
<template>
<!-- ... -->
</template>
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
const form = useForm({
defaultValues: {
age: 0,
socials: [],
details: {
email: '',
},
},
validators: {
// Add validators to the form the same way you would add them to a field
onSubmitAsync({ value }) {
// Validate the value on the server
const hasErrors = await verifyDataOnServer(value)
if (hasErrors) {
return {
form: 'Invalid data', // The `form` key is optional
fields: {
age: 'Must be 13 or older to sign',
// Set errors on nested fields with the field's name
'socials[0].url': 'The provided URL does not exist',
'details.email': 'An email is required',
},
}
}
return null
},
},
})
</script>
<template>
<!-- ... -->
</template>
值得一提的是,如果您有一个返回错误的表单验证函数,该错误可能会被字段特定的验证覆盖。
这意味着:
vue<script setup lang="ts"> import { useForm } from '@tanstack/vue-form' const form = useForm({ defaultValues: { age: 0, }, validators: { onChange: ({ value }) => { return { fields: { age: value.age < 12 ? 'Too young!' : undefined, }, } }, }, }) </script> <template> <!-- ... --> <form.Field name="age" :validators="{ onChange: ({ value }) => (value % 2 === 0 ? 'Must be odd!' : undefined), }" > <template v-slot="{ field }"> <!-- ... --> </template> </form.Field> </template>
<script setup lang="ts"> import { useForm } from '@tanstack/vue-form' const form = useForm({ defaultValues: { age: 0, }, validators: { onChange: ({ value }) => { return { fields: { age: value.age < 12 ? 'Too young!' : undefined, }, } }, }, }) </script> <template> <!-- ... --> <form.Field name="age" :validators="{ onChange: ({ value }) => (value % 2 === 0 ? 'Must be odd!' : undefined), }" > <template v-slot="{ field }"> <!-- ... --> </template> </form.Field> </template>
只会显示 “必须是奇数!”,即使“太年轻!”错误是由表单级别的验证器返回的。
虽然我们认为大多数验证将是同步的,但在许多情况下,网络调用或其他异步操作对于验证很有用。
为了做到这一点,我们有专门的 `onChangeAsync`、`onBlurAsync` 以及其他方法,可用于验证:
<script setup lang="ts">
// ...
const onChangeAge = async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value < 13 ? 'You must be 13 to make an account' : undefined
}
</script>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChangeAsync: onChangeAge,
}"
>
<template v-slot="{ field }">
<label :for="field.name">Age:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
type="number"
@input="
(e) =>
field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
<script setup lang="ts">
// ...
const onChangeAge = async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value < 13 ? 'You must be 13 to make an account' : undefined
}
</script>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChangeAsync: onChangeAge,
}"
>
<template v-slot="{ field }">
<label :for="field.name">Age:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
type="number"
@input="
(e) =>
field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
同步和异步验证可以共存。例如,可以在同一个字段上同时定义 `onBlur` 和 `onBlurAsync`。
<script setup lang="ts">
// ...
const onBlurAge = ({ value }) => (value < 0 ? 'Invalid value' : undefined)
const onBlurAgeAsync = async ({ value }) => {
const currentAge = await fetchCurrentAgeOnProfile()
return value < currentAge ? 'You can only increase the age' : undefined
}
</script>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onBlur: onBlurAge,
onBlurAsync: onBlurAgeAsync,
}"
>
<template v-slot="{ field }">
<label :for="field.name">Age:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
type="number"
@blur="field.handleBlur"
@input="
(e) =>
field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
<script setup lang="ts">
// ...
const onBlurAge = ({ value }) => (value < 0 ? 'Invalid value' : undefined)
const onBlurAgeAsync = async ({ value }) => {
const currentAge = await fetchCurrentAgeOnProfile()
return value < currentAge ? 'You can only increase the age' : undefined
}
</script>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onBlur: onBlurAge,
onBlurAsync: onBlurAgeAsync,
}"
>
<template v-slot="{ field }">
<label :for="field.name">Age:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
type="number"
@blur="field.handleBlur"
@input="
(e) =>
field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="!field.state.meta.isValid">{{
field.state.meta.errors.join(', ')
}}</em>
</template>
</form.Field>
<!-- ... -->
</template>
同步验证方法(`onBlur`)首先运行,而异步方法(`onBlurAsync`)仅在同步方法(`onBlur`)成功时运行。要更改此行为,请将 `asyncAlways` 选项设置为 `true`,然后异步方法将无论同步方法的结果如何都会运行。
虽然在验证数据库时异步调用是最佳选择,但在每次按键时运行网络请求会很容易导致您的数据库遭受拒绝服务攻击(DDOS)。
相反,我们通过添加一个简单的属性来启用一种简便的方法来对 async 调用进行去抖动。
<template>
<!-- ... -->
<form.Field
name="age"
:async-debounce-ms="500"
:validators="{
onChangeAsync: async ({ value }) => {
// ...
},
}"
>
<template v-slot="{ field }">
<!-- ... -->
</template>
</form.Field>
<!-- ... -->
</template>
<template>
<!-- ... -->
<form.Field
name="age"
:async-debounce-ms="500"
:validators="{
onChangeAsync: async ({ value }) => {
// ...
},
}"
>
<template v-slot="{ field }">
<!-- ... -->
</template>
</form.Field>
<!-- ... -->
</template>
这将以 500 毫秒的延迟对每个异步调用进行去抖动。您甚至可以在每个验证属性上覆盖此属性。
<template>
<!-- ... -->
<form.Field
name="age"
:async-debounce-ms="500"
:validators="{
onChangeAsyncDebounceMs: 1500,
onChangeAsync: async ({ value }) => {
// ...
},
onBlurAsync: async ({ value }) => {
// ...
},
}"
>
<template v-slot="{ field }">
<!-- ... -->
</template>
</form.Field>
<!-- ... -->
</template>
<template>
<!-- ... -->
<form.Field
name="age"
:async-debounce-ms="500"
:validators="{
onChangeAsyncDebounceMs: 1500,
onChangeAsync: async ({ value }) => {
// ...
},
onBlurAsync: async ({ value }) => {
// ...
},
}"
>
<template v-slot="{ field }">
<!-- ... -->
</template>
</form.Field>
<!-- ... -->
</template>
这将每 1500 毫秒运行一次 `onChangeAsync`,而 `onBlurAsync` 将每 500 毫秒运行一次。
虽然函数提供了更灵活的自定义验证方式,但它们可能有些冗长。为了解决这个问题,有一些库提供了基于模式的验证,可以使简写和类型严格的验证变得更加容易。你也可以为整个表单定义一个模式,并将其传递给表单级别,错误将自动传播到字段。
TanStack Form 原生地支持所有遵循 Standard Schema 规范 的库,最值得注意的是:
注意:请确保使用最新版本的模式库,因为旧版本可能尚不支持 Standard Schema。
验证不会为你提供转换后的值。有关更多信息,请参阅 提交处理。
要使用这些库中的模式,您可以像使用自定义函数一样将它们传递给 validators props。
<script setup lang="ts">
import { z } from 'zod'
// ...
const form = useForm({
// ...
})
</script>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: z.number().gte(13, 'You must be 13 to make an account'),
}"
>
<template v-slot="{ field }">
<!-- ... -->
</template>
</form.Field>
<!-- ... -->
</template>
<script setup lang="ts">
import { z } from 'zod'
// ...
const form = useForm({
// ...
})
</script>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: z.number().gte(13, 'You must be 13 to make an account'),
}"
>
<template v-slot="{ field }">
<!-- ... -->
</template>
</form.Field>
<!-- ... -->
</template>
表单和字段级别的异步验证也受支持。
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: z.number().gte(13, 'You must be 13 to make an account'),
onChangeAsyncDebounceMs: 500,
onChangeAsync: z.number().refine(
async (value) => {
const currentAge = await fetchCurrentAgeOnProfile()
return value >= currentAge
},
{
message: 'You can only increase the age',
},
),
}"
>
<template v-slot="{ field }">
<!-- ... -->
</template>
</form.Field>
<!-- ... -->
</template>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: z.number().gte(13, 'You must be 13 to make an account'),
onChangeAsyncDebounceMs: 500,
onChangeAsync: z.number().refine(
async (value) => {
const currentAge = await fetchCurrentAgeOnProfile()
return value >= currentAge
},
{
message: 'You can only increase the age',
},
),
}"
>
<template v-slot="{ field }">
<!-- ... -->
</template>
</form.Field>
<!-- ... -->
</template>
如果您需要对 Standard Schema 验证进行更精细地控制,可以像这样将 Standard Schema 与回调函数结合使用。
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: ({ value, fieldApi }) => {
const errors = fieldApi.parseValueWithSchema(
z.number().gte(13, 'You must be 13 to make an account'),
)
if (errors) return errors
// continue with your validation
},
}"
>
<template v-slot="{ field }">
<!-- ... -->
</template>
</form.Field>
<!-- ... -->
</template>
<template>
<!-- ... -->
<form.Field
name="age"
:validators="{
onChange: ({ value, fieldApi }) => {
const errors = fieldApi.parseValueWithSchema(
z.number().gte(13, 'You must be 13 to make an account'),
)
if (errors) return errors
// continue with your validation
},
}"
>
<template v-slot="{ field }">
<!-- ... -->
</template>
</form.Field>
<!-- ... -->
</template>
`onChange`、`onBlur` 等回调函数在表单提交时也会运行,并且如果表单无效,提交将被阻止。
表单状态对象有一个 `canSubmit` 标志,当任何字段无效且表单已被触碰时,此标志为 false(在表单被触碰之前,`canSubmit` 始终为 true,即使某些字段“技术上”无效,具体取决于它们的 `onChange` / `onBlur` props)。
你可以通过 `form.Subscribe` 订阅它,并使用该值来禁用提交按钮(例如,当表单无效时)。(实际上,禁用的按钮不可访问,请使用 `aria-disabled` 代替)。
<script setup lang="ts">
const form = useForm(/* ... */)
</script>
<template>
<!-- ... -->
<!-- Dynamic submit button -->
<form.Subscribe>
<template v-slot="{ canSubmit, isSubmitting }">
<button type="submit" :disabled="!canSubmit">
{{ isSubmitting ? '...' : 'Submit' }}
</button>
</template>
</form.Subscribe>
<!-- ... -->
</template>
<script setup lang="ts">
const form = useForm(/* ... */)
</script>
<template>
<!-- ... -->
<!-- Dynamic submit button -->
<form.Subscribe>
<template v-slot="{ canSubmit, isSubmitting }">
<button type="submit" :disabled="!canSubmit">
{{ isSubmitting ? '...' : 'Submit' }}
</button>
</template>
</form.Subscribe>
<!-- ... -->
</template>
您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。