'use client'
import { useState } from 'react'
import * as v from 'valibot'
import { Button, Form, FormField, Input, Select, Switch, Text, Textarea } from '@hina-ui/react'
const schema = v.object({
title: v.pipe(
v.string('请输入标题'),
v.nonEmpty('请输入标题'),
v.maxLength(20, '标题不超过 20 个字'),
),
category: v.pipe(v.string('请选择分类'), v.nonEmpty('请选择分类')),
summary: v.pipe(v.string('请输入简介'), v.maxLength(80, '简介不超过 80 个字')),
published: v.boolean('请选择是否公开'),
})
const categories = [
{ label: '游戏', value: 'game' },
{ label: '小说', value: 'novel' },
{ label: '漫画', value: 'manga' },
]
export default function Demo() {
const [values, setValues] = useState({
title: '',
category: null as string | number | null | undefined,
summary: '',
published: true,
})
const [saved, setSaved] = useState('')
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 800))
setSaved(JSON.stringify(data))
}
return (
<Form values={values} rules={schema} className="w-80" onSubmit={save}>
{({ submitting }) => (
<>
<FormField name="title" label="标题" required>
<Input
value={values.title}
onValueChange={title => setValues({ ...values, title })}
placeholder="作品名称"
/>
</FormField>
<FormField name="category" label="分类" required>
<Select
value={values.category}
onValueChange={category => setValues({ ...values, category })}
options={categories}
/>
</FormField>
<FormField name="summary" label="简介" description="不超过 80 个字">
<Textarea
value={values.summary}
onValueChange={summary => setValues({ ...values, summary })}
/>
</FormField>
<FormField name="published">
<Switch
checked={values.published}
onCheckedChange={published => setValues({ ...values, published })}
>
公开显示
</Switch>
</FormField>
<Button type="submit" loading={submitting} className="self-start">
提交
</Button>
{saved && (
<Text tone="muted" size="sm">
已提交:{saved}
</Text>
)}
</>
)}
</Form>
)
}
tsx
用法
import { Form, FormField } from '@hina-ui/react'
ts
表单把一组字段的值、校验规则与提交动作放在一处。values 传入当前的值对象(通常来自 useState),各控件用 value 与 onValueChange 读写其中的字段,修改时传入新的对象;rules 传入校验规则,提交时先校验,全部通过才调用 onSubmit。每个字段用 FormField 包住,标签、说明与错误信息由它渲染,控件会自动与字段关联。
'use client'
import { useState } from 'react'
import * as v from 'valibot'
import { Button, Form, FormField, Input, Text } from '@hina-ui/react'
const schema = v.object({
name: v.pipe(v.string('请输入昵称'), v.nonEmpty('请输入昵称')),
email: v.pipe(v.string('请输入邮箱'), v.nonEmpty('请输入邮箱'), v.email('邮箱格式不正确')),
})
export default function Demo() {
const [values, setValues] = useState({ name: '', email: '' })
const [saved, setSaved] = useState('')
function save(data: unknown) {
setSaved(JSON.stringify(data))
}
return (
<Form values={values} rules={schema} className="w-80" onSubmit={save}>
<FormField name="name" label="昵称" required>
<Input value={values.name} onValueChange={name => setValues({ ...values, name })} />
</FormField>
<FormField name="email" label="邮箱" required>
<Input
value={values.email}
onValueChange={email => setValues({ ...values, email })}
type="email"
/>
</FormField>
<Button type="submit" className="self-start">
提交
</Button>
{saved && (
<Text tone="muted" size="sm">
已提交:{saved}
</Text>
)}
</Form>
)
}
tsx
示例
校验规则
rules 接受两种写法。一种是实现了 Standard Schema 规范的对象,Valibot、Zod、ArkType 等库生成的 schema 都可以直接传入,问题的路径按 . 拼接成字段名;另一种是一个函数,接收当前的值,返回以字段名为键、错误文字为值的对象,没有错误时返回空对象,也可以返回 Promise。
'use client'
import { useState } from 'react'
import {
Button,
Form,
FormField,
Input,
PasswordInput,
Text,
type FormValidator,
} from '@hina-ui/react'
const rules: FormValidator = data => {
const errors: Record<string, string> = {}
const username = String(data.username ?? '')
if (!username) errors.username = '请输入用户名'
else if (!/^[a-z0-9_]{3,16}$/.test(username)) {
errors.username = '3 到 16 位小写字母、数字或者下划线'
}
if (!data.password) errors.password = '请输入密码'
if (data.confirm !== data.password) errors.confirm = '两次输入的密码不一致'
return errors
}
export default function Demo() {
const [values, setValues] = useState({ username: '', password: '', confirm: '' })
const [saved, setSaved] = useState('')
function save() {
setSaved(values.username)
}
return (
<Form values={values} rules={rules} className="w-80" onSubmit={save}>
<FormField name="username" label="用户名" required>
<Input
value={values.username}
onValueChange={username => setValues({ ...values, username })}
/>
</FormField>
<FormField name="password" label="密码" required>
<PasswordInput
value={values.password}
onValueChange={password => setValues({ ...values, password })}
/>
</FormField>
<FormField name="confirm" label="确认密码" required>
<PasswordInput
value={values.confirm}
onValueChange={confirm => setValues({ ...values, confirm })}
/>
</FormField>
<Button type="submit" className="self-start">
注册
</Button>
{saved && (
<Text tone="muted" size="sm">
已注册:{saved}
</Text>
)}
</Form>
)
}
tsx
校验时机
默认在提交时校验;提交未通过后,每次值变化都会重新校验,错误随修改即时消失。validateOn 设为 blur 时,字段在失去焦点后开始校验;设为 change 时,值一变化就校验。
'use client'
import { useState } from 'react'
import * as v from 'valibot'
import {
Button,
Form,
FormField,
Input,
SegmentedControl,
Stack,
type FormValidateOn,
} from '@hina-ui/react'
const modes = [
{ label: '提交时', value: 'submit' },
{ label: '失去焦点', value: 'blur' },
{ label: '值变化', value: 'change' },
]
const schema = v.object({
name: v.pipe(v.string('请输入昵称'), v.nonEmpty('请输入昵称')),
email: v.pipe(v.string('请输入邮箱'), v.nonEmpty('请输入邮箱'), v.email('邮箱格式不正确')),
})
export default function Demo() {
const [mode, setMode] = useState<string | number>('submit')
const [values, setValues] = useState({ name: '', email: '' })
return (
<Stack gap="md" align="stretch" className="w-80">
<SegmentedControl
value={mode}
onValueChange={setMode}
options={modes}
aria-label="校验时机"
/>
<Form key={mode} values={values} rules={schema} validateOn={mode as FormValidateOn}>
<FormField name="name" label="昵称" required>
<Input value={values.name} onValueChange={name => setValues({ ...values, name })} />
</FormField>
<FormField name="email" label="邮箱" required>
<Input
value={values.email}
onValueChange={email => setValues({ ...values, email })}
type="email"
/>
</FormField>
<Button type="submit" className="self-start">
提交
</Button>
</Form>
</Stack>
)
}
tsx
服务端返回的错误
提交之后服务端也可能拒绝某些字段。通过 ref 调用 setErrors,传入字段名到错误文字的映射,错误显示在对应的字段下;该字段的值被修改后,这条错误自动清除。没有对应字段的错误可以把 children 写成函数,从参数的 error 取得,自行展示。
'use client'
import { useRef, useState } from 'react'
import * as v from 'valibot'
import { Button, Form, FormField, Input, Text, type FormHandle } from '@hina-ui/react'
const schema = v.object({
email: v.pipe(v.string('请输入邮箱'), v.nonEmpty('请输入邮箱'), v.email('邮箱格式不正确')),
})
export default function Demo() {
const [values, setValues] = useState({ email: 'taken@example.com' })
const form = useRef<FormHandle>(null)
const [saved, setSaved] = useState('')
async function save(data: Record<string, unknown>) {
await new Promise(resolve => setTimeout(resolve, 600))
if (data.email === 'taken@example.com') {
form.current?.setErrors({ email: '该邮箱已被注册' })
return
}
setSaved(String(data.email))
}
return (
<Form ref={form} values={values} rules={schema} className="w-80" onSubmit={save}>
{({ submitting }) => (
<>
<FormField name="email" label="邮箱" description="taken@example.com 会被服务端拒绝">
<Input
value={values.email}
onValueChange={email => setValues({ ...values, email })}
type="email"
/>
</FormField>
<Button type="submit" loading={submitting} className="self-start">
注册
</Button>
{saved && (
<Text tone="muted" size="sm">
已注册:{saved}
</Text>
)}
</>
)}
</Form>
)
}
tsx
提交中与禁用
onSubmit 返回 Promise 时,表单在其结束前处于提交中状态,所有字段禁用,children 函数参数中的 submitting 可用于按钮的加载指示。disabled 禁用整个表单。
'use client'
import { useState } from 'react'
import * as v from 'valibot'
import { Button, Form, FormField, Input, Stack, Switch } from '@hina-ui/react'
const schema = v.object({
name: v.pipe(v.string('请输入名称'), v.nonEmpty('请输入名称')),
})
function save() {
return new Promise(resolve => setTimeout(resolve, 1500))
}
export default function Demo() {
const [values, setValues] = useState({ name: '星见书音' })
const [disabled, setDisabled] = useState(false)
return (
<Stack gap="md" align="stretch" className="w-80">
<Switch checked={disabled} onCheckedChange={setDisabled}>
禁用表单
</Switch>
<Form values={values} rules={schema} disabled={disabled} onSubmit={save}>
{({ submitting }) => (
<>
<FormField name="name" label="名称">
<Input value={values.name} onValueChange={name => setValues({ ...values, name })} />
</FormField>
<Button type="submit" loading={submitting} disabled={disabled} className="self-start">
保存
</Button>
</>
)}
</Form>
</Stack>
)
}
tsx
行为
- 提交时先校验;未通过则显示错误,把焦点移到第一个无效的控件,不调用提交处理函数。
- 校验通过后调用处理函数;返回 Promise 时等待其完成,期间表单处于提交中。
- 提交未通过之后,或者校验时机为
change时,值的变化触发重新校验;校验时机为blur时,只有失去过焦点的字段显示错误。 setErrors设置的错误优先显示,字段的值改变后清除。- 路径为空的问题不属于任何字段,作为表单整体的错误通过渲染函数参数
error暴露。
无障碍
- 根元素是原生表单,关闭了浏览器自带的校验提示;回车提交与提交按钮的行为保持默认。
- 提交中时根元素带有
aria-busy。 - 错误信息由 FormField 渲染,并通过
aria-describedby与aria-invalid关联到控件。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
values | Record<string, unknown> | — | 字段值所在的对象 |
rules | FormRules | — | 校验规则,Standard Schema 对象或者校验函数 |
validateOn | 'submit' | 'blur' | 'change' | 'submit' | 校验时机 |
disabled | boolean | false | 是否禁用整个表单 |
className | string | — | 追加至根元素的类名 |
内容属性
| 属性 | 参数 | 说明 |
|---|---|---|
children | errors、error、invalid、submitting、submitted | 表单内容 |
回调
| 回调 | 参数 | 说明 |
|---|---|---|
onSubmit | values: Record<string, unknown> | 校验通过后触发;处理函数返回 Promise 时表单进入提交中 |
方法
| 方法 | 说明 |
|---|---|
submit() | 触发一次提交 |
validate() | 校验并返回是否通过 |
setErrors(errors) | 设置外部错误,键为字段名 |
reset() | 清空错误、失焦记录与提交状态 |
FormRules、FormErrors、FormValidator 与 StandardSchema 类型可以从包入口导入。