Form
列表式布局的表单容器与字段
Form 把 CellGroup 的分组外观(分隔线 / 圆角 / 标题)与 @skyroc/form 的状态管理、校验上下文合成一层渲染,FormItem 则把标签、控件、错误与描述映射到 Cell 的各个插槽——因此整个表单读起来就是一张设置列表。需要自由布局时改用 FieldGroup / FieldItem。
import { Form, FormComputedField, FormItem, useForm } from '@skyroc/native-ui';基础用法
useForm 创建表单实例,FormItem 用 name 声明字段路径并把子组件接到表单上。form.submit() 触发校验,通过走 onFinish,失败走 onFinishFailed;form.resetFields() 重置回初始值。
import { Button, Form, FormItem, Input, Text, useForm } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
interface BasicFormValues {
/** 联系邮箱 */
email: string;
/** 昵称 */
nickname: string;
}
const FormBasic = () => {
const [result, setResult] = useState('尚未提交');
const [form] = useForm<BasicFormValues>();
function handleFinish(values: BasicFormValues) {
setResult(`提交成功:${values.nickname} / ${values.email}`);
}
function handleFinishFailed() {
setResult('校验未通过');
}
function handleReset() {
form.resetFields();
setResult('尚未提交');
}
return (
<View className="gap-4 bg-background p-4">
<Form<BasicFormValues>
form={form}
initialValues={{ nickname: '示例用户' }}
onFinish={handleFinish}
onFinishFailed={handleFinishFailed}
>
<FormItem<BasicFormValues>
label="昵称"
name="nickname"
>
<Input
placeholder="请输入昵称"
variant="none"
/>
</FormItem>
<FormItem<BasicFormValues>
description="提交时会校验邮箱格式"
label="邮箱"
name="email"
rules={[{ message: '请输入正确的邮箱', pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, required: true }]}
>
<Input
autoCapitalize="none"
keyboardType="email-address"
placeholder="name@example.com"
variant="none"
/>
</FormItem>
</Form>
<View className="flex-row gap-3">
<Button
className="flex-1"
onPress={() => form.submit()}
>
提交
</Button>
<Button
className="flex-1"
variant="outline"
onPress={handleReset}
>
重置
</Button>
</View>
<Text className="text-sm text-muted-foreground">{result}</Text>
</View>
);
};
export { FormBasic };字段内的输入控件通常配 variant="none":行本身已经是 Cell,控件再画一层边框会显得重。
何时使用
- 设置页、信息录入页这类列表式表单:一行一个字段,标签在左、控件在右。
- 需要标签压在控件上方的自由布局时用
FieldGroup/FieldItem,两者的字段配置与校验体系完全一致。 Form只负责一组字段的外观,一屏里可以放多个Form共享同一个form实例,做成分组表单。
值适配
缺省的取值逻辑取回调的第一个参数,同时把 TextInput 的原生事件抹平成 nativeEvent.text,因此 Input 与走 onChange(value) 约定的控件都不需要额外配置。要接管这条链路时:
getValueFromEvent:改「从回调参数里怎么取值」,拿到的是子组件回调的原始参数;normalize:值写入表单前的归一化,改的是存下来的值;getValueProps:值传给子组件前的转换,只改显示值;trigger/valuePropName:接入不走value/onChange约定的控件(如Switch的checked/onCheckedChange)。
import { Form, FormItem, Input, Switch, useForm } from '@skyroc/native-ui';
import { View } from 'react-native';
interface AdapterFormValues {
/** 自动转为大写的编号 */
code: string;
/** 是否启用 */
enabled: boolean;
}
const FormValueAdapter = () => {
const [form] = useForm<AdapterFormValues>();
return (
<View className="bg-background p-4">
<Form<AdapterFormValues>
form={form}
initialValues={{ code: 'demo', enabled: true }}
>
<FormItem<AdapterFormValues>
description="先过滤非字母数字字符,再统一转换为大写"
getValueFromEvent={event => String(event.nativeEvent.text).replace(/[^a-z0-9]/gi, '')}
label="编号"
name="code"
normalize={value => String(value).toUpperCase()}
>
<Input
autoCapitalize="characters"
placeholder="请输入编号"
variant="none"
/>
</FormItem>
<FormItem<AdapterFormValues>
classNames={{ control: 'items-end' }}
label="启用"
name="enabled"
trigger="onCheckedChange"
valuePropName="checked"
>
<Switch />
</FormItem>
</Form>
</View>
);
};
export { FormValueAdapter };弹层类控件(Picker / DatePicker)的值由 onConfirm 回传,把 trigger="onConfirm" 传给 FormItem 即可;弹层的显隐仍由页面自己持有,整行的 onPress 负责打开。
标签布局
labelAlign 决定标签与控件的关系:
| 取值 | 布局 | 适用场景 |
|---|---|---|
left | 标签在左、控件在右(默认) | 短文本、开关、选择器 |
top | 标签压在控件上方 | 地址、备注这类长内容 |
labelWidth 只在 labelAlign="left" 时生效,默认 120。同一个表单里各行保持同一个 labelWidth,标签列才对得齐——FormComputedField 的默认值也是 120,就是为了和 FormItem 对齐。
size 提供三档密度,影响行高与标签 / 错误 / 描述的排版,同样不会透传给子组件。
| 尺寸 | 标签字号 | 提示字号 |
|---|---|---|
sm | text-sm | text-2xs |
md | text-base | text-xs |
lg | text-lg | text-sm |
import { Form, FormItem, Input, useForm } from '@skyroc/native-ui';
import { View } from 'react-native';
interface LayoutForm {
/** 收货地址 */
address: string;
/** 昵称 */
nickname: string;
/** 备注 */
remark: string;
}
const FormLayout = () => {
const [layoutForm] = useForm<LayoutForm>();
return (
<View className="bg-background p-4">
<Form<LayoutForm> form={layoutForm}>
{/* 长内容用 labelAlign="top",标签压在输入区上方 */}
<FormItem<LayoutForm>
required
description="送货上门时使用"
label="收货地址"
labelAlign="top"
name="address"
rules={[{ message: '请填写收货地址', required: true }]}
>
<Input
placeholder="请输入详细地址"
variant="none"
/>
</FormItem>
<FormItem<LayoutForm>
label="昵称"
labelWidth={72}
name="nickname"
size="sm"
>
<Input
placeholder="小号行高"
variant="none"
/>
</FormItem>
<FormItem<LayoutForm>
label="备注"
name="remark"
size="lg"
>
<Input
placeholder="大号行高"
variant="none"
/>
</FormItem>
</Form>
</View>
);
};
export { FormLayout };labelAlign="left" 时错误与描述会被拆到 Cell 外面独占一行,并按「标签列宽 + Cell 内边距」缩进,与控件左对齐;labelAlign="top" 时提示跟着内容走,不需要缩进。
必填与校验
rules 与 required 共用一份事实:只写 rules={[{ required: true }]} 会自动显示星号,只写 required 会自动补一条 required 规则,显式写 required={false} 只隐藏星号、不动已有规则。校验失败时标签整体变红,并向子组件注入 error,Input 这类支持 error 变体的控件因此自动变红。
多条规则同时失败时提示位只显示第一条,改完再暴露下一条。
交互与禁用
onPress 让整行可点,通常用来打开选择器;箭头缺省由 onPress 推导,也可以用 showArrow / arrowDirection 显式控制。disabled 禁用整行交互,trailing 在控件与箭头之间插入自定义内容。
import { Form, FormItem, Input, Text, useForm } from '@skyroc/native-ui';
import { View } from 'react-native';
interface InteractionFormValues {
/** 禁用字段 */
locked: string;
/** 可选项 */
option: string;
}
const FormInteraction = () => {
const [form] = useForm<InteractionFormValues>();
function handleOptionPress() {
const current = form.getFieldValue('option');
form.setFieldValue('option', current === '选项 A' ? '选项 B' : '选项 A');
}
return (
<View className="bg-background p-4">
<Form<InteractionFormValues>
form={form}
initialValues={{ locked: '不可修改', option: '选项 A' }}
>
<FormItem<InteractionFormValues>
showArrow
arrowDirection="down"
label="整行点击"
name="option"
onPress={handleOptionPress}
>
<Input
readOnly
variant="none"
/>
</FormItem>
<FormItem<InteractionFormValues>
disabled
label="禁用字段"
name="locked"
trailing={<Text className="text-sm text-muted-foreground">disabled</Text>}
>
<Input
readOnly
variant="none"
/>
</FormItem>
</Form>
</View>
);
};
export { FormInteraction };样式覆盖
Form 的 border / inset / title / classNames 直接取自 CellGroup;FormItem 的 className 追加到字段根容器,classNames 按 slot 覆盖。
| 组件 | slot | 作用位置 |
|---|---|---|
Form | root | CellGroup 根节点 |
Form | title | 分组标题 |
Form | divider | 字段之间的分隔线 |
FormItem | root | 字段根容器 View(背景色在这里) |
FormItem | cell | 内层 Cell 根节点,自身保持透明 |
FormItem | control | 控件所在区域,右对齐 Switch / Rate 这类控件时改它 |
FormItem | label | 标签文字 |
FormItem | required | 必填星号 |
FormItem | extra | 错误与描述的外层容器 |
FormItem | message | 错误文案 |
FormItem | description | 描述文本 |
import { Form, FormItem, Input, useForm } from '@skyroc/native-ui';
import { View } from 'react-native';
interface StylesFormValues {
/** 示例值 */
value: string;
}
const FormStyles = () => {
const [form] = useForm<StylesFormValues>();
return (
<View className="bg-muted p-4">
<Form<StylesFormValues>
inset
form={form}
initialValues={{ value: '可覆盖各个 slot' }}
title="自定义分组标题"
classNames={{ divider: 'bg-primary/20', root: 'border border-primary/30', title: 'text-primary' }}
>
<FormItem<StylesFormValues>
className="bg-primary/5"
classNames={{ description: 'text-info', label: 'font-semibold text-primary' }}
description="FormItem 支持 root、label、description 等 slot"
label="样式"
name="value"
>
<Input variant="none" />
</FormItem>
</Form>
</View>
);
};
export { FormStyles };labelRow(星号与标签的排列容器)和 extraRow(左右布局下提示独占的那一行)是纯结构类,不开放覆盖。
计算字段
FormComputedField 依据 deps 声明的字段自动重算 compute 的结果,字段本身只读,布局与 FormItem 的左右布局完全一致。
import { Form, FormComputedField, FormItem, Input, Stepper, useForm } from '@skyroc/native-ui';
import { View } from 'react-native';
interface OrderForm {
/** 单价 */
price: string;
/** 数量 */
quantity: number;
/** 合计金额,展示在 Input 里,因此存字符串 */
total: string;
}
const FormComputed = () => {
const [orderForm] = useForm<OrderForm>();
return (
<View className="bg-background p-4">
<Form<OrderForm>
form={orderForm}
initialValues={{ price: '99', quantity: 1 }}
>
<FormItem<OrderForm>
label="单价"
name="price"
>
<Input
keyboardType="decimal-pad"
placeholder="请输入单价"
variant="none"
/>
</FormItem>
<FormItem<OrderForm>
label="数量"
name="quantity"
>
<Stepper min={1} />
</FormItem>
{/* 合计由单价与数量推出,字段本身只读;RN 的 TextInput 只接受字符串,算完要转成字符串 */}
<FormComputedField<OrderForm>
deps={['price', 'quantity']}
description="随单价与数量自动重算"
label="合计"
name="total"
compute={get => (Number(get('price') || 0) * Number(get('quantity') || 0)).toFixed(2)}
>
<Input variant="none" />
</FormComputedField>
</Form>
</View>
);
};
export { FormComputed };子组件是 Input 时 compute 必须返回字符串——RN 的 TextInput 只接受字符串,返回数字不会显示。
稳定性说明
无论校验有没有出错,FormItem 的根元素结构都保持不变(始终是 View > Cell),提示区的间距只靠切换 className 实现。这一点是刻意的:根元素类型在 Cell / View 之间切换会让 React 卸载并重建整棵子树(含内部的 TextInput),表现为校验出错后键盘收起、焦点丢失或跳到别的输入框。
API
Form
除下表外,Form 还接受 CellGroup 的 border / inset / title / classNames。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| form | useForm 创建的表单实例,不传时内部自建一个 | FormInstance<Values> | - |
| initialValues | 表单初始值,只在首次挂载时生效 | DeepPartial<Values> | - |
| title | 分组标题,string 自动包裹 Text | ReactNode | - |
| inset | 卡片式内嵌样式(左右留边、圆角) | boolean | false |
| border | 是否在字段之间插入分隔线 | boolean | true |
| validateTrigger | 字段默认的校验触发时机,可被 FormItem 覆盖 | string | string[] | 'onChange' |
| validateMessages | 默认错误文案模板,支持占位符 | ValidateMessages | - |
| schema | 表单级 schema 校验(zod / valibot 等 Standard Schema 实现) | FormSchema<Values> | - |
| preserve | 字段卸载后是否保留值 | boolean | true |
| clearOnDestroy | 表单卸载时是否清空数据 | boolean | false |
| onFinish | 提交且校验通过的回调 | (values: Values) => void | - |
| onFinishFailed | 提交但校验失败的回调,参数含 errorFields / firstErrorName | (errorInfo: ValidateErrorEntity<Values>) => void | - |
| onValuesChange | 任一字段值变化的回调 | (changedValues: Partial<Values>, values: Values) => void | - |
| onFieldsChange | 字段元信息(错误、校验状态等)变化的回调 | (changedFields: Meta[], allFields: Meta[]) => void | - |
| classNames | 分组各 slot 的类名覆盖,取自 CellGroup | SlotClassNames<CellGroupSlots> | - |
| ref | 根节点的 ref,用于 measure / 滚动定位等命令式操作 | Ref<View> | - |
FormItem
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| name* | 字段名,支持 a.b[0] 这样的路径 | AllPathsKeys<Values> | - |
| children* | 子组件,接收注入的 value / onChange / error / disabled | ReactElement | - |
| label | 标签文本,为空时标签列不渲染 | string | - |
| labelAlign | 标签对齐方式,left 为左右排布,top 为标签压在控件上方 | 'left' | 'top' | 'left' |
| labelWidth | 标签列宽度,仅 labelAlign 为 left 时生效 | number | 120 |
| required | 是否显示必填星号;缺省由 rules 推导,显式传 true 会自动补一条 required 规则 | boolean | - |
| rules | 校验规则 | Rule[] | - |
| description | 描述文本,显示在控件下方、错误文案之后 | string | - |
| size | 尺寸,只影响行高与标签 / 错误 / 描述的排版,不会透传给子组件 | 'sm' | 'md' | 'lg' | 'md' |
| onPress | 整行点击回调,通常用于打开选择器 | () => void | - |
| disabled | 禁用整行交互 | boolean | false |
| showArrow | 是否显示右侧箭头,缺省由 onPress 推导 | boolean | - |
| arrowDirection | 箭头方向,showArrow 为 false 时无效 | 'down' | 'left' | 'right' | 'up' | - |
| trailing | 右侧自定义内容,位于控件与箭头之间 | ReactNode | - |
| initialValue | 字段初始值,优先级低于 Form 的 initialValues | any | - |
| valuePropName | 值属性名 | string | 'value' |
| trigger | 触发取值的回调名 | string | 'onChange' |
| validateTrigger | 触发校验的回调名,传 false 则只在提交时校验;缺省跟随 Form | string | string[] | false | - |
| getValueFromEvent | 从子组件回调参数里取值,缺省取第一个参数并抹平 TextInput 的原生事件 | (...args: any[]) => any | - |
| getValueProps | 值传给子组件前的转换,只改显示值 | (value: any) => any | - |
| normalize | 值写入表单前的归一化,改的是存下来的值 | (value: any, prevValue: any, allValues: Values) => any | - |
| preserve | 子组件卸载后是否保留字段值 | boolean | true |
| className | 字段根容器类名,合并到变体样式之后 | string | - |
| classNames | 各 slot 的类名覆盖 | FormItemClassNames | - |
| ref | 根节点的 ref,用于 measure / 滚动定位等命令式操作 | Ref<View> | - |
FormComputedField
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| name* | 字段名,计算结果写回表单的这个路径 | AllPathsKeys<Values> | - |
| deps* | 依赖的字段名,任一变化都会触发重算 | AllPathsKeys<Values>[] | - |
| compute* | 依据依赖字段计算当前值,get 按路径读取单个字段 | (get: (name: AllPathsKeys<Values>) => any, all: Values) => any | - |
| children* | 子组件,接收计算值并被置为只读 | ReactElement | - |
| label | 标签文本 | string | - |
| labelWidth | 标签列宽度,与同表单的 FormItem 保持一致才能对齐 | number | 120 |
| required | 是否显示必填星号,计算字段不参与推导,只作展示 | boolean | false |
| rules | 校验规则 | Rule[] | - |
| description | 描述文本,显示在计算结果下方 | string | - |
| size | 尺寸,只影响行高与标签 / 提示的排版 | 'sm' | 'md' | 'lg' | 'md' |
| valuePropName | 值属性名 | string | 'value' |
| preserve | 子组件卸载后是否保留字段值 | boolean | true |
| className | 字段根容器类名 | string | - |
| classNames | 各 slot 的类名覆盖,与 FormItem 同一套 | FormItemClassNames | - |
| ref | 根节点的 ref | Ref<View> | - |
useForm
const [form] = useForm<Values>();不传 form 时 Form 会自建一个实例,只有需要在组件里命令式读写表单时才必须自己创建。实例方法见 FormInstance。
类型
import type {
AllPathsKeys,
FormComputedFieldProps,
FormInstance,
FormItemClassNames,
FormItemProps,
FormItemSlots,
FormProps,
Meta,
Rule,
ValidateErrorEntity,
ValidateMessages
} from '@skyroc/native-ui';FormItemSlots
FormItem / FormComputedField 可通过 classNames 覆盖的 slot 名称;纯结构的 labelRow / extraRow 不在其中。
FormItemClassNames
FormItem 的 classNames 取值形态,等价于 SlotClassNames<FormItemSlots>。
AllPathsKeys
表单值对象里所有可用的字段路径,形如 'user' | 'user.name' | 'list[0].id'。
RuleType
内置类型校验器,Rule.type 缺省为 string。
FormSchema
表单级 schema:Standard Schema 实现(zod / valibot 等),或一个返回问题列表的异步函数。
ValidateMessages
默认错误文案模板,按 required / string.minLength / number.max 这类分层键取值,值里可写 ${min} ${max} ${len} 占位符。
SlotClassNames
classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 FormItemSlots / CellGroupSlots。
Rule
单条校验规则;一条规则里可以同时写多个约束,按基础校验 → 类型校验 → 自定义校验的顺序执行。
| 字段 | 类型 | 说明 |
|---|---|---|
| required | boolean | 是否必填(值为空即失败)。 |
| type | RuleType | 启用哪个内置类型校验器,缺省为 string。 |
| message | string | 覆盖默认错误文案。 |
| pattern | RegExp | 字符串正则校验。 |
| len | number | 精确的数值或字符串长度,取决于 type。 |
| min | number | Date | string | 最小数值 / 最早日期。 |
| max | number | Date | string | 最大数值 / 最晚日期。 |
| minLength | number | 字符串最小长度。 |
| maxLength | number | 字符串最大长度。 |
| enum | any[] | type 为 enum 时的允许值,按深比较。 |
| whitespace | boolean | 为 true 时纯空白字符串视为失败。 |
| skipIfEmpty | boolean | 默认 true:值为空且非必填时跳过其余校验。 |
| transform | (value: any) => any | 校验前对原始值做转换。 |
| validator | (rule: Rule, value: any, values: any) => Promise<string | any> | string | undefined | null | 自定义校验,返回字符串表示失败;可异步。 |
| debounceMs | number | 异步校验的防抖时长(毫秒)。 |
| validateTrigger | string | string[] | 这条规则在哪些触发时机执行。 |
| warningOnly | boolean | 为 true 时失败只算警告,不阻塞提交。 |
FormInstance
useForm 返回的表单实例,本页只列常用方法。
| 字段 | 类型 | 说明 |
|---|---|---|
| submit | () => void | 校验全部字段并结算 onFinish / onFinishFailed。 |
| resetFields | (names?: AllPathsKeys<Values>[]) => void | 重置指定字段(缺省全部)到初始值。 |
| getFieldValue | (name: AllPathsKeys<Values>) => any | 读取单个字段的值。 |
| getFieldsValue | (names?: AllPathsKeys<Values>[]) => Values | 读取指定字段(缺省全部)的值。 |
| setFieldValue | (name: AllPathsKeys<Values>, value: any) => void | 写入单个字段的值。 |
| setFieldsValue | (values: DeepPartial<Values>) => void | 批量写入字段值。 |
| validateFields | (names?: AllPathsKeys<Values>[], opts?: { dirty?: boolean }) => Promise<boolean> | 手动校验指定字段(缺省全部)。 |
| validateField | (name: AllPathsKeys<Values>) => Promise<boolean> | 手动校验单个字段。 |
| getFieldError | (name: AllPathsKeys<Values>) => string[] | 读取单个字段的错误信息。 |
| getField | (name: AllPathsKeys<Values>) => Meta | 读取单个字段的完整元信息。 |
| setDisabled | (name: AllPathsKeys<Values>, disabled: boolean) => void | 设置字段禁用态,会注入给子组件。 |
| setHidden | (name: AllPathsKeys<Values>, hidden: boolean) => void | 设置字段隐藏态,隐藏后该字段不渲染。 |
| arrayOp | (name: AllPathsKeys<Values>) => { insert; move; remove; replace; swap } | 数组字段的增删改序操作。 |
Meta
单个字段的元信息,onFieldsChange 与 form.getField 都返回它。
| 字段 | 类型 | 说明 |
|---|---|---|
| name* | string | 字段路径。 |
| value* | any | 当前值。 |
| errors* | string[] | 错误信息列表。 |
| warnings* | string[] | 警告信息列表(warningOnly 规则产生)。 |
| touched* | boolean | 是否被用户交互过。 |
| validated* | boolean | 是否已完成过校验。 |
| validating* | boolean | 是否正在校验(异步规则)。 |
ValidateErrorEntity
onFinishFailed 的入参。
| 字段 | 类型 | 说明 |
|---|---|---|
| values* | Values | 校验失败时的完整表单值。 |
| errorCount* | number | 出错字段的数量。 |
| errorFields* | Meta[] | 出错字段的元信息列表,可用于滚动定位。 |
| errorMap* | Record<string, string[]> | 字段路径 → 错误信息。 |
| warningMap* | Record<string, string[]> | 字段路径 → 警告信息。 |
| firstErrorName | string | 第一个出错字段的路径,用于自动聚焦。 |
| submittedAt* | number | 校验失败的时间戳。 |