'use client'
import { useState } from 'react'
import * as v from 'valibot'
import { Button, Form, FormField, FormLayout, Input, Text } from '@hina-ui/react'
const schema = v.object({
lastName: v.pipe(v.string('请输入姓氏'), v.nonEmpty('请输入姓氏')),
firstName: v.pipe(v.string('请输入名字'), v.nonEmpty('请输入名字')),
email: v.pipe(v.string('请输入邮箱'), v.nonEmpty('请输入邮箱'), v.email('邮箱格式不正确')),
phone: v.string(),
})
export default function Demo() {
const [values, setValues] = useState({ lastName: '', firstName: '', email: '', phone: '' })
const [saved, setSaved] = useState('')
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
setSaved(JSON.stringify(data))
}
return (
<Form values={values} rules={schema} className="w-full max-w-lg" onSubmit={save}>
{({ submitting }) => (
<>
<FormLayout legend="联系人" description="姓名与至少一种联系方式" columns={2}>
<FormField name="lastName" label="姓" required>
<Input
value={values.lastName}
onValueChange={lastName => setValues({ ...values, lastName })}
/>
</FormField>
<FormField name="firstName" label="名" required>
<Input
value={values.firstName}
onValueChange={firstName => setValues({ ...values, firstName })}
/>
</FormField>
<FormField name="email" label="邮箱" required className="sm:col-span-2">
<Input
value={values.email}
onValueChange={email => setValues({ ...values, email })}
type="email"
/>
</FormField>
<FormField name="phone" label="电话" className="sm:col-span-2">
<Input
value={values.phone}
onValueChange={phone => setValues({ ...values, phone })}
type="tel"
/>
</FormField>
</FormLayout>
<Button type="submit" loading={submitting} className="self-start">
保存
</Button>
{saved && (
<Text tone="muted" size="sm">
已保存:{saved}
</Text>
)}
</>
)}
</Form>
)
}
tsx
用法
import { FormLayout } from '@hina-ui/react'
ts
表单布局把一组相关的字段放进一个 fieldset,legend 是这组字段的标题,description 是说明;columns 决定字段排成几列,窄屏时自动收成一列。它通常放在 Form 里,把长表单分成几个部分;单独使用也可以。
'use client'
import { useState } from 'react'
import { FormField, FormLayout, Input } from '@hina-ui/react'
export default function Demo() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
return (
<FormLayout legend="账号" description="登录时使用" className="w-80">
<FormField label="用户名">
<Input value={name} onValueChange={setName} />
</FormField>
<FormField label="邮箱">
<Input value={email} onValueChange={setEmail} type="email" />
</FormField>
</FormLayout>
)
}
tsx
示例
标签列对齐
在布局上统一设置 orientation、labelWidth 和 descriptionPlacement,内部 FormField 会继承这些设置,也可以逐项覆盖。嵌套 FormLayout 继承外层配置,其覆盖值只作用于自己的后代。columns 仍表示一行放几个字段,不表示标签与控件的列数。
响应式模式下,每个字段的可用宽度小于 32rem 时改为纵向排列。没有标签和标签侧说明的字段占满整行,适合包装自带文案的 Switch 或 Checkbox。
'use client'
import { useState } from 'react'
import { FormField, FormLayout, Input, Switch } from '@hina-ui/react'
export default function Demo() {
const [name, setName] = useState('Hina')
const [email, setEmail] = useState('hello@example.com')
const [sync, setSync] = useState(true)
return (
<FormLayout
legend="个人资料设置"
orientation="responsive"
labelWidth="9rem"
descriptionPlacement="label"
className="w-full"
>
<FormField label="显示名称" description="显示在你的公开个人资料中。">
<Input value={name} onValueChange={setName} />
</FormField>
<FormField
label="邮箱地址"
description="使用可以接收通知的邮箱地址。"
descriptionPlacement="control"
>
<Input value={email} onValueChange={setEmail} type="email" />
</FormField>
<FormField>
<Switch
checked={sync}
onCheckedChange={setSync}
controlPlacement="end"
block
description="在其他设备上继续上次的阅读进度。"
>
同步阅读进度
</Switch>
</FormField>
</FormLayout>
)
}
tsx
多列
columns 取 1 到 4;需要占满一行的字段给它加上 sm:col-span-2 这类跨列的类。
'use client'
import { useState } from 'react'
import { FormField, FormLayout, Input } from '@hina-ui/react'
export default function Demo() {
const [values, setValues] = useState({ province: '', city: '', district: '', street: '' })
return (
<FormLayout legend="收货地址" columns={3} className="w-full max-w-lg">
<FormField label="省">
<Input
value={values.province}
onValueChange={province => setValues({ ...values, province })}
/>
</FormField>
<FormField label="市">
<Input value={values.city} onValueChange={city => setValues({ ...values, city })} />
</FormField>
<FormField label="区">
<Input
value={values.district}
onValueChange={district => setValues({ ...values, district })}
/>
</FormField>
<FormField label="详细地址" className="sm:col-span-3">
<Input value={values.street} onValueChange={street => setValues({ ...values, street })} />
</FormField>
</FormLayout>
)
}
tsx
分组
一个表单里放多个布局,每组各有标题与说明,表单的间距把各组隔开。
'use client'
import { useState } from 'react'
import * as v from 'valibot'
import { Button, Form, FormField, FormLayout, Input, Switch, Text, Textarea } from '@hina-ui/react'
const schema = v.object({
name: v.pipe(v.string('请输入昵称'), v.nonEmpty('请输入昵称')),
bio: v.pipe(v.string(), v.maxLength(80, '简介不超过 80 个字')),
notifyEmail: v.boolean(),
notifyPush: v.boolean(),
})
export default function Demo() {
const [values, setValues] = useState({ name: '', bio: '', notifyEmail: true, notifyPush: false })
const [saved, setSaved] = useState('')
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
setSaved(JSON.stringify(data))
}
return (
<Form values={values} rules={schema} className="w-80" onSubmit={save}>
{({ submitting }) => (
<>
<FormLayout legend="个人资料" description="公开显示在个人页">
<FormField name="name" label="昵称" required>
<Input value={values.name} onValueChange={name => setValues({ ...values, name })} />
</FormField>
<FormField name="bio" label="简介">
<Textarea value={values.bio} onValueChange={bio => setValues({ ...values, bio })} />
</FormField>
</FormLayout>
<FormLayout legend="通知" description="有新的回复或者关注时提醒你">
<FormField name="notifyEmail">
<Switch
checked={values.notifyEmail}
onCheckedChange={notifyEmail => setValues({ ...values, notifyEmail })}
>
邮件
</Switch>
</FormField>
<FormField name="notifyPush">
<Switch
checked={values.notifyPush}
onCheckedChange={notifyPush => setValues({ ...values, notifyPush })}
>
推送
</Switch>
</FormField>
</FormLayout>
<Button type="submit" loading={submitting} className="self-start">
保存
</Button>
{saved && (
<Text tone="muted" size="sm">
已保存:{saved}
</Text>
)}
</>
)}
</Form>
)
}
tsx
禁用一组
disabled 禁用这一组里的所有字段;表单整体禁用时,每一组也随之禁用。
'use client'
import { useState } from 'react'
import { FormField, FormLayout, Input, Stack, Switch } from '@hina-ui/react'
export default function Demo() {
const [values, setValues] = useState({ name: '星见书音', email: 'shion@example.com' })
const [locked, setLocked] = useState(true)
return (
<Stack gap="md" align="stretch" className="w-80">
<Switch checked={locked} onCheckedChange={setLocked}>
锁定账号信息
</Switch>
<FormLayout legend="账号" disabled={locked}>
<FormField label="用户名">
<Input value={values.name} onValueChange={name => setValues({ ...values, name })} />
</FormField>
<FormField label="邮箱">
<Input
value={values.email}
onValueChange={email => setValues({ ...values, email })}
type="email"
/>
</FormField>
</FormLayout>
</Stack>
)
}
tsx
行为
- 栅格从
sm断点起按columns分列,更窄的屏幕上是一列;四列在sm到lg之间先收成两列。 - 组内字段照常通过
name从表单取得错误,布局不改变字段与表单的关系。 disabled同时写到fieldset上并传给组内的字段。
无障碍
- 根元素是
fieldset,标题是legend,辅助技术会把组名读在每个字段之前。 - 说明文字通过
aria-describedby关联到fieldset。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
legend | string | — | 这组字段的标题 |
description | string | — | 说明文字 |
columns | 1 | 2 | 3 | 4 | 1 | 栅格列数 |
orientation | 'vertical' | 'horizontal' | 'responsive' | 'vertical' | 后代字段的默认布局 |
descriptionPlacement | 'label' | 'control' | 'control' | 后代字段的默认说明位置 |
labelWidth | string | number | '10rem' | 统一标签列宽:CSS 长度或像素数 |
disabled | boolean | false | 是否禁用整组字段 |
className | string | — | 追加至根元素的类名 |
内容属性
| 属性 | 说明 |
|---|---|
children | 字段 |
legend | 标题内容 |
description | 说明内容 |