TanStack Form 功能的核心是验证概念。TanStack Form 使验证高度可定制:
这取决于您!<form.Field /> 组件接受一些回调作为属性,如 onChange 或 onBlur。这些回调会传递字段的当前值以及 fieldAPI 对象,以便您可以执行验证。如果您发现验证错误,只需将错误消息作为字符串返回,它将在 field.state.meta.errors 中可用。
以下是一个示例:
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 13 ? '您必须年满 13 岁才能创建账户' : undefined,
}}
>
{#snippet children(field)}
<label for={field.name}>年龄:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onchange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 13 ? '您必须年满 13 岁才能创建账户' : undefined,
}}
>
{#snippet children(field)}
<label for={field.name}>年龄:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onchange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
在上面的示例中,验证在每次按键时执行(onchange)。如果我们希望在字段失焦时执行验证,我们会像这样更改上面的代码:
<form.Field
name="age"
validators={{
onBlur: ({ value }) =>
value < 13 ? '您必须年满 13 岁才能创建账户' : undefined,
}}
>
{#snippet children(field)}
<label for={field.name}>年龄:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onblur={field.handleBlur}
onchange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
<form.Field
name="age"
validators={{
onBlur: ({ value }) =>
value < 13 ? '您必须年满 13 岁才能创建账户' : undefined,
}}
>
{#snippet children(field)}
<label for={field.name}>年龄:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onblur={field.handleBlur}
onchange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
因此,您可以通过实现所需的回调来控制何时执行验证。您甚至可以在不同时间执行不同的验证:
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 13 ? '您必须年满 13 岁才能创建账户' : undefined,
onBlur: ({ value }) => (value < 0 ? '无效值' : undefined),
}}
>
{#snippet children(field)}
<label for={field.name}>年龄:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onblur={field.handleBlur}
onchange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 13 ? '您必须年满 13 岁才能创建账户' : undefined,
onBlur: ({ value }) => (value < 0 ? '无效值' : undefined),
}}
>
{#snippet children(field)}
<label for={field.name}>年龄:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onblur={field.handleBlur}
onchange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
在上面的示例中,我们在不同时间对同一字段验证不同的内容(在每次按键时和字段失焦时)。由于 field.state.meta.errors 是一个数组,所有相关的错误都会在给定时间显示。您还可以使用 field.state.meta.errorMap 根据验证执行的_时间_(onChange、onBlur 等)获取错误。有关显示错误的更多信息请参见下文。
一旦您设置了验证,您可以将错误从数组映射到 UI 中显示:
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 13 ? '您必须年满 13 岁才能创建账户' : undefined,
}}
>
{#snippet children(field)}
<!-- ... -->
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 13 ? '您必须年满 13 岁才能创建账户' : undefined,
}}
>
{#snippet children(field)}
<!-- ... -->
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
或使用 errorMap 属性访问您要查找的特定错误:
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 13 ? '您必须年满 13 岁才能创建账户' : undefined,
}}
>
{#snippet children(field)}
<!-- ... -->
{#if field.state.meta.errorMap['onChange']}
<em role="alert">{field.state.meta.errorMap['onChange']}</em>
{/if}
{/snippet}
</form.Field>
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 13 ? '您必须年满 13 岁才能创建账户' : undefined,
}}
>
{#snippet children(field)}
<!-- ... -->
{#if field.state.meta.errorMap['onChange']}
<em role="alert">{field.state.meta.errorMap['onChange']}</em>
{/if}
{/snippet}
</form.Field>
值得一提的是,我们的 errors 数组和 errorMap 与验证器返回的类型匹配。这意味着:
<form.Field
name="age"
validators={{
onChange: ({ value }) => (value < 13 ? { isOldEnough: false } : undefined),
}}
>
{#snippet children(field)}
<!-- ... -->
<!-- errorMap.onChange is type `{isOldEnough: false} | undefined` -->
<!-- meta.errors is type `Array<{isOldEnough: false} | undefined>` -->
{#if field.state.meta.errorMap['onChange']?.isOldEnough}
<em>用户年龄不够</em>
{/if}
{/snippet}
</form.Field>
<form.Field
name="age"
validators={{
onChange: ({ value }) => (value < 13 ? { isOldEnough: false } : undefined),
}}
>
{#snippet children(field)}
<!-- ... -->
<!-- errorMap.onChange is type `{isOldEnough: false} | undefined` -->
<!-- meta.errors is type `Array<{isOldEnough: false} | undefined>` -->
{#if field.state.meta.errorMap['onChange']?.isOldEnough}
<em>用户年龄不够</em>
{/if}
{/snippet}
</form.Field>
如上所示,每个 <form.Field> 通过 onChange、onBlur 等回调接受自己的验证规则。也可以通过向 createForm() 钩子传递类似的回调来在表单级别(而不是逐个字段)定义验证规则。
示例:
<script>
import { createForm } from '@tanstack/svelte-form'
const form = createForm(() => ({
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 '必须年满 13 岁才能注册'
}
return undefined
},
},
}))
// 订阅表单的错误映射��以便对其的更新会渲染
// 或者,您可以使用 `form.Subscribe`
const formErrorMap = form.useStore((state) => state.errorMap)
</script>
<div>
<!-- ... -->
{#if formErrorMap.current.onChange}
<div>
<em>表单出现错误:{formErrorMap.current.onChange}</em>
</div>
{/if}
<!-- ... -->
</div>
<script>
import { createForm } from '@tanstack/svelte-form'
const form = createForm(() => ({
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 '必须年满 13 岁才能注册'
}
return undefined
},
},
}))
// 订阅表单的错误映射,以便对其的更新会渲染
// 或者,您可以使用 `form.Subscribe`
const formErrorMap = form.useStore((state) => state.errorMap)
</script>
<div>
<!-- ... -->
{#if formErrorMap.current.onChange}
<div>
<em>表单出现错误:{formErrorMap.current.onChange}</em>
</div>
{/if}
<!-- ... -->
</div>
虽然我们认为大多数验证都是同步的,但在许多情况下,网络调用或其他异步操作对验证很有用。
为此,我们有专门的 onChangeAsync、onBlurAsync 和其他可用于验证的方法:
<form.Field
name="age"
validators={{
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value < 13 ? '您必须年满 13 岁才能创建账户' : undefined
},
}}
>
{#snippet children(field)}
<label for={field.name}>年龄:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onchange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
<form.Field
name="age"
validators={{
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value < 13 ? '您必须年满 13 岁才能创建账户' : undefined
},
}}
>
{#snippet children(field)}
<label for={field.name}>年龄:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onchange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
同步和异步验证可以共存。例如,可以在同一字段上定义 onBlur 和 onBlurAsync:
<form.Field
name="age"
validators={{
onBlur: ({ value }) => (value < 13 ? '您必须至少年满 13 岁' : undefined),
onBlurAsync: async ({ value }) => {
const currentAge = await fetchCurrentAgeOnProfile()
return value < currentAge ? '您只能增加年龄' : undefined
},
}}
>
{#snippet children(field)}
<label for={field.name}>年龄:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onblur={field.handleBlur}
onchange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
<form.Field
name="age"
validators={{
onBlur: ({ value }) => (value < 13 ? '您必须至少年满 13 岁' : undefined),
onBlurAsync: async ({ value }) => {
const currentAge = await fetchCurrentAgeOnProfile()
return value < currentAge ? '您只能增加年龄' : undefined
},
}}
>
{#snippet children(field)}
<label for={field.name}>年龄:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
onblur={field.handleBlur}
onchange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{#if field.state.meta.errors}
<em role="alert">{field.state.meta.errors.join(', ')}</em>
{/if}
{/snippet}
</form.Field>
同步验证方法(onBlur)首先运行,异步方法(onBlurAsync)仅在同步方法(onBlur)成功时运行。要更改此行为,请将 asyncAlways 选项设置为 true,异步方法将无论同步方法的结果如何都会运行。
虽然异步调用是针对数据库进行验证的方法,但在每次按键时运行网络请求是 DDOS 数据库的好方法。
相反,我们通过添加单个属性来启用对 async 调用进行防抖的简单方法:
<form.Field
name="age"
asyncDebounceMs={500}
validators={{
onChangeAsync: async ({ value }) => {
// ...
},
}}
>
<!-- ... -->
</form.Field>
<form.Field
name="age"
asyncDebounceMs={500}
validators={{
onChangeAsync: async ({ value }) => {
// ...
},
}}
>
<!-- ... -->
</form.Field>
这将以 500ms 延迟对每个异步调用进行防抖。您甚至可以在每个验证属性上覆盖此属性:
<form.Field
name="age"
asyncDebounceMs={500}
validators={{
onChangeAsyncDebounceMs: 1500,
onChangeAsync: async ({ value }) => {
// ...
},
onBlurAsync: async ({ value }) => {
// ...
},
}}
>
<!-- ... -->
</form.Field>
<form.Field
name="age"
asyncDebounceMs={500}
validators={{
onChangeAsyncDebounceMs: 1500,
onChangeAsync: async ({ value }) => {
// ...
},
onBlurAsync: async ({ value }) => {
// ...
},
}}
>
<!-- ... -->
</form.Field>
这将每 1500ms 运行一次 onChangeAsync,而 onBlurAsync 将每 500ms 运行一次。
虽然函数为您的验证提供了更多的灵活性和自定义,但它们可能有点冗长。为了帮助解决这个问题,有一些库提供基于模式的验证,使简写和类型严格的验证变得更加容易。您还可以为整个表单定义单个模式并将其传递到表单级别,错误将自动传播到字段。
TanStack Form 原生支持所有遵循 Standard Schema 规范 的库,最著名的是:
注意: 确保使用模式库的最新版本,因为旧版本可能还不支持 Standard Schema。
要使用这些库中的模式,您可以将它们传递给 validators 属性,就像使用自定义函数一样:
<script>
import { z } from 'zod'
// ...
const form = createForm(() => ({
// ...
}))
</script>
<form.Field
name="age"
validators={{
onChange: z.number().gte(13, '您必须年满 13 岁才能创建账户'),
}}
>
<!-- ... -->
</form.Field>
<script>
import { z } from 'zod'
// ...
const form = createForm(() => ({
// ...
}))
</script>
<form.Field
name="age"
validators={{
onChange: z.number().gte(13, '您必须年满 13 岁才能创建账户'),
}}
>
<!-- ... -->
</form.Field>
表单和字段级别的异步验证也受支持:
<form.Field
name="age"
validators={{
onChange: z.number().gte(13, '您必须年满 13 岁才能创建账户'),
onChangeAsyncDebounceMs: 500,
onChangeAsync: z.number().refine(
async (value) => {
const currentAge = await fetchCurrentAgeOnProfile()
return value >= currentAge
},
{
message: '您只能增加年龄',
},
),
}}
>
<!-- ... -->
</form.Field>
<form.Field
name="age"
validators={{
onChange: z.number().gte(13, '您必须年满 13 岁才能创建账户'),
onChangeAsyncDebounceMs: 500,
onChangeAsync: z.number().refine(
async (value) => {
const currentAge = await fetchCurrentAgeOnProfile()
return value >= currentAge
},
{
message: '您只能增加年龄',
},
),
}}
>
<!-- ... -->
</form.Field>
当提交表单时,onChange、onBlur 等回调也会运行,如果表单无效,提交将被阻止。
表单状态对象有一个 canSubmit 标志,当任何字段无效且表单已被触摸时为 false(canSubmit 在表单被触摸之前为 true,即使某些字段根据其 onChange/onBlur 属性在"技术上"无效)。
您可以通过 form.Subscribe 订阅它并使用该值,例如,在表单无效时禁用��交按钮(在实践中,禁用的按钮不可访问,请改用 aria-disabled)。
<script>
import { createForm } from '@tanstack/svelte-form'
const form = createForm(() => ({
/* ... */
}))
</script>
<!-- ... -->
<!-- 动态提交按钮 -->
<form.Subscribe
selector={(state) => ({
canSubmit: state.canSubmit,
isSubmitting: state.isSubmitting,
})}
children={(state) => (
<button type="submit" disabled={!state().canSubmit}>
{state().isSubmitting ? '...' : '提交'}
</button>
)}
>
{#snippet children(state)}
<button type="submit" disabled={!state.canSubmit}>
{state.isSubmitting ? '...' : '提交'}
</button>
{/snippet}
</form.Subscribe>
<script>
import { createForm } from '@tanstack/svelte-form'
const form = createForm(() => ({
/* ... */
}))
</script>
<!-- ... -->
<!-- 动态提交按钮 -->
<form.Subscribe
selector={(state) => ({
canSubmit: state.canSubmit,
isSubmitting: state.isSubmitting,
})}
children={(state) => (
<button type="submit" disabled={!state().canSubmit}>
{state().isSubmitting ? '...' : '提交'}
</button>
)}
>
{#snippet children(state)}
<button type="submit" disabled={!state.canSubmit}>
{state.isSubmitting ? '...' : '提交'}
</button>
{/snippet}
</form.Subscribe>