Skyroc Native UI

Form

列表式布局的表单容器与字段

FormCellGroup 的分组外观(分隔线 / 圆角 / 标题)与 @skyroc/form 的状态管理、校验上下文合成一层渲染,FormItem 则把标签、控件、错误与描述映射到 Cell 的各个插槽——因此整个表单读起来就是一张设置列表。需要自由布局时改用 FieldGroup / FieldItem

import { Form, FormComputedField, FormItem, useForm } from '@skyroc/native-ui';

基础用法

useForm 创建表单实例,FormItemname 声明字段路径并把子组件接到表单上。form.submit() 触发校验,通过走 onFinish,失败走 onFinishFailedform.resetFields() 重置回初始值。

FormBasic.tsx
Loading…
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 约定的控件(如 Switchchecked / onCheckedChange)。
FormValueAdapter.tsx
Loading…
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 提供三档密度,影响行高与标签 / 错误 / 描述的排版,同样不会透传给子组件

尺寸标签字号提示字号
smtext-smtext-2xs
mdtext-basetext-xs
lgtext-lgtext-sm
FormLayout.tsx
Loading…
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" 时提示跟着内容走,不需要缩进。

必填与校验

rulesrequired 共用一份事实:只写 rules={[{ required: true }]} 会自动显示星号,只写 required 会自动补一条 required 规则,显式写 required={false} 只隐藏星号、不动已有规则。校验失败时标签整体变红,并向子组件注入 errorInput 这类支持 error 变体的控件因此自动变红。

多条规则同时失败时提示位只显示第一条,改完再暴露下一条。

交互与禁用

onPress 让整行可点,通常用来打开选择器;箭头缺省由 onPress 推导,也可以用 showArrow / arrowDirection 显式控制。disabled 禁用整行交互,trailing 在控件与箭头之间插入自定义内容。

FormInteraction.tsx
Loading…
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 };

样式覆盖

Formborder / inset / title / classNames 直接取自 CellGroupFormItemclassName 追加到字段根容器,classNames 按 slot 覆盖。

组件slot作用位置
FormrootCellGroup 根节点
Formtitle分组标题
Formdivider字段之间的分隔线
FormItemroot字段根容器 View(背景色在这里)
FormItemcell内层 Cell 根节点,自身保持透明
FormItemcontrol控件所在区域,右对齐 Switch / Rate 这类控件时改它
FormItemlabel标签文字
FormItemrequired必填星号
FormItemextra错误与描述的外层容器
FormItemmessage错误文案
FormItemdescription描述文本
FormStyles.tsx
Loading…
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 的左右布局完全一致。

FormComputed.tsx
Loading…
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 };

子组件是 Inputcompute 必须返回字符串——RN 的 TextInput 只接受字符串,返回数字不会显示。

稳定性说明

无论校验有没有出错,FormItem 的根元素结构都保持不变(始终是 View > Cell),提示区的间距只靠切换 className 实现。这一点是刻意的:根元素类型在 Cell / View 之间切换会让 React 卸载并重建整棵子树(含内部的 TextInput),表现为校验出错后键盘收起、焦点丢失或跳到别的输入框。

API

Form

除下表外,Form 还接受 CellGroupborder / inset / title / classNames

属性说明类型默认值
formuseForm 创建的表单实例,不传时内部自建一个FormInstance<Values>-
initialValues表单初始值,只在首次挂载时生效DeepPartial<Values>-
title分组标题,string 自动包裹 TextReactNode-
inset卡片式内嵌样式(左右留边、圆角)booleanfalse
border是否在字段之间插入分隔线booleantrue
validateTrigger字段默认的校验触发时机,可被 FormItem 覆盖string | string[]'onChange'
validateMessages默认错误文案模板,支持占位符ValidateMessages-
schema表单级 schema 校验(zod / valibot 等 Standard Schema 实现)FormSchema<Values>-
preserve字段卸载后是否保留值booleantrue
clearOnDestroy表单卸载时是否清空数据booleanfalse
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 的类名覆盖,取自 CellGroupSlotClassNames<CellGroupSlots>-
ref根节点的 ref,用于 measure / 滚动定位等命令式操作Ref<View>-

FormItem

