#Form 表单
组织字段、标签和校验提示,收集表单数据。
#提交与校验
basic.tsx
import { Button, Flex, Form, FormField, Input, Select, Text } from '@ifhover/leaf-ui';
import { useState } from 'react';
export function FormBasic() {
const [result, setResult] = useState('');
return (
<Form
onSubmit={(event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
setResult(`${data.get('name')} · ${data.get('email')} · ${data.get('team')}`);
}}
onReset={() => setResult('')}
>
<FormField label="姓名" required>
<Input name="name" placeholder="你的姓名" />
</FormField>
<FormField label="工作邮箱" required help="用于接收项目通知">
<Input name="email" type="email" placeholder="hello@example.com" />
</FormField>
<FormField label="所属团队" required>
<Select
name="team"
options={[
{ value: 'design', label: '设计团队' },
{ value: 'engineering', label: '工程团队' },
]}
/>
</FormField>
<FormField>
<Flex gap="md" align="center" wrap justify="center">
<Button type="submit">提交</Button>
<Button type="reset" variant="outline">
重置
</Button>
</Flex>
</FormField>
{result && (
<output>
<Text tone="muted">{result}</Text>
</output>
)}
</Form>
);
}
#标签宽度
label-width.tsx
import { Button, Flex, Form, FormField, Input, Space, Switch } from '@ifhover/leaf-ui';
import { useState } from 'react';
export function FormLabelWidth() {
const [automatic, setAutomatic] = useState(true);
const [extra, setExtra] = useState(false);
return (
<Space direction="vertical" align="stretch">
<Flex gap="md" align="center" wrap justify="center">
<Switch checked={automatic} onChange={(event) => setAutomatic(event.target.checked)}>
自动标签宽度
</Switch>
<Button variant="outline" onClick={() => setExtra(!extra)}>
切换长标签
</Button>
</Flex>
<Form labelWidth={automatic ? 'auto' : 110}>
<FormField label="姓名">
<Input name="name" />
</FormField>
<FormField label="工作邮箱">
<Input name="email" />
</FormField>
{extra && (
<FormField label="紧急联系人电话号码">
<Input name="emergency" />
</FormField>
)}
</Form>
</Space>
);
}
#扩展用法
extensions.tsx
import { Button, Flex, Form, FormErrorSummary, FormField, FormGroup, FormList, Input, Space } from '@ifhover/leaf-ui';
export function ExtensionDemo() {
return (
<Form layout="vertical" onSubmit={(e) => e.preventDefault()}>
<FormErrorSummary />
<FormGroup legend="联系人" description="添加、移除或调整联系人顺序。">
<FormList name="contacts" defaultValue={['']}>
{(fields, ops) => (
<>
{fields.map((field) => (
<FormField key={field.key} label={`联系人 ${field.index + 1}`} required>
<Flex gap="sm" align="start">
<Input
style={{ flex: 1, minWidth: 0 }}
name={`${field.name}.email`}
type="email"
value={field.value}
onChange={(e) => ops.update(field.index, e.target.value)}
/>
<Button variant="ghost" onClick={() => ops.remove(field.index)}>
移除
</Button>
{field.index > 0 && (
<Button
variant="ghost"
onClick={() => ops.move(field.index, field.index - 1)}
>
上移
</Button>
)}
</Flex>
</FormField>
))}
<Button variant="outline" onClick={() => ops.add('')}>
添加联系人
</Button>
</>
)}
</FormList>
</FormGroup>
<Space>
<Button type="submit">检查表单</Button>
<Button type="reset" variant="outline">
重置
</Button>
</Space>
</Form>
);
}
#Form API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
layout | 'horizontal' | 'vertical' | 'horizontal' | 标签与字段的排列方式 |
labelWidth | number | 'auto' | 'auto' | 数字设置像素宽度;auto 统一使用最宽标签 |
labelAlign | 'left' | 'right' | 'right' | 标签对齐方向 |
disabled | boolean | false | 禁用交互。 |
ref | Ref<HTMLFormElement> | — | 表单引用 |
#FormField API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
label | ReactNode | — | 字段标签 |
htmlFor | string | 自动生成 | 与输入控件的 id 关联;一个字段对应一个控件或组 |
required | boolean | false | 要求整体值非空,复合控件只校验一次。 |
help | ReactNode | — | 显示在控件下方的辅助提示,占据正常布局空间 |
error | ReactNode | — | 错误提示,优先于 help 展示 |
labelWidth | number | — | 当前字段的标签宽度,单位像素 |
#类型
定义组件属性或组装变量时可导入相关类型。
import type { FormProps, FormFieldProps } from '@ifhover/leaf-ui';#使用说明
默认自动计算同一 Form 中最宽的标签。标签文本、字体或可见字段变化时更新;窄屏和 vertical 布局按纵向排列。支持 onSubmit、onReset、action、method 等表单属性。
help 和错误提示会占据正常布局空间,并与下一项保留表单间距;文字换行后自然增加高度。
用 FormData 读取控件上的 name。简单校验使用 required、type、minLength 等属性,错误提示跟随字段显示;业务与服务端错误通过 error 传入。设置 noValidate 可以接管校验。FormData 返回字符串,不会自动转换为数字或嵌套对象,未勾选复选框不会提交 false。受控字段的重置由应用处理。
#扩展类型
import type { FormGroupProps, FormListProps, FormListField, FormListOperations, FormErrorSummaryProps, FormError } from '@ifhover/leaf-ui';#FormGroupProps API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
legend | ReactNode | — | 字段组名称 |
description | ReactNode | — | 补充说明 |
#FormListProps API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name | string | 必填 | 必需。表单字段名称 |
value | readonly T[] | — | 受控值 |
defaultValue | readonly T[] | [] | 初始值;非受控表单 reset 时恢复 |
onChange | (values: T[]) => void | — | 值变化回调 |
itemKey | (value: T) => string | — | 提取稳定的项目标识 |
children | (fields: readonly FormListField<T>[], operations: FormListOperations<T>) => ReactNode | 必填 | 必需。内容或下级项目 |
#FormListField API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
key | string | 必填 | 必需。唯一且稳定的标识 |
index | number | 必填 | 必需。当前索引,从 0 开始 |
name | string | 必填 | 必需。表单字段名称 |
value | T | 必填 | 必需。受控值 |
#FormListOperations API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
add | (value: T, index?: number) => void | 必填 | 必需。插入字段,省略 index 时追加 |
remove | (index: number | readonly number[]) => void | 必填 | 必需。移除一个或多个位置 |
move | (from: number, to: number) => void | 必填 | 必需。移动字段并保留稳定 key |
update | (index: number, value: T) => void | 必填 | 必需。更新字段值 |
#FormErrorSummaryProps API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
errors | readonly FormError[] | — | 省略时汇总同一 Form 内字段错误 |
title | ReactNode | — | 标题内容 |
#FormError API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fieldId | string | 必填 | 必需。关联输入控件 id,点击错误后聚焦 |
message | ReactNode | 必填 | 必需。错误内容 |
label | ReactNode | — | 显示内容 |
#接入 React Hook Form
在应用中安装 react-hook-form。原生输入使用 register;onChange 返回业务值的组件使用 Controller。Leaf Form 负责布局,表单库负责状态与校验,使用 noValidate 接管校验。FormList 的点号 name 只是字段名,不会自动生成嵌套对象;使用 React Hook Form 管理数组时优先使用 useFieldArray。
import { useForm, Controller } from 'react-hook-form';
import { Form, FormField, FormErrorSummary, Input, Select, Button } from '@ifhover/leaf-ui';
export function AccountForm() {
const { register, control, handleSubmit, formState: { errors } } = useForm({ defaultValues: { email: '', role: '' } });
return <Form noValidate onSubmit={handleSubmit(values => console.log(values))}>
<FormErrorSummary />
<FormField label="Email" error={errors.email?.message}>
<Input {...register('email', { required: 'Enter an email' })} />
</FormField>
<Controller name="role" control={control} rules={{ required: 'Choose a role' }} render={({ field, fieldState }) =>
<FormField label="Role" error={fieldState.error?.message}>
<Select {...field} options={[{ value: 'editor', label: 'Editor' }]} />
</FormField>}
/>
<Button type="submit">Save</Button>
</Form>;
}#FormValidationApi
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
errors | Record<string, string> | — | 必需。按字段名组织的错误,可传给 FormField。 |
pending | boolean | — | 必需。校验或提交是否进行中。 |
setErrors | Dispatch<SetStateAction<Record<string, string>>> | — | 必需。设置服务端或业务错误。 |
handleSubmit | (event: FormEvent<HTMLFormElement>) => Promise<void> | — | 必需。绑定到 Form.onSubmit。 |
handleReset | (event: FormEvent<HTMLFormElement>) => void | — | 必需。绑定到 Form.onReset,取消请求并清除错误。 |
cancel | () => void | — | 必需。取消当前请求。 |
clearErrors | (name?: string) => void | — | 必需。清除一个或全部错误并取消旧请求。 |
#FormValidationOptions
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
validate | ( data: FormData, signal: AbortSignal, ) => Record<string, string> | Promise<Record<string, string>> | — | 必需。接收 FormData 和取消信号,返回按字段名组织的错误。 |
onSubmit | (data: FormData, signal: AbortSignal) => void | Promise<void> | — | 校验通过后提交;应响应取消信号。 |
onError | (error: unknown) => void | — | 操作失败时调用;命令面板会保留并展示错误。 |
focusError | boolean | — | 出现错误时聚焦第一个字段。 |