表单模块
表单模块基于 react-hook-form 和 Zod 提供强大的表单构建和验证功能。
导出
typescript
import {
// Hooks
useForm,
useFormSchema,
useFieldUIState,
useFieldUIStateValue,
useSetFieldUIState,
// Context
FormProvider,
useFormContext,
// React Hook Form 导出
useFormState,
useWatch,
useFieldArray,
Controller
} from '@airiot/client'核心概念
表单模块已从 react-final-form + Ajv 迁移到 react-hook-form + Zod,提供:
- ✅ 更好的性能和更小的包体积
- ✅ 完整的 TypeScript 类型支持
- ✅ 更简洁的 API
- ✅ 字段 UI 状态管理
Hooks
useForm(props)
增强的表单 Hook,集成了字段 UI 状态管理。
类型定义:
typescript
interface UseFormProps {
defaultValues?: Partial<FieldValues>
onEffect?: (values: FieldValues, methods: UseFormReturn<FieldValues>) => void
resolver?: Resolver<any, any>
mode?: 'onSubmit' | 'onChange' | 'onBlur' | 'all'
reValidateMode?: 'onSubmit' | 'onChange' | 'onBlur'
criteriaMode?: 'firstError' | 'all'
shouldFocusError?: boolean
}
interface UseFormReturnExtended extends UseFormReturn {
watch: UseFormWatch<FieldValues>
getValues: UseFormGetValues<FieldValues>
setValue: UseFormSetValue<FieldValues>
trigger: UseFormTrigger<FieldValues>
reset: UseFormReset<FieldValues>
formState: UseFormState
register: UseFormRegister
setFieldUIState: (fieldName: string, update: FieldUIStateUpdate) => void
store: Store
}使用示例:
typescript
import { useForm, FormProvider } from '@airiot/client'
function UserForm() {
const handleSubmit = (values: any) => {
console.log('表单提交:', values)
}
const form = useForm({
defaultValues: {
username: '',
email: '',
age: 0
},
onEffect: (values, methods) => {
console.log('表单值变化:', values)
// 动态控制字段可见性
if (values.age < 18) {
methods.setFieldUIState('guardianContact', {
visible: true,
required: true
})
}
}
})
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
{/* 表单字段 */}
</form>
</FormProvider>
)
}useFormSchema(props)
将 JSON Schema 转换为表单字段和 Zod resolver。
类型定义:
typescript
interface UseFormSchemaProps {
schema: SchemaField // JSON Schema
formSchema?: Record<string, FormField> // UI 配置
mode?: 'onSubmit' | 'onChange' | 'onBlur' | 'all'
reValidateMode?: 'onSubmit' | 'onChange' | 'onBlur'
criteriaMode?: 'firstError' | 'all'
shouldFocusError?: boolean
}
interface UseFormSchemaReturn {
fields: FormField[] // 转换后的表单字段
resolver: Resolver<any, any> // Zod resolver
}使用示例:
typescript
import { useFormSchema, useForm, FormProvider } from '@airiot/client'
const userSchema = {
type: 'object',
properties: {
username: {
type: 'string',
title: '用户名',
minLength: 3,
maxLength: 20
},
email: {
type: 'string',
format: 'email',
title: '邮箱'
},
age: {
type: 'number',
title: '年龄',
minimum: 0,
maximum: 120
},
gender: {
type: 'string',
title: '性别',
enum: ['male', 'female', 'other']
},
status: {
type: 'string',
title: '状态',
enum: ['active', 'inactive', 'pending']
}
},
required: ['username', 'email']
}
function UserForm() {
const { fields, resolver } = useFormSchema({
schema: userSchema,
formSchema: {
username: { component: Input },
gender: {
component: Select,
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '其他', value: 'other' }
]
},
status: {
component: Select,
options: [
{ label: '活跃', value: 'active' },
{ label: '未激活', value: 'inactive' },
{ label: '待定', value: 'pending' }
]
}
}
})
const form = useForm({ resolver, mode: 'onSubmit' })
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{fields.map(field => (
<FormField key={field.name} {...field} />
))}
</form>
</FormProvider>
)
}字段 UI 状态管理
FieldUIState 接口
字段 UI 状态接口定义:
typescript
interface FieldUIState {
visible?: boolean // 字段是否可见
disabled?: boolean // 字段是否禁用
loading?: boolean // 字段是否加载中
readonly?: boolean // 字段是否只读
required?: boolean // 是否必填
custom?: Record<string, any> // 自定义状态
}useFieldUIState(fieldName)
获取和管理字段的 UI 状态。
typescript
const [uiState, setUIState, resetUIState] = useFieldUIState('username')
uiState.visible // 字段是否可见
uiState.disabled // 字段是否禁用
uiState.loading // 字段是否加载中
uiState.readonly // 字段是否只读
uiState.required // 是否必填
uiState.custom // 自定义状态
// 更新状态
setUIState({ visible: false })
setUIState({ disabled: true })
setUIState({ loading: true })
// 函数式更新
setUIState(prev => ({ ...prev, disabled: true, visible: false }))
// 重置状态
resetUIState()useFieldUIStateValue(fieldName)
只读获取字段 UI 状态。
typescript
const uiState = useFieldUIStateValue('username')
if (!uiState.visible) return null
if (uiState.disabled) return <ReadOnlyComponent />useSetFieldUIState(fieldName)
只写更新字段 UI 状态。
typescript
const setUIState = useSetFieldUIState('username')
setUIState({ visible: false })
setUIState({ disabled: true })工具函数
typescript
import {
setFieldVisibility,
setFieldDisabled,
setFieldLoading
} from '@airiot/client'
// 设置字段可见性
setFieldVisibility('username', false)
// 设置字段禁用状态
setFieldDisabled('username', true)
// 设置加载状态
setFieldLoading('username', true)注意: 字段的验证错误由 react-hook-form 的表单状态管理,使用 fieldState.error 访问。
Context
FormProvider
表单上下文提供者,用于包裹表单组件。
typescript
import { FormProvider, useForm } from '@airiot/client'
function MyForm() {
const form = useForm({
defaultValues: { username: '' }
})
return (
<FormProvider {...form}>
{/* 表单内容 */}
</FormProvider>
)
}useFormContext()
访问表单上下文。
typescript
import { useFormContext, Controller } from '@airiot/client'
function MyField({ name }: { name: string }) {
const { control } = useFormContext()
const uiState = useFieldUIStateValue(name)
return uiState.visible ? (
<Controller
name={name}
control={control}
render={({ field }) => (
<input {...field} disabled={uiState.disabled} />
)}
/>
) : null
}JSON Schema 格式
基础字段类型
typescript
const schema = {
type: 'object',
properties: {
// 字符串字段
username: {
type: 'string',
title: '用户名'
},
// 数字字段
age: {
type: 'number',
title: '年龄'
},
// 整数字段
count: {
type: 'integer',
title: '数量'
},
// 布尔字段
isActive: {
type: 'boolean',
title: '是否激活'
},
// 日期字段
birthDate: {
type: 'string',
format: 'date',
title: '出生日期'
},
// 日期时间字段
createdAt: {
type: 'string',
format: 'date-time',
title: '创建时间'
},
// 时间字段
time: {
type: 'string',
format: 'time',
title: '时间'
}
}
}枚举字段
typescript
const schema = {
type: 'object',
properties: {
status: {
type: 'string',
title: '状态',
enum: ['active', 'inactive', 'pending'],
enumNames: ['激活', '未激活', '待定']
},
priority: {
type: 'number',
title: '优先级',
enum: [1, 2, 3],
enum_title: { 1: '高', 2: '中', 3: '低' }
}
}
}对象字段
typescript
const schema = {
type: 'object',
properties: {
address: {
type: 'object',
title: '地址',
properties: {
street: { type: 'string', title: '街道' },
city: { type: 'string', title: '城市' },
zipCode: { type: 'string', title: '邮编' }
},
required: ['city']
}
}
}数组字段
typescript
const schema = {
type: 'object',
properties: {
tags: {
type: 'array',
title: '标签',
items: {
type: 'string'
}
},
items: {
type: 'array',
title: '项目',
items: {
type: 'object',
properties: {
name: { type: 'string', title: '名称' },
price: { type: 'number', title: '价格' }
}
}
}
}
}验证
内置验证规则
typescript
const schema = {
type: 'object',
properties: {
username: {
type: 'string',
title: '用户名',
minLength: 3, // 最小长度
maxLength: 20, // 最大长度
},
age: {
type: 'integer',
title: '年龄',
minimum: 0, // 最小值
maximum: 120, // 最大值
exclusiveMinimum: false, // 不包含最小值
exclusiveMaximum: false // 不包含最大值
},
email: {
type: 'string',
format: 'email', // 邮箱格式验证
title: '邮箱'
}
},
required: ['username', 'age'] // 必填字段
}自定义验证
typescript
import { useFormSchema, useForm } from '@airiot/client'
const { resolver } = useFormSchema({
schema: userSchema
})
const form = useForm({
resolver,
mode: 'onSubmit'
})
// 在 SchemaForm 中使用自定义验证
<SchemaForm
schema={userSchema}
validate={(values) => {
const errors: any = {}
// 自定义验证规则
if (values.password !== values.confirmPassword) {
errors.confirmPassword = '两次密码输入不一致'
}
return errors
}}
onSubmit={handleSubmit}
/>完整示例
基础表单示例
typescript
import { useForm, FormProvider, Controller } from '@airiot/client'
import { useFormContext, useFieldUIStateValue } from '@airiot/client'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
function UserForm() {
const form = useForm({
defaultValues: {
username: '',
email: '',
age: 0
},
onEffect: (values, methods) => {
// 当年龄小于 18 时,显示监护人联系方式字段
if (values.age < 18) {
methods.setFieldUIState('guardianContact', {
visible: true,
required: true
})
} else {
methods.setFieldUIState('guardianContact', {
visible: false
})
}
}
})
const onSubmit = (data: any) => {
console.log('提交的数据:', data)
}
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Controller
name="username"
control={form.control}
render={({ field, fieldState }) => (
<div>
<label>用户名</label>
<input {...field} />
{fieldState.error && <span>{fieldState.error.message}</span>}
</div>
)}
/>
<Controller
name="email"
control={form.control}
render={({ field }) => (
<div>
<label>邮箱</label>
<input {...field} type="email" />
</div>
)}
/>
<Controller
name="age"
control={form.control}
render={({ field }) => (
<div>
<label>年龄</label>
<input {...field} type="number" />
</div>
)}
/>
<Controller
name="guardianContact"
control={form.control}
render={({ field }) => {
const ui = useFieldUIStateValue('guardianContact')
return ui.visible ? (
<div>
<label>监护人联系方式 (必填)</label>
<input {...field} />
</div>
) : null
}}
/>
<Button type="submit">提交</Button>
</form>
</FormProvider>
)
}Schema 表单示例
typescript
import { useFormSchema, useForm, FormProvider } from '@airiot/client'
const userSchema = {
type: 'object',
properties: {
username: {
type: 'string',
title: '用户名',
minLength: 3,
maxLength: 20
},
email: {
type: 'string',
format: 'email',
title: '邮箱'
},
age: {
type: 'number',
title: '年龄',
minimum: 0,
maximum: 120
},
gender: {
type: 'string',
title: '性别',
enum: ['male', 'female', 'other']
},
status: {
type: 'string',
title: '状态',
enum: ['active', 'inactive', 'pending']
}
},
required: ['username', 'email']
}
const formSchema = {
username: { component: Input },
email: { component: Input },
age: { component: Input },
gender: {
component: Select,
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '其他', value: 'other' }
]
},
status: {
component: Select,
options: [
{ label: '活跃', value: 'active' },
{ label: '未激活', value: 'inactive' },
{ label: '待定', value: 'pending' }
]
}
}
function SchemaUserForm() {
const { fields, resolver } = useFormSchema({
schema: userSchema,
formSchema
})
const form = useForm({ resolver, mode: 'onSubmit' })
const handleSubmit = (values: any) => {
console.log('提交的数据:', values)
}
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
{fields.map(field => (
<FormField key={field.name} {...field} />
))}
<Button type="submit">提交</Button>
</form>
</FormProvider>
)
}字段 UI 状态示例
typescript
import { useForm, FormProvider, useFieldUIStateValue, Controller } from '@airiot/client'
function ConditionalFieldsForm() {
const form = useForm({
defaultValues: { accountType: 'personal', age: 0 }
})
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Controller
name="accountType"
control={form.control}
render={({ field }) => (
<select {...field}>
<option value="personal">个人账户</option>
<option value="business">企业账户</option>
</select>
)}
/>
{/* 根据账户类型显示不同字段 */}
<AccountTypeFields />
</form>
</FormProvider>
)
}
function AccountTypeFields() {
const form = useFormContext()
const accountType = useWatch({
control: form.control,
name: 'accountType'
})
if (accountType === 'business') {
return (
<>
<Controller name="companyName" control={form.control} render={({ field }) => (
<input {...field} placeholder="公司名称" />
)} />
<Controller name="taxId" control={form.control} render={({ field }) => (
<input {...field} placeholder="税号" />
)} />
</>
)
}
return (
<>
<Controller name="firstName" control={form.control} render={({ field }) => (
<input {...field} placeholder="名" />
)} />
<Controller name="lastName" control={form.control} render={({ field }) => (
<input {...field} placeholder="姓" />
)} />
</>
)
}React Hook Form 导出
除了自定义的 hooks 外,我们还导出了 react-hook-form 的核心 API:
useFormState()
获取表单状态。
typescript
import { useFormState } from '@airiot/client'
function SubmitButton() {
const { isSubmitting, isValid, dirty } = useFormState()
return (
<Button
type="submit"
disabled={isSubmitting || !isValid || !dirty}
>
{isSubmitting ? '提交中...' : '提交'}
</Button>
)
}useWatch()
监听表单值的变化。
typescript
import { useWatch } from '@airiot/client'
function WatchValues() {
const values = useWatch()
useEffect(() => {
console.log('表单值变化:', values)
}, [values])
return <pre>{JSON.stringify(values, null, 2)}</pre>
}useFieldArray(props)
管理数组字段。
typescript
import { useFieldArray, FormProvider, useForm } from '@airiot/client'
function TagsForm() {
const form = useForm()
return (
<FormProvider {...form}>
<TagsInput />
</FormProvider>
)
}
function TagsInput() {
const { fields, append, remove } = useFieldArray({
control: useFormContext().control,
name: 'tags'
})
return (
<div>
{fields.map((item, index) => (
<div key={item.id}>
<Controller
name={`tags.${index}.name`}
control={useFormContext().control}
render={({ field }) => (
<input {...field} />
)}
/>
<button onClick={() => remove(index)}>删除</button>
</div>
))}
<button onClick={() => append({ name: '' })}>添加标签</button>
</div>
)
}Controller
完全控制字段渲染。
typescript
import { Controller, useFormContext } from '@airiot/client'
function CustomInput({ name }: { name: string }) {
const { control } = useFormContext()
const uiState = useFieldUIStateValue(name)
return uiState.visible ? (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<div className="flex flex-col gap-1">
<label>{name}</label>
<input {...field} disabled={uiState.disabled} />
{fieldState.error?.message && (
<span className="text-red-500">{fieldState.error.message}</span>
)}
{uiState.loading && <span>加载中...</span>}
</div>
)}
/>
) : null
}迁移指南
如果您从旧版本(react-final-form + Ajv)迁移,请注意以下变化:
1. 表单组件
旧版本:
typescript
import { Form, SchemaForm } from '@airiot/client'
<SchemaForm schema={schema} onSubmit={onSubmit} />新版本:
typescript
import { useForm, useFormSchema, FormProvider } from '@airiot/client'
const { fields, resolver } = useFormSchema({ schema })
const form = useForm({ resolver })
<FormProvider {...form}>
{/* 表单内容 */}
</FormProvider>2. 表单 Hook
旧版本:
typescript
const { form, formState, values } = useForm({ schema })新版本:
typescript
const form = useForm({ onEffect: (values, methods) => {} })
form.setFieldUIState('fieldName', { visible: false })3. Context
旧版本:
typescript
const { form, ... } = useFormContext()新版本:
typescript
import { useFormContext, FormProvider } from '@airiot/client'
const { control } = useFormContext()4. 字段渲染
旧版本:
typescript
<Field name="username" component={Input} />新版本:
typescript
import { Controller } from '@airiot/client'
<Controller
name="username"
control={control}
render={({ field }) => <Input {...field} />}
/>