TanStack Form 的核心功能是验证的概念。TanStack Form 使验证高度可定制。
这取决于你!field() 方法接受一些回调作为验证器,例如onChange 或 onBlur。这些回调会接收字段的当前值以及 fieldAPI 对象,以便你执行验证。如果你发现验证错误,只需返回一个字符串形式的错误消息,它将可用在 field.state.meta.errors 中。
这里有一个例子
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
},
},
(field) => {
return html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert">${field.state.meta.errors.join(', ')}</em>`
: nothing}
`
},
)}`
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
},
},
(field) => {
return html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert">${field.state.meta.errors.join(', ')}</em>`
: nothing}
`
},
)}`
在上面的示例中,验证是在每次击键时完成的(onChange)。如果相反,我们希望在字段失去焦点时进行验证,我们可以像这样修改上面的代码
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onBlur: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
},
},
(field) => {
return html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@blur="${() => field.handleBlur()}"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert">${field.state.meta.errors.join(', ')}</em>`
: nothing}
`
},
)}`
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onBlur: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
},
},
(field) => {
return html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@blur="${() => field.handleBlur()}"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert">${field.state.meta.errors.join(', ')}</em>`
: nothing}
`
},
)}`
因此,您可以通过实现所需的回调函数来控制验证何时完成。您甚至可以在不同时间执行不同的验证部分。
import { html, nothing } from 'lit'
;`${this.#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),
},
},
(field) => {
return html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@blur="${() => field.handleBlur()}"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert">${field.state.meta.errors.join(', ')}</em>`
: nothing}
`
},
)}`
import { html, nothing } from 'lit'
;`${this.#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),
},
},
(field) => {
return html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@blur="${() => field.handleBlur()}"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert">${field.state.meta.errors.join(', ')}</em>`
: nothing}
`
},
)}`
在上面的示例中,我们在不同时间(每次击键时和失去焦点时)对同一个字段进行不同的验证。由于 field.state.meta.errors 是一个数组,所有在给定时间相关的错误都会被显示。你还可以使用 field.state.meta.errorMap 来根据验证发生的时间(onChange、onBlur 等)获取错误。更多关于显示错误的信息见下文。
一旦设置好验证,您就可以映射错误数组以在 UI 中显示。
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
},
},
(field) => {
return html`
<!-- ... -->
${!field.state.meta.isValid
? html`<em>${field.state.meta.errors.join(',')}</em>`
: nothing}
`
},
)}`
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
},
},
(field) => {
return html`
<!-- ... -->
${!field.state.meta.isValid
? html`<em>${field.state.meta.errors.join(',')}</em>`
: nothing}
`
},
)}`
或者使用 errorMap 属性来访问您正在寻找的特定错误。
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
},
},
(field) => {
return html`
<!-- ... -->
${field.state.meta.errorMap['onChange']
? html`<em>${field.state.meta.errorMap['onChange']}</em>`
: nothing}
`
},
)}`
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onChange: ({ value }) =>
value < 13 ? 'You must be 13 to make an account' : undefined,
},
},
(field) => {
return html`
<!-- ... -->
${field.state.meta.errorMap['onChange']
? html`<em>${field.state.meta.errorMap['onChange']}</em>`
: nothing}
`
},
)}`
值得一提的是,我们的 errors 数组和 errorMap 与验证器返回的类型匹配。这意味着
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onChange: ({ value }) => (value < 13 ? { isOldEnough: false } : undefined),
},
},
(field) => {
return html`
<!-- ... -->
<!-- errorMap.onChange is type `{isOldEnough: false} | undefined` -->
<!-- meta.errors is type `Array<{isOldEnough: false} | undefined>` -->
${!field.state.meta.errorMap['onChange']?.isOldEnough
? html`<em>The user is not old enough</em>`
: nothing}
`
},
)}`
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onChange: ({ value }) => (value < 13 ? { isOldEnough: false } : undefined),
},
},
(field) => {
return html`
<!-- ... -->
<!-- errorMap.onChange is type `{isOldEnough: false} | undefined` -->
<!-- meta.errors is type `Array<{isOldEnough: false} | undefined>` -->
${!field.state.meta.errorMap['onChange']?.isOldEnough
? html`<em>The user is not old enough</em>`
: nothing}
`
},
)}`
如上所示,每个字段都通过 onChange、onBlur 等回调接受其自身的验证规则。也可以通过将类似的回调传递给 TanStackFormController 构造函数来定义表单级别的验证规则(而不是逐个字段定义)。
示例
import { LitElement, html, nothing } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'
@customElement('my-form')
export class MyForm extends LitElement {
#form = new TanStackFormController(this, {
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
},
},
})
render() {
return html`
<div>
<!-- ... -->
${this.#form.api.state.errorMap.onChange
? html`
<div>
<em
>There was an error on the form:
${this.#form.api.state.errorMap.onChange}</em
>
</div>
`
: nothing}
<!-- ... -->
</div>
`
}
}
import { LitElement, html, nothing } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'
@customElement('my-form')
export class MyForm extends LitElement {
#form = new TanStackFormController(this, {
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
},
},
})
render() {
return html`
<div>
<!-- ... -->
${this.#form.api.state.errorMap.onChange
? html`
<div>
<em
>There was an error on the form:
${this.#form.api.state.errorMap.onChange}</em
>
</div>
`
: nothing}
<!-- ... -->
</div>
`
}
}
你可以从表单的验证器中设置字段的错误。一个常见的用例是在提交时通过调用表单的 onSubmitAsync 验证器中的单个 API 端点来验证所有字段。
import { LitElement, html, nothing } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'
@customElement('my-form')
export class MyForm extends LitElement {
#form = new TanStackFormController(this, {
defaultValues: {
age: 0,
socials: [],
details: {
email: '',
},
},
validators: {
onSubmitAsync: async ({ 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
},
},
})
render() {
return html`
<div>
<form
@submit="${(e: Event) => {
e.preventDefault()
e.stopPropagation()
this.#form.api.handleSubmit()
}}"
>
${this.#form.field(
{ name: 'age' },
(field) => html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert"
>${field.state.meta.errors.join(', ')}</em
>`
: nothing}
`,
)}
${this.#form.api.state.errorMap.onSubmit
? html`
<div>
<em
>There was an error on the form:
${this.#form.api.state.errorMap.onSubmit}</em
>
</div>
`
: nothing}
<!--...-->
</form>
</div>
`
}
}
import { LitElement, html, nothing } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'
@customElement('my-form')
export class MyForm extends LitElement {
#form = new TanStackFormController(this, {
defaultValues: {
age: 0,
socials: [],
details: {
email: '',
},
},
validators: {
onSubmitAsync: async ({ 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
},
},
})
render() {
return html`
<div>
<form
@submit="${(e: Event) => {
e.preventDefault()
e.stopPropagation()
this.#form.api.handleSubmit()
}}"
>
${this.#form.field(
{ name: 'age' },
(field) => html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert"
>${field.state.meta.errors.join(', ')}</em
>`
: nothing}
`,
)}
${this.#form.api.state.errorMap.onSubmit
? html`
<div>
<em
>There was an error on the form:
${this.#form.api.state.errorMap.onSubmit}</em
>
</div>
`
: nothing}
<!--...-->
</form>
</div>
`
}
}
值得一提的是,如果您有一个返回错误的表单验证函数,该错误可能会被字段特定的验证覆盖。
这意味着:
tsconst form = new TanStackFormController(this, { defaultValues: { age: 0, }, validators: { onChange: ({ value }) => { return { fields: { age: value.age < 12 ? 'Too young!' : undefined, }, } }, }, }) // ... return html` ${this.#form.field( { name: 'age', validators: { onChange: ({ value }) => value % 2 === 0 ? 'Must be odd!' : undefined, }, }, () => html`<!-- ... -->`, )} `
const form = new TanStackFormController(this, { defaultValues: { age: 0, }, validators: { onChange: ({ value }) => { return { fields: { age: value.age < 12 ? 'Too young!' : undefined, }, } }, }, }) // ... return html` ${this.#form.field( { name: 'age', validators: { onChange: ({ value }) => value % 2 === 0 ? 'Must be odd!' : undefined, }, }, () => html`<!-- ... -->`, )} `
只会显示 “必须是奇数!”,即使“太年轻!”错误是由表单级别的验证器返回的。
虽然我们认为大多数验证将是同步的,但在许多情况下,网络调用或其他异步操作对于验证很有用。
为了做到这一点,我们提供了专用的 onChangeAsync、onBlurAsync 等方法,可以用来验证
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value < 13 ? 'You must be 13 to make an account' : undefined
},
},
},
(field) => {
return html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert">${field.state.meta.errors.join(', ')}</em>`
: nothing}
`
},
)}`
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value < 13 ? 'You must be 13 to make an account' : undefined
},
},
},
(field) => {
return html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert">${field.state.meta.errors.join(', ')}</em>`
: nothing}
`
},
)}`
同步和异步验证可以共存。例如,可以在同一个字段上同时定义 onBlur 和 onBlurAsync。
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onBlur: ({ value }) =>
value < 13 ? 'You must be at least 13' : undefined,
onBlurAsync: async ({ value }) => {
const currentAge = await fetchCurrentAgeOnProfile()
return value < currentAge ? 'You can only increase the age' : undefined
},
},
},
(field) => {
return html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@blur="${() => field.handleBlur()}"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert">${field.state.meta.errors.join(', ')}</em>`
: nothing}
`
},
)}`
import { html, nothing } from 'lit'
;`${this.#form.field(
{
name: 'age',
validators: {
onBlur: ({ value }) =>
value < 13 ? 'You must be at least 13' : undefined,
onBlurAsync: async ({ value }) => {
const currentAge = await fetchCurrentAgeOnProfile()
return value < currentAge ? 'You can only increase the age' : undefined
},
},
},
(field) => {
return html`
<label for="${field.name}">Age:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
type="number"
@blur="${() => field.handleBlur()}"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.valueAsNumber)
}}"
/>
${!field.state.meta.isValid
? html`<em role="alert">${field.state.meta.errors.join(', ')}</em>`
: nothing}
`
},
)}`
同步验证方法(onBlur)会先运行,而异步方法(onBlurAsync)只有在同步方法(onBlur)成功后才会运行。要改变这种行为,请将 asyncAlways 选项设置为 true,那么异步方法将无论同步方法的结果如何都会运行。
虽然在验证数据库时异步调用是最佳选择,但在每次按键时运行网络请求会很容易导致您的数据库遭受拒绝服务攻击(DDOS)。
相反,我们通过添加一个简单的属性来启用一种简便的方法来对 async 调用进行去抖动。
;`${this.#form.field(
{
name: 'age',
asyncDebounceMs: 500,
validators: {
onChangeAsync: async ({ value }) => {
// ...
},
},
},
(field) => {
return html`<!-- ... -->`
},
)}`
;`${this.#form.field(
{
name: 'age',
asyncDebounceMs: 500,
validators: {
onChangeAsync: async ({ value }) => {
// ...
},
},
},
(field) => {
return html`<!-- ... -->`
},
)}`
这将以 500 毫秒的延迟对每个异步调用进行去抖动。您甚至可以在每个验证属性上覆盖此属性。
;`${this.#form.field(
{
name: 'age',
asyncDebounceMs: 500,
validators: {
onChangeAsyncDebounceMs: 1500,
onChangeAsync: async ({ value }) => {
// ...
},
onBlurAsync: async ({ value }) => {
// ...
},
},
},
(field) => {
return html`<!-- ... -->`
},
)}`
;`${this.#form.field(
{
name: 'age',
asyncDebounceMs: 500,
validators: {
onChangeAsyncDebounceMs: 1500,
onChangeAsync: async ({ value }) => {
// ...
},
onBlurAsync: async ({ value }) => {
// ...
},
},
},
(field) => {
return html`<!-- ... -->`
},
)}`
这将每 1500 毫秒运行一次 onChangeAsync,而 onBlurAsync 将每 500 毫秒运行一次。
虽然函数提供了更灵活和定制化的验证方式,但它们可能会显得有些冗长。为了解决这个问题,有一些库提供了基于模式(schema-based)的验证,可以使简短和类型严格的验证变得更容易。你也可以为整个表单定义一个单一的模式,并将其传递给表单级别,错误将自动传播到字段。
TanStack Form 原生地支持所有遵循 Standard Schema 规范 的库,最值得注意的是:
注意:请确保使用最新版本的模式库,因为旧版本可能尚不支持 Standard Schema。
验证不会为你提供转换后的值。有关更多信息,请参阅提交处理。
要使用这些库中的模式,您可以像使用自定义函数一样将它们传递给 validators props。
import { z } from 'zod'
import { LitElement, html } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'
const userSchema = z.object({
age: z.number().gte(13, 'You must be 13 to make an account'),
})
@customElement('my-form')
export class MyForm extends LitElement {
#form = new TanStackFormController(this, {
defaultValues: {
age: 0,
},
validators: {
onChange: userSchema,
},
})
render() {
return html`
<div>
${this.#form.field({ name: 'age' }, (field) => {
return html`<!-- ... -->`
})}
</div>
`
}
}
import { z } from 'zod'
import { LitElement, html } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'
const userSchema = z.object({
age: z.number().gte(13, 'You must be 13 to make an account'),
})
@customElement('my-form')
export class MyForm extends LitElement {
#form = new TanStackFormController(this, {
defaultValues: {
age: 0,
},
validators: {
onChange: userSchema,
},
})
render() {
return html`
<div>
${this.#form.field({ name: 'age' }, (field) => {
return html`<!-- ... -->`
})}
</div>
`
}
}
表单和字段级别的异步验证也受支持。
import { html } from 'lit'
import { z } from 'zod'
${this.#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',
},
),
},
},
(field) => {
return html`<!-- ... -->`
},
)}
import { html } from 'lit'
import { z } from 'zod'
${this.#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',
},
),
},
},
(field) => {
return html`<!-- ... -->`
},
)}
如果您需要对 Standard Schema 验证进行更精细地控制,可以像这样将 Standard Schema 与回调函数结合使用。
import { html } from 'lit'
import { z } from 'zod'
${this.#form.field(
{
name: 'age',
asyncDebounceMs: 500,
validators: {
onChangeAsync: async ({ 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
},
},
},
(field) => {
return html`<!-- ... -->`
},
)}
import { html } from 'lit'
import { z } from 'zod'
${this.#form.field(
{
name: 'age',
asyncDebounceMs: 500,
validators: {
onChangeAsync: async ({ 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
},
},
},
(field) => {
return html`<!-- ... -->`
},
)}
onChange、onBlur 等回调在表单提交时也会运行,如果表单无效,提交将被阻止。
表单状态对象有一个 canSubmit 标志,当任何字段无效且表单已被触碰时,该标志为 false(canSubmit 在表单被触碰之前一直为 true,即使某些字段“技术上”无效,取决于它们的 onChange/onBlur 属性)。
你可以通过 this.#form.api.state 访问此标志,并使用该值来实现,例如,当表单无效时禁用提交按钮(实际上,禁用的按钮不可访问,请使用 aria-disabled 代替)。
class MyForm extends LitElement {
#form = new TanStackFormController(this, {
/* ... */
})
render() {
return html`
<!-- ... -->
<!-- Dynamic submit button -->
<button type="submit" ?disabled="${!this.#form.api.state.canSubmit}">
${this.#form.api.state.isSubmitting ? '...' : 'Submit'}
</button>
`
}
}
class MyForm extends LitElement {
#form = new TanStackFormController(this, {
/* ... */
})
render() {
return html`
<!-- ... -->
<!-- Dynamic submit button -->
<button type="submit" ?disabled="${!this.#form.api.state.canSubmit}">
${this.#form.api.state.isSubmitting ? '...' : 'Submit'}
</button>
`
}
}
您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。