适用于 Taro 3.x/4.x 的简单表单组件,不包含任何输入组件,只提供简单的表单布局/数据收集/联动/校验功能。仅支持 React。
- 完全响应式。你说用户体验?用户体验有我开发体验重要吗。
- 强劲的动态表单支持,支持动态增删 Form.Item,暴打 @antmjs/vantui 的表单组件。
- 表单快照,随时 capture / restore,缓存用户编辑状态不求人。
- 性能跟屎一样,但是我肯定不会爆炸。
yarn add taro-form-reactimport Form from "taro-form-react";
// 样式只需要在入口文件引入一次
// 包含 Label 和一些简单布局样式
import "taro-form-react/dist/styles/index.scss";
// 或者使用 css
import "taro-form-react/dist/styles/index.css";
import { Input, Button } from "@tarojs/components";
import type { FormActions } from "taro-form-react";
const Component = () => {
const formRef = useRef<FormActions>();
const handleSubmit = () => {
formRef.current?.submit();
}
const handleReset = () => {
formRef.current?.reset();
}
const handleFinish = (values) => {
console.log(values);
}
const handleFinishedFailed = (errors) => {
console.log(errors);
}
return (
<Form
// ref 用于触发表单校验、设置值、获取值等操作
// 具体支持的操作可参考后续文档
ref={formRef}
// 丢弃值为 undefined 和 null 的字段
// 默认为 true,如需保留请设置为 false
omitNil={true}
// 提交成功时触发
onFinish={handleFinish}
// 提交失败时触发(有表单元素校验失败)
onFinishFailed={handleFinishedFailed}
>
{/* 表单元素示例 */}
{/* 基础控件 */}
<Form.Item
// Form.Item 必须存在 name 属性
// 仅支持数组形式,如 ["field", "array", 0, "name"]
name={["name"]}
// Form.Item 应该劫持的输入事件,默认为 onChange
trigger="onInput"
// Form.Item 应该劫持的 value,默认为 value
valuePropName="value"
// 从事件中获取值的方法,默认为 (...args) => args[0]
getValueFromEvent={e => e.detail.value}
>
{/* 必须存在单个子元素,不能是 React.Fragment */}
{/* Form.Item 会劫持该元素的输入事件 */}
<Input placeholder="请输入你的姓名" />
</Form.Item>
{/* 保留控件 */}
{/* Form 不会保留不存在对应控件的值 */}
{/* 即使你调用了 setFieldValue 设置了它 */}
{/* 如果需要保留某些值,但不需要展示给用户编辑 */}
{/* 可以使用 Form.Keep */}
<Form.Keep
fields={[
["field", "array", 0, "gender"],
["field", "array", 1, "gender"],
]}
/>
{/* 上下文控件 */}
{/* 用于实现复杂的表单联动 */}
{/* Form.Provider 只暴露只读方法(data / getFieldValue 等) */}
{/* 如果需要写入表单数据,请通过 formRef 调用 */}
<Form.Provider>
{({ data }) => {
return (
<>
<Text>你好,{data.name}</Text>
<Button
onClick={() => {
formRef.current?.setFieldValue(["name"], "");
}}
>
清空姓名
</Button>
</>
);
}}
</Form.Provider>
<Button onClick={handleSubmit}>提交</Button>
<Button onClick={handleReset}>重置</Button>
</Form>
)
}<Form.Item
name={["phone"]}
label="手机号"
// 设置 required 会自动添加必填校验规则
// 错误文案默认为 "{label} is required"
// 可通过 Form 的 getRequiredMessage 自定义
required
rules={[
// 正则校验,值非空时才触发
{ pattern: /^1\d{10}$/, message: "请输入正确的手机号" },
]}
// 在哪些事件上触发校验,默认跟随 trigger
validateTrigger={["onInput", "onBlur"]}
trigger="onInput"
getValueFromEvent={e => e.detail.value}
>
<Input placeholder="请输入手机号" />
</Form.Item>
<Form.Item
name={["password"]}
label="密码"
rules={[
// 长度校验
{ min: 6, max: 20, message: "密码长度 6-20 位" },
// 自定义异步校验
{
validator: async (value) => {
if (value?.includes(" ")) {
return "密码不能包含空格";
}
},
},
]}
trigger="onInput"
getValueFromEvent={e => e.detail.value}
>
<Input password placeholder="请输入密码" />
</Form.Item><Form
initialValues={{ type: "personal" }}
>
<Form.Item
name={["type"]}
label="类型"
trigger="onChange"
>
<Radio.Group>
<Radio value="personal">个人</Radio>
<Radio value="company">企业</Radio>
</Radio.Group>
</Form.Item>
{/* 通过 Form.Provider 实现条件渲染 */}
{/* data 是响应式的,type 变化时会自动重渲染 */}
<Form.Provider>
{({ data }) => {
if (data.type === "company") {
return (
<Form.Item
name={["companyName"]}
label="企业名称"
required
trigger="onInput"
getValueFromEvent={e => e.detail.value}
>
<Input placeholder="请输入企业名称" />
</Form.Item>
);
}
return null;
}}
</Form.Provider>
{/* 使用 dependencies 实现依赖校验 */}
{/* 当 password 变化时,confirmPassword 会自动重新校验 */}
<Form.Item
name={["confirmPassword"]}
label="确认密码"
dependencies={[["password"]]}
rules={[{
validator: async (value) => {
const password = formRef.current?.getFieldValue(["password"]);
if (value && value !== password) {
return "两次输入的密码不一致";
}
},
}]}
trigger="onInput"
getValueFromEvent={e => e.detail.value}
>
<Input password placeholder="请再次输入密码" />
</Form.Item>
</Form><Form>
{/* 单源同步:province 变化时,自动将值同步到 city 和 district */}
<Form.Sync source={["province"]} target={[["city"], ["district"]]} />
{/* 多字段互相同步:任一字段变化时,其值同步到其余字段 */}
<Form.Sync fields={[["startDate"], ["endDate"]]} />
</Form><Form.Item
name={["date"]}
label="日期"
// transform 在 getFieldsFormattedValue / submit 时生效
// 可以将组件值转换为需要的格式
transform={(value) => value?.format("YYYY-MM-DD")}
>
<DatePicker />
</Form.Item>
<Form.Item
name={["address"]}
label="地址"
// 当 transformBehavior 为 "merge" 时
// transform 返回的对象会被 merge 到整个表单数据中
// 此时 name 会被忽略
transform={(value) => ({
province: value?.[0],
city: value?.[1],
district: value?.[2],
})}
>
<RegionPicker />
</Form.Item>import type { FormActions, FormSnapshot } from "taro-form-react";
const EditPage = () => {
const formRef = useRef<FormActions>();
// 页面进入时恢复草稿
useEffect(() => {
const saved = localStorage.getItem("draft");
if (saved) {
formRef.current?.restore(JSON.parse(saved));
}
}, []);
// 页面离开时保存草稿
const handleSaveDraft = () => {
const snapshot = formRef.current?.capture();
if (snapshot) {
localStorage.setItem("draft", JSON.stringify(snapshot));
}
};
return (
<Form ref={formRef} onFinish={handleSubmit}>
{/* ... */}
<Button onClick={handleSaveDraft}>保存草稿</Button>
</Form>
);
};import { useFormContext } from "taro-form-react";
// 在 Form 内部的任意子组件中使用
const SubmitButton = () => {
// useFormContext 返回完整的表单上下文
// 包含所有数据和操作方法
const { data, validateFields, isFieldsTouched } = useFormContext();
const disabled = !isFieldsTouched();
return (
<Button
disabled={disabled}
onClick={async () => {
const errors = await validateFields();
if (!errors) {
console.log("提交数据:", data);
}
}}
>
提交
</Button>
);
};NamePath = (string | number)[],所有字段名均使用数组形式,如 ["user", "address", 0, "city"]。
initialValues?: Record<string, any>— 表单初始值omitNil?: boolean(默认true)— 提交时是否丢弃值为undefined/null的字段onFinish?: (values: Record<string, any>) => void— 提交成功(校验通过)回调onFinishFailed?: (errors: { name: NamePath; errors: string[] }[]) => void— 提交失败(校验未通过)回调onFieldsChange?: (changedFields, allFields) => void— 字段注册/卸载时触发onValuesChange?: (changedValues, allValues) => void— 字段值变更时触发
以下配置作用于所有子 Form.Item,也可在每个 Form.Item 上单独覆盖:
colon?: boolean— Label 是否显示冒号labelProps?: FormLabelProps— 传递给 Label 的 propslayout?: "horizontal" | "vertical"— 布局方向validateFirst?: boolean | "parallel"(默认false)—true遇到第一个校验错误即停止;"parallel"并行执行所有规则showErrors?: boolean— 是否在 Form.Item 下方显示错误文本passthroughErrors?: boolean— 是否将hasError和errors透传给子组件 propstransformBehavior?: "merge" | "replace"— 当 Form.Item 设置了transform时,"merge"将转换结果合并到表单数据,"replace"直接替换对应字段updateTickLimit?: number(默认50)— onChange 节流间隔(ms),过小可能导致数据异常getRequiredMessage?: (label: string) => string— 自定义 required 规则的默认错误文案
name: NamePath— 必填,字段路径label?: ReactNode(默认null)— 标签文本,传null不渲染标签required?: boolean— 是否必填,会自动添加 required 规则(如果rules中没有)rules?: Rule[]— 校验规则,见下方「校验规则」trigger?: string(默认"onChange")— 劫持子组件的哪个事件来收集值valuePropName?: string(默认"value")— 劫持子组件的哪个 prop 来注入值getValueFromEvent?: (...args) => any(默认(...args) => args[0])— 从事件参数中提取值initialValue?: any— 字段初始值(优先级高于 Form 的initialValues)dependencies?: NamePath[]— 依赖字段,依赖变化时自动重新校验noStyle?: boolean— 不渲染外层容器,直接返回子组件hidden?: boolean— 隐藏模式,不渲染子组件但保留字段注册(Form.Keep内部使用)validateTrigger?: string[](默认[trigger])— 触发校验的事件transform?: (value: any) => any— 提交时对值进行转换className?: classNames.Argument— 外层容器 classNameinnerClassName?: classNames.Argument— 内层容器 className
Form.Item 要求有且仅有一个子元素(不能是 Fragment),会劫持该元素的输入事件和 value prop。
保留不需要展示给用户的字段值。Form 默认在字段对应的 Form.Item 卸载后会清除该字段的数据,如果需要保留某些字段的值,使用 Form.Keep。
fields: NamePath[]— 需要保留的字段列表
<Form.Keep fields={[["hiddenField"], ["nested", "value"]]} />字段同步组件,当源字段值变化时自动同步到目标字段。两种用法:
单源多目标
<Form.Sync source={["province"]} target={[["city"], ["district"]]} />source 变化时,将其值同步到所有 target 字段。
多字段互相同步
<Form.Sync fields={[["fieldA"], ["fieldB"], ["fieldC"]]} />fields 中任一字段变化时,其值同步到其余所有字段。
Label 组件,通常由 Form.Item 自动渲染,也可独立使用。
label?: ReactNode— 标签文本required?: boolean— 是否显示必填星号size?: "normal" | "small"(默认"normal")colon?: boolean(默认true)— 是否显示冒号addonAfter?: ReactNode— 标签后附加内容asteriskTookSpace?: boolean(默认true)— 星号是否占据空间(不必填时保留占位)className?: classNames.ArgumentlabelClassName?: classNames.ArgumentcolonClassName?: classNames.ArgumentcontainerClassName?: classNames.Argument
Render Props 模式的上下文访问组件,用于实现复杂的表单联动。
<Form.Provider>
{({ data, getFieldValue, getFieldsValue, getFields, getFieldError, getFieldsFormattedValue, validateFields, isFieldsTouched, capture, restore, submit, reset }) => {
return <Text>{data.name}</Text>;
}}
</Form.Provider>回调参数是只读的表单上下文(不包含 setFieldValue、setFields、resetFields、setFieldError、setData 等写入方法),加上 submit 和 reset。如果需要在 Provider 内写入表单数据,请通过 formRef 调用对应方法。
通过 ref 获取表单实例,调用以下方法:
const formRef = useRef<FormActions>();
// ...
<Form ref={formRef}>setData(data: Record<string, any>)— 直接替换整个表单数据setFieldValue(name: NamePath, value: any)— 设置单个字段的值getFieldValue(name: NamePath): any— 获取单个字段的值getFieldsValue(nameList?: NamePath[]): Record<string, any>— 获取多个字段的值,不传参返回所有getFieldsFormattedValue(nameList?: NamePath[]): Promise<Record<string, any>>— 获取经过transform处理后的值
setFields(fields: { name: NamePath; value?: any; touched?: boolean }[])— 批量设置字段值和 touched 状态getFields(nameList?: NamePath[]): { name: NamePath; value: any; touched: boolean; errors: string[] }[]— 获取字段完整信息resetFields(nameList?: NamePath[])— 重置字段到初始值并清除 touched 状态,不传参重置所有
validateFields(nameList?: NamePath[]): Promise<void | { name: NamePath; errors: string[] }[]>— 校验字段,通过返回undefined,失败返回错误数组setFieldError(name: NamePath, errors: string[])— 手动设置字段错误getFieldError(name: NamePath): string[]— 获取字段错误isFieldsTouched(nameList?: NamePath[], options?: { allTouched?: boolean }): boolean— 检查字段是否被修改过,allTouched为true时要求全部被修改
submit(): Promise<Record<string, any> | undefined>— 触发校验并提交,校验通过返回表单值并触发onFinish,失败返回undefined并触发onFinishFailedreset()— 重置所有字段
capture(): FormSnapshot— 捕获当前表单完整状态(数据 + touched + errors),返回一个可序列化的对象restore(snapshot: FormSnapshot)— 从快照恢复表单状态
capture 返回的对象可以用 JSON.stringify 序列化后存入 localStorage 等持久化存储,也可以直接保存在变量中。restore 会自动处理字段集合不一致的情况(快照中有但当前未注册的字段会在后续注册时自动恢复)。
Form.Item 的 rules 接受以下四种规则,可混合使用:
必填
{ required: boolean; message: string }正则
{ pattern: RegExp; message: string }值非空时校验,空值不触发。
长度/大小
{ min?: number; max?: number; message: string }对 number 类型比较数值大小,其他类型比较 .length。值非空时校验。
自定义
{ validator: (value: any) => Promise<void | string> }返回 string 表示错误信息,返回 void / undefined 表示通过。
在 Form 内部的任意组件中获取表单上下文:
import { useFormContext } from "taro-form-react";
const MyComponent = () => {
const { data, setFieldValue, getFieldValue } = useFormContext();
// ...
};返回值同 FormContextProps,包含所有表单操作方法。必须在 <Form> 内部使用,否则会抛出异常。