表单和字段验证

TanStack Form 功能的核心是验证概念。TanStack Form 使验证高度可定制:

  • 您可以控制何时执行验证(在更改时、输入时、失焦时、提交时...)
  • 验证规则可以在字段级别或表单级别定义
  • 验证可以是同步的或异步的(例如,作为 API 调用的结果)

何时执行验证?

这取决于您!<form.Field /> 组件接受一些回调作为属性,如 onChangeonBlur。这些回调会传递字段的当前值以及 fieldAPI 对象,以便您可以执行验证。如果您发现验证错误,只需将错误消息作为字符串返回,它将在 field.state.meta.errors 中可用。

以下是一个示例:

svelte
<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)。如果我们希望在字段失焦时执行验证,我们会像这样更改上面的代码:

svelte
<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>

因此,您可以通过实现所需的回调来控制何时执行验证。您甚至可以在不同时间执行不同的验证:

svelte
<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 中显示:

svelte
<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 属性访问您要查找的特定错误:

svelte
<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 与验证器返回的类型匹配。这意味着:

svelte
<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> 通过 onChangeonBlur 等回调接受自己的验证规则。也可以通过向 createForm() 钩子传递类似的回调来在表单级别(而不是逐个字段)定义验证规则。

示例:

svelte
<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>

异步函数验证

虽然我们认为大多数验证都是同步的,但在许多情况下,网络调用或其他异步操作对验证很有用。

为此,我们有专门的 onChangeAsynconBlurAsync 和其他可用于验证的方法:

svelte
<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>

同步和异步验证可以共存。例如,可以在同一字段上定义 onBluronBlurAsync

svelte
<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 调用进行防抖的简单方法:

svelte
<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 延迟对每个异步调用进行防抖。您甚至可以在每个验证属性上覆盖此属性:

svelte
<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 属性,就像使用自定义函数一样:

svelte
<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>

表单和字段级别的异步验证也受支持:

svelte
<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>

防止提交无效表单

当提交表单时,onChangeonBlur 等回调也会运行,如果表单无效,提交将被阻止。

表单状态对象有一个 canSubmit 标志,当任何字段无效且表单已被触摸时为 false(canSubmit 在表单被触摸之前为 true,即使某些字段根据其 onChange/onBlur 属性在"技术上"无效)。

您可以通过 form.Subscribe 订阅它并使用该值,例如,在表单无效时禁用��交按钮(在实践中,禁用的按钮不可访问,请改用 aria-disabled)。

svelte
<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>