Field
纵向堆叠布局的表单字段
FieldGroup + FieldItem 是自由布局的一套表单组件:FieldGroup 承载表单状态(基于 @skyroc/form),FieldItem 把标签、控件、错误与描述纵向堆叠成一个字段。与 Form / FormItem 的 Cell 行布局平行,两者共用同一套字段配置和校验体系。
import { FieldGroup, FieldItem, useForm } from '@skyroc/native-ui';基础用法
FieldGroup 提供表单上下文,FieldItem 用 name 声明字段路径并把子组件接到表单上。required 只写一次,星号与 required 校验规则同源;校验失败时组件会向子组件注入 error,Input 这类支持 error 变体的控件因此自动变红。
import { Button, FieldGroup, FieldItem, Input, Text, useForm } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
interface LoginForm {
/** 登录密码 */
password: string;
/** 手机号 */
phone: string;
}
const FieldBasic = () => {
const [form] = useForm<LoginForm>();
const [submitted, setSubmitted] = useState('尚未提交');
function handleFinish(values: LoginForm) {
setSubmitted(JSON.stringify(values));
}
function handleFinishFailed() {
setSubmitted('校验未通过');
}
function handleReset() {
form.resetFields();
setSubmitted('尚未提交');
}
return (
<FieldGroup<LoginForm>
className="bg-background p-4"
form={form}
onFinish={handleFinish}
onFinishFailed={handleFinishFailed}
>
{/* required 只写一次:星号与校验规则同源 */}
<FieldItem
required
label="手机号"
name="phone"
rules={[{ message: '请输入 11 位手机号', pattern: /^1\d{10}$/ }]}
>
<Input
keyboardType="number-pad"
placeholder="请输入手机号"
/>
</FieldItem>
{/* 校验失败时 error 会注入到 Input,边框同步变红 */}
<FieldItem
required
description="至少 6 位,区分大小写"
label="登录密码"
name="password"
rules={[{ message: '密码至少 6 位', minLength: 6 }]}
>
<Input
placeholder="请输入密码"
type="password"
/>
</FieldItem>
<View className="flex-row gap-3">
<Button
className="flex-1"
color="primary"
variant="solid"
onPress={() => form.submit()}
>
提交
</Button>
<Button
className="flex-1"
color="primary"
variant="outline"
onPress={handleReset}
>
重置
</Button>
</View>
<Text color="muted">提交结果:{submitted}</Text>
</FieldGroup>
);
};
export { FieldBasic };何时使用
- 独立的编辑页、登录 / 注册页这类自由布局的表单:标签压在控件上方,字段之间靠
gap拉开距离。 - 需要列表式(一行一个字段、带分隔线和分组标题)的表单时,改用
Form/FormItem。 - 只是要一个受控输入框、不需要表单状态时,直接用
Input等控件即可,不必套FieldItem。
必填与校验
rules 与 required 共用一份事实:
- 只写
rules={[{ required: true }]}→ 自动显示星号; - 只写
required→ 自动补一条{ required: true }规则; - 显式写
required={false}→ 只隐藏星号,不动已有规则。
validateTrigger 控制什么时候校验,缺省跟随 FieldGroup 的 validateTrigger(默认 'onChange');传 false 则只在 form.submit() / validateFields() 时校验。
import { Button, FieldGroup, FieldItem, Input, Text, useForm } from '@skyroc/native-ui';
import { useState } from 'react';
interface ValidationForm {
/** 联系邮箱 */
email: string;
}
const FieldValidation = () => {
const [form] = useForm<ValidationForm>();
const [result, setResult] = useState('点击提交触发校验');
function handleFinish(values: ValidationForm) {
setResult(`校验通过:${values.email}`);
}
function handleFinishFailed() {
setResult('校验未通过');
}
return (
<FieldGroup<ValidationForm>
className="bg-background p-4"
form={form}
gap={4}
onFinish={handleFinish}
onFinishFailed={handleFinishFailed}
>
<FieldItem
description="required 由 rules 推导,错误信息显示在描述上方。"
label="联系邮箱"
name="email"
rules={[
{ message: '请输入邮箱地址', required: true },
{ message: '邮箱格式不正确', type: 'email' }
]}
validateTrigger={false}
>
<Input placeholder="name@example.com" />
</FieldItem>
<Button onPress={() => form.submit()}>提交校验</Button>
<Text className="text-sm text-muted-foreground">{result}</Text>
</FieldGroup>
);
};
export { FieldValidation };多条规则同时失败时提示位只显示第一条,改完再暴露下一条——提示区固定一行,避免字段高度随错误数量跳动。星号无论必填与否都占位,只在必填时可见,因此同一组表单里各行标签的左边缘始终对齐;非必填时星号对读屏器隐藏。
尺寸
size 只影响标签 / 错误 / 描述的排版,不会透传给子组件——控件自身的尺寸要单独传(例如 <Input size="sm" />)。
| 尺寸 | 标签字号 | 提示字号 | 标签与控件间距 |
|---|---|---|---|
sm | text-sm | text-2xs | mt-1 |
md | text-base | text-xs | mt-1.5 |
lg | text-lg | text-sm | mt-2 |
import { FieldGroup, FieldItem, Input, useForm } from '@skyroc/native-ui';
interface SizeForm {
/** 大尺寸字段 */
large: string;
/** 中尺寸字段 */
medium: string;
/** 小尺寸字段 */
small: string;
}
const FieldSize = () => {
const [form] = useForm<SizeForm>();
return (
<FieldGroup<SizeForm>
className="bg-background p-4"
form={form}
gap={4}
>
<FieldItem
description="紧凑标签与辅助文字"
label="小尺寸"
name="small"
size="sm"
>
<Input
placeholder="size=sm"
size="sm"
/>
</FieldItem>
<FieldItem
description="适合常规信息录入"
label="中尺寸"
name="medium"
size="md"
>
<Input placeholder="size=md" />
</FieldItem>
<FieldItem
description="默认尺寸,标签与提示最醒目"
label="大尺寸"
name="large"
size="lg"
>
<Input
placeholder="size=lg"
size="lg"
/>
</FieldItem>
</FieldGroup>
);
};
export { FieldSize };默认是 lg(与 FormItem 的 md 不同,堆叠布局里标签需要更强的层级)。没有 label 时控件不加上间距,字段整体贴着上一行。
非文本控件
缺省的取值逻辑取回调的第一个参数,同时把 TextInput 的原生事件抹平成 nativeEvent.text。所以 Input 与走 onChange(value) 约定的控件(Rate / Stepper / RadioGroup / CheckboxGroup)都不需要额外配置。
import { FieldGroup, FieldItem, Rate, Stepper, Text, useForm } from '@skyroc/native-ui';
import { useState } from 'react';
interface ControlsForm {
/** 数量 */
quantity: number;
/** 评分 */
score: number;
}
const FieldControls = () => {
const [form] = useForm<ControlsForm>();
const [values, setValues] = useState<ControlsForm>({ quantity: 2, score: 3 });
return (
<FieldGroup<ControlsForm>
className="bg-background p-4"
form={form}
gap={4}
initialValues={values}
onValuesChange={(_, allValues) => setValues(allValues)}
>
<FieldItem
description="Stepper 通过 onChange(value) 直接收集数值。"
label="数量"
name="quantity"
size="md"
>
<Stepper min={0} />
</FieldItem>
<FieldItem
description="Rate 同样使用默认 value / onChange 约定。"
label="评分"
name="score"
size="md"
>
<Rate />
</FieldItem>
<Text className="text-sm text-muted-foreground">
当前值:数量 {values.quantity},评分 {values.score}
</Text>
</FieldGroup>
);
};
export { FieldControls };自定义值绑定
不走 value / onChange 约定的控件,用 valuePropName 改值属性名、trigger 改变更回调名。例如 Switch 是 checked / onCheckedChange。
import { FieldGroup, FieldItem, Switch, Text, useForm } from '@skyroc/native-ui';
import { useState } from 'react';
interface BindingForm {
/** 开关是否启用 */
enabled: boolean;
}
const FieldBinding = () => {
const [form] = useForm<BindingForm>();
const [enabled, setEnabled] = useState(true);
return (
<FieldGroup<BindingForm>
className="bg-background p-4"
form={form}
initialValues={{ enabled }}
onValuesChange={(_, values) => setEnabled(values.enabled)}
>
<FieldItem
classNames={{ control: 'items-start' }}
description="Switch 使用 checked 保存状态,并通过 onCheckedChange 更新。"
label="自定义绑定"
name="enabled"
trigger="onCheckedChange"
valuePropName="checked"
>
<Switch />
</FieldItem>
<Text className="text-sm text-muted-foreground">字段值:{enabled ? 'true' : 'false'}</Text>
</FieldGroup>
);
};
export { FieldBinding };字段没有值时子组件收到的是空字符串而不是 undefined,因此接管这类控件时建议用 initialValues 或 initialValue 给一个明确的初值。
值转换
normalize:变更后、写入表单前的归一化,改的是存下来的值;getValueProps:取值后、传给子组件前的转换,只改显示值,不影响表单里的值。
import { FieldGroup, FieldItem, Input, Text, useForm } from '@skyroc/native-ui';
import { useState } from 'react';
interface TransformForm {
/** 需要归一化的编码 */
code: string;
/** 原始别名 */
name: string;
}
const FieldTransform = () => {
const [form] = useForm<TransformForm>();
const [values, setValues] = useState<TransformForm>({ code: 'AB12', name: 'skyroc' });
return (
<FieldGroup<TransformForm>
className="bg-background p-4"
form={form}
gap={4}
initialValues={values}
onValuesChange={(_, allValues) => setValues(allValues)}
>
<FieldItem
description="normalize 会移除空格并转成大写后再保存。"
label="保存前归一化"
name="code"
normalize={value => String(value).replaceAll(' ', '').toUpperCase()}
size="md"
>
<Input placeholder="例如 ab 12" />
</FieldItem>
<FieldItem
description="getValueProps 只转换传给子组件的显示值。"
getValueProps={value => String(value).toUpperCase()}
label="显示值转换"
name="name"
size="md"
>
<Input />
</FieldItem>
<Text className="text-sm text-muted-foreground">
表单原值:{values.code} / {values.name}
</Text>
</FieldGroup>
);
};
export { FieldTransform };需要改「从回调参数里怎么取值」时用 getValueFromEvent,它比 normalize 更靠前,拿到的是子组件回调的原始参数。
字段间距
gap 统一控制 FieldGroup 子项之间的距离,取 Tailwind 的标准档位(0、1—6、8、10、12),默认 6(24px)。间距挂在内容容器上而不是根节点——component 传 ScrollView 时根节点的样式作用于滚动视图本身,gap 落不到子项上。
import { FieldGroup, FieldItem, Input, Text } from '@skyroc/native-ui';
import { View } from 'react-native';
const FieldGap = () => {
return (
<View className="gap-4 bg-background p-4">
<View className="rounded-xl border border-border p-3">
<Text className="mb-3 text-sm font-medium text-foreground">gap=2</Text>
<FieldGroup gap={2}>
<FieldItem
label="字段一"
name="compactFirst"
size="sm"
>
<Input size="sm" />
</FieldItem>
<FieldItem
label="字段二"
name="compactSecond"
size="sm"
>
<Input size="sm" />
</FieldItem>
</FieldGroup>
</View>
<View className="rounded-xl border border-border p-3">
<Text className="mb-3 text-sm font-medium text-foreground">gap=8</Text>
<FieldGroup gap={8}>
<FieldItem
label="字段一"
name="looseFirst"
size="sm"
>
<Input size="sm" />
</FieldItem>
<FieldItem
label="字段二"
name="looseSecond"
size="sm"
>
<Input size="sm" />
</FieldItem>
</FieldGroup>
</View>
</View>
);
};
export { FieldGap };容器组件
component 决定 FieldGroup 渲染成什么,默认 View。传什么组件就能继续传它的属性:
<FieldGroup
component={ScrollView}
contentContainerClassName="p-6"
form={form}
keyboardShouldPersistTaps="handled"
>
...
</FieldGroup>ref 拿到的是容器组件实例,因此 component={ScrollView} 时可以直接 ref.current?.scrollTo(...),配合 onFinishFailed 的 firstErrorName 做「滚动到第一个出错字段」。
样式覆盖
className 追加到根容器上,classNames 按 slot 细粒度覆盖。
| 组件 | slot | 作用位置 |
|---|---|---|
FieldGroup | root | 容器组件(component)本身 |
FieldGroup | content | 子项包裹层,gap 就挂在这里 |
FieldItem | root | 字段根容器 View |
FieldItem | label | 标签文字 |
FieldItem | required | 必填星号 |
FieldItem | control | 控件外层容器(有标签时带上间距) |
FieldItem | extra | 错误与描述的外层容器 |
FieldItem | message | 错误文案 |
FieldItem | description | 描述文本 |
import { FieldGroup, FieldItem, Input } from '@skyroc/native-ui';
const FieldStyles = () => {
return (
<FieldGroup
className="bg-background p-4"
classNames={{ content: 'rounded-xl bg-primary/5 p-4' }}
gap={4}
>
<FieldItem
required
className="rounded-xl border border-primary/20 bg-background p-3"
classNames={{ description: 'text-primary', label: 'text-primary', required: 'text-warning' }}
description="root、label、required 与 description 分别覆盖。"
label="自定义字段"
name="styled"
>
<Input placeholder="FieldItem classNames" />
</FieldItem>
</FieldGroup>
);
};
export { FieldStyles };星号与标签的排列容器(labelRow)是纯结构类,不开放覆盖。
API
FieldGroup
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| form | useForm 创建的表单实例,不传时内部自建一个 | FormInstance<Values> | - |
| initialValues | 表单初始值,只在首次挂载时生效 | DeepPartial<Values> | - |
| component | 容器组件,可传 ScrollView / KeyboardAwareScrollView 等,其自身属性照常透传 | ElementType | View |
| gap | 子项间距档位,对应 Uniwind 的 gap-* | FieldGroupGap | 6 |
| validateTrigger | 字段默认的校验触发时机,可被 FieldItem 覆盖 | string | string[] | 'onChange' |
| validateMessages | 默认错误文案模板,支持 ${min} 这类占位符 | 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 | - |
| className | 根容器类名 | string | - |
| classNames | 各 slot 的类名覆盖 | SlotClassNames<'content' | 'root'> | - |
| ref | 容器组件实例的 ref,用于 scrollTo / measure 等命令式操作 | Ref<ElementType> | - |
FieldItem
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| name* | 字段名,支持 a.b[0] 这样的路径 | AllPathsKeys<Values> | - |
| children* | 子组件,接收注入的 value / onChange / error / disabled | ReactElement | - |
| label | 标签文本,为空时整行不渲染 | string | - |
| required | 是否显示必填星号;缺省由 rules 推导,显式传 true 会自动补一条 required 规则 | boolean | - |
| rules | 校验规则 | Rule[] | - |
| description | 描述文本,显示在控件下方、错误文案之后 | string | - |
| size | 尺寸,只影响标签 / 错误 / 描述的排版,不会透传给子组件 | 'sm' | 'md' | 'lg' | 'lg' |
| initialValue | 字段初始值,优先级低于 FieldGroup 的 initialValues | any | - |
| valuePropName | 值属性名 | string | 'value' |
| trigger | 触发取值的回调名 | string | 'onChange' |
| validateTrigger | 触发校验的回调名,传 false 则只在提交时校验;缺省跟随 FieldGroup | 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 的类名覆盖 | FieldItemClassNames | - |
| ref | 根节点的 ref,用于 measure / 滚动定位等命令式操作 | Ref<View> | - |
类型
import type {
FieldGroupGap,
FieldGroupOwnProps,
FieldGroupProps,
FieldGroupSlots,
FieldItemClassNames,
FieldItemProps,
FieldItemSlots
} from '@skyroc/native-ui';表单实例与校验相关的类型(FormInstance / Rule / ValidateErrorEntity / Meta / ValidateMessages)由 form 模块统一出口,定义见 Form 文档的类型区。
FieldGroupGap
FieldGroup 子项间距档位,对应 Uniwind 的 gap-* 标准档位。
FieldGroupSlots
FieldGroup 可通过 classNames 覆盖的 slot 名称。
FieldItemSlots
FieldItem 可通过 classNames 覆盖的 slot 名称;纯结构的 labelRow 不在其中。
FieldItemClassNames
FieldItem 的 classNames 取值形态,等价于 SlotClassNames<FieldItemSlots>。
AllPathsKeys
表单值对象里所有可用的字段路径,形如 'user' | 'user.name' | 'list[0].id'。
SlotClassNames
classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 FieldGroupSlots / FieldItemSlots。