Skyroc Native UI

Field

纵向堆叠布局的表单字段

FieldGroup + FieldItem 是自由布局的一套表单组件:FieldGroup 承载表单状态(基于 @skyroc/form),FieldItem 把标签、控件、错误与描述纵向堆叠成一个字段。与 Form / FormItem 的 Cell 行布局平行,两者共用同一套字段配置和校验体系。

import { FieldGroup, FieldItem, useForm } from '@skyroc/native-ui';

基础用法

FieldGroup 提供表单上下文,FieldItemname 声明字段路径并把子组件接到表单上。required 只写一次,星号与 required 校验规则同源;校验失败时组件会向子组件注入 errorInput 这类支持 error 变体的控件因此自动变红。

FieldBasic.tsx
Loading…
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

必填与校验

rulesrequired 共用一份事实:

  • 只写 rules={[{ required: true }]} → 自动显示星号;
  • 只写 required → 自动补一条 { required: true } 规则;
  • 显式写 required={false} → 只隐藏星号,不动已有规则。

validateTrigger 控制什么时候校验,缺省跟随 FieldGroupvalidateTrigger(默认 'onChange');传 false 则只在 form.submit() / validateFields() 时校验。

FieldValidation.tsx
Loading…
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" />)。

尺寸标签字号提示字号标签与控件间距
smtext-smtext-2xsmt-1
mdtext-basetext-xsmt-1.5
lgtext-lgtext-smmt-2
FieldSize.tsx
Loading…
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(与 FormItemmd 不同,堆叠布局里标签需要更强的层级)。没有 label 时控件不加上间距,字段整体贴着上一行。

非文本控件

缺省的取值逻辑取回调的第一个参数,同时把 TextInput 的原生事件抹平成 nativeEvent.text。所以 Input 与走 onChange(value) 约定的控件(Rate / Stepper / RadioGroup / CheckboxGroup)都不需要额外配置。

FieldControls.tsx
Loading…
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 改变更回调名。例如 Switchchecked / onCheckedChange

FieldBinding.tsx
Loading…
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,因此接管这类控件时建议用 initialValuesinitialValue 给一个明确的初值。

值转换

  • normalize:变更后、写入表单前的归一化,改的是存下来的值
  • getValueProps:取值后、传给子组件前的转换,只改显示值,不影响表单里的值。
FieldTransform.tsx
Loading…
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 的标准档位(01681012),默认 6(24px)。间距挂在内容容器上而不是根节点——componentScrollView 时根节点的样式作用于滚动视图本身,gap 落不到子项上。

FieldGap.tsx
Loading…
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(...),配合 onFinishFailedfirstErrorName 做「滚动到第一个出错字段」。

样式覆盖

className 追加到根容器上,classNames 按 slot 细粒度覆盖。

组件slot作用位置
FieldGrouproot容器组件(component)本身
FieldGroupcontent子项包裹层,gap 就挂在这里
FieldItemroot字段根容器 View
FieldItemlabel标签文字
FieldItemrequired必填星号
FieldItemcontrol控件外层容器(有标签时带上间距)
FieldItemextra错误与描述的外层容器
FieldItemmessage错误文案
FieldItemdescription描述文本
FieldStyles.tsx
Loading…
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

属性说明类型默认值
formuseForm 创建的表单实例,不传时内部自建一个FormInstance<Values>-
initialValues表单初始值,只在首次挂载时生效DeepPartial<Values>-
component容器组件,可传 ScrollView / KeyboardAwareScrollView 等,其自身属性照常透传ElementTypeView
gap子项间距档位,对应 Uniwind 的 gap-*FieldGroupGap6
validateTrigger字段默认的校验触发时机,可被 FieldItem 覆盖string | string[]'onChange'
validateMessages默认错误文案模板,支持 ${min} 这类占位符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-
className根容器类名string-
classNames各 slot 的类名覆盖SlotClassNames<'content' | 'root'>-
ref容器组件实例的 ref,用于 scrollTo / measure 等命令式操作Ref<ElementType>-

FieldItem

属性说明类型默认值
name*字段名,支持 a.b[0] 这样的路径AllPathsKeys<Values>-
children*子组件,接收注入的 value / onChange / error / disabledReactElement-
label标签文本,为空时整行不渲染string-
required是否显示必填星号;缺省由 rules 推导,显式传 true 会自动补一条 required 规则boolean-
rules校验规则Rule[]-
description描述文本,显示在控件下方、错误文案之后string-
size尺寸,只影响标签 / 错误 / 描述的排版,不会透传给子组件'sm' | 'md' | 'lg''lg'
initialValue字段初始值,优先级低于 FieldGroup 的 initialValuesany-
valuePropName值属性名string'value'
trigger触发取值的回调名string'onChange'
validateTrigger触发校验的回调名,传 false 则只在提交时校验;缺省跟随 FieldGroupstring | 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 的类名覆盖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-* 标准档位。

0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12

FieldGroupSlots

FieldGroup 可通过 classNames 覆盖的 slot 名称。

'content' | 'root'

FieldItemSlots

FieldItem 可通过 classNames 覆盖的 slot 名称;纯结构的 labelRow 不在其中。

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

FieldItemClassNames

FieldItem 的 classNames 取值形态,等价于 SlotClassNames<FieldItemSlots>。

Partial<Record<FieldItemSlots, string>>

AllPathsKeys

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

string

SlotClassNames

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

Partial<Record<Slots, string>>