属性说明类型默认值
name*字段名,支持 a.b[0] 这样的路径AllPathsKeys<Values>-
children*子组件,接收注入的 value / onChange / error / disabledReactElement-
label标签文本,为空时标签列不渲染string-
labelAlign标签对齐方式,left 为左右排布,top 为标签压在控件上方'left' | 'top''left'
labelWidth标签列宽度,仅 labelAlign 为 left 时生效number120
required是否显示必填星号;缺省由 rules 推导,显式传 true 会自动补一条 required 规则boolean-
rules校验规则Rule[]-
description描述文本,显示在控件下方、错误文案之后string-
size尺寸,只影响行高与标签 / 错误 / 描述的排版,不会透传给子组件'sm' | 'md' | 'lg''md'
onPress整行点击回调,通常用于打开选择器() => void-
disabled禁用整行交互booleanfalse
showArrow是否显示右侧箭头,缺省由 onPress 推导boolean-
arrowDirection箭头方向,showArrow 为 false 时无效'down' | 'left' | 'right' | 'up'-
trailing右侧自定义内容,位于控件与箭头之间ReactNode-
initialValue字段初始值,优先级低于 Form 的 initialValuesany-
valuePropName值属性名string'value'
trigger触发取值的回调名string'onChange'
validateTrigger触发校验的回调名,传 false 则只在提交时校验;缺省跟随 Formstring | string[] | false-
getValueFromEvent从子组件回调参数里取值,缺省取第一个参数并抹平 TextInput 的原生事件(...args: any[]) => any-
getValueProps值传给子组件前的转换,只改显示值(value: any) => any-
normalize值写入表单前的归一化,改的是存下来的值(value: any, prevValue: any, allValues: Values) => any-
preserve子组件卸载后是否保留字段值booleantrue
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 保持一致才能对齐number120
required是否显示必填星号,计算字段不参与推导,只作展示booleanfalse
rules校验规则Rule[]-
description描述文本,显示在计算结果下方string-
size尺寸,只影响行高与标签 / 提示的排版'sm' | 'md' | 'lg''md'
valuePropName值属性名string'value'
preserve子组件卸载后是否保留字段值booleantrue
className字段根容器类名string-
classNames各 slot 的类名覆盖,与 FormItem 同一套FormItemClassNames-
ref根节点的 refRef<View>-

useForm

const [form] = useForm<Values>();

不传 formForm 会自建一个实例,只有需要在组件里命令式读写表单时才必须自己创建。实例方法见 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 不在其中。

'cell' | 'control' | 'description' | 'extra' | 'label' | 'message' | 'required' | 'root'

FormItemClassNames

FormItem 的 classNames 取值形态,等价于 SlotClassNames<FormItemSlots>。

Partial<Record<FormItemSlots, string>>

AllPathsKeys

表单值对象里所有可用的字段路径,形如 'user' | 'user.name' | 'list[0].id'。

string

RuleType

内置类型校验器,Rule.type 缺省为 string。

'boolean' | 'date' | 'email' | 'enum' | 'float' | 'hex' | 'integer' | 'number' | 'regexp' | 'string' | 'url'

FormSchema

表单级 schema:Standard Schema 实现(zod / valibot 等),或一个返回问题列表的异步函数。

StandardSchemaV1 | ((state: Values, name?: string | string[]) => Promise<StandardSchemaV1NormalizedIssue[]>)

ValidateMessages

默认错误文案模板,按 required / string.minLength / number.max 这类分层键取值,值里可写 ${min} ${max} ${len} 占位符。

{ default?: string; required?: string; string?: { len?: string; maxLength?: string; minLength?: string; pattern?: string }; number?: { invalid?: string; len?: string; max?: string; min?: string }; date?: { invalid?: string; max?: string; min?: string }; email?: string; url?: string; ... }

SlotClassNames

classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 FormItemSlots / CellGroupSlots。

Partial<Record<Slots, string>>

Rule

单条校验规则;一条规则里可以同时写多个约束,按基础校验 → 类型校验 → 自定义校验的顺序执行。

字段类型说明
requiredboolean是否必填(值为空即失败)。
typeRuleType启用哪个内置类型校验器,缺省为 string。
messagestring覆盖默认错误文案。
patternRegExp字符串正则校验。
lennumber精确的数值或字符串长度,取决于 type。
minnumber | Date | string最小数值 / 最早日期。
maxnumber | Date | string最大数值 / 最晚日期。
minLengthnumber字符串最小长度。
maxLengthnumber字符串最大长度。
enumany[]type 为 enum 时的允许值,按深比较。
whitespaceboolean为 true 时纯空白字符串视为失败。
skipIfEmptyboolean默认 true:值为空且非必填时跳过其余校验。
transform(value: any) => any校验前对原始值做转换。
validator(rule: Rule, value: any, values: any) => Promise<string | any> | string | undefined | null自定义校验,返回字符串表示失败;可异步。
debounceMsnumber异步校验的防抖时长(毫秒)。
validateTriggerstring | string[]这条规则在哪些触发时机执行。
warningOnlyboolean为 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[]>字段路径 → 警告信息。
firstErrorNamestring第一个出错字段的路径,用于自动聚焦。
submittedAt*number校验失败的时间戳。