@kne/react-form
v3.1.13
Published
react-form 是一个轻量级且功能强大的 React 表单库,专为现代化应用设计,提供了简洁的 API 和完整的表单管理解决方案。
Keywords
Readme
react-form
描述
react-form 是一个轻量级且功能强大的 React 表单库,专为现代化应用设计,提供了简洁的 API 和完整的表单管理解决方案。
安装
npm i --save @kne/react-form概述
核心特性
📦 轻量级状态管理
- 基于 React Context + State,零额外依赖
- 支持嵌套分组和复杂数据结构
- 实时状态更新,O(1) 高效数据访问
🎛️ 灵活字段控制
useFieldHook,字段级精细化控制- 支持字段级拦截器与关联联动
- 内置防抖,智能去空值处理
📡 事件驱动架构
- 基于事件发射器的解耦设计
- 支持表单/字段/分组三级事件监听
- 便于扩展和自定义
✅ 强大验证系统
- 内置规则:必填、手机号、邮箱、长度等
- 支持异步远程验证
- 可自定义规则与错误提示
📁 分组管理
- 支持无限嵌套分组结构
- GroupList 动态增删,轻松管理
- 支持分组级数据批量操作
🎮 表单 API
useFormApiHook,完整操作能力- 支持获取/设置表单数据
- 支持字段级验证控制
设计理念
Form 组件采用分层架构设计,将表单管理、状态管理、事件处理、验证逻辑等职责分离,通过 Context 进行数据传递,实现了高度解耦和可扩展性。
核心架构
Context 层
Form 通过 Provider 将表单的核心能力传递给子组件,包括:
emitter- 事件发射器,负责表单内部的事件通信openApi- 表单操作接口,提供数据获取、设置、验证等方法task- 任务管理器,处理异步任务队列formState- 表单状态,使用 Map 存储所有字段的状态setFormState- 表单状态更新函数formIsMount- 表单挂载状态rules- 验证规则集合,包含内置规则和自定义规则interceptor- 拦截器配置onSubmit/onError/onFormDataChange- 提交、错误与数据变化回调
Provider 层次结构
Form (根组件)
├─ Provider (Context 传递)
│ ├─ FormApiProvider (封装 openApi)
│ │ └─ FormEvent (事件监听和处理)
│ │ └─ children (表单字段)字段系统
useField Hook
const fieldProps = useField({
name: 'fieldName', // 字段名称(必填)
label: '字段标签', // 字段标签
rule: 'REQ LEN-3-10', // 验证规则
interceptor: 'trim', // 拦截器
associations: { ... } // 字段关联
});字段状态
每个字段都有独立的状态对象,包含:
id- 字段唯一标识name- 字段名称groupName- 所属分组名称groupIndex- 分组索引value- 字段值label- 字段标签rule- 验证规则validate- 验证状态(INIT/PENDING/PASS/ERROR)errMsg- 错误信息
表单状态
使用 Map 结构存储所有字段状态,通过 formStateRef.current 持有引用,确保状态更新的一致性。
验证规则
规则格式
验证规则支持三种格式:
- 字符串格式:
"REQ LEN-3-10 EMAIL"- 空格分隔多个规则 - 函数格式:自定义验证函数
- 正则表达式:直接使用正则验证
规则解析
规则格式:{RULE_NAME}-{arg1}-{arg2}-{arg3}...
示例:
- "REQ" → 规则名:REQ,无参数
- "LEN-3-10" → 规则名:LEN,参数:min=3, max=10
- "REQ LEN-3-10" → 多个规则组合解析步骤:
- 按空格分割规则字符串 →
["REQ", "LEN-3-10"] - 对每个规则按
-分割 →["LEN", "3", "10"] - 第一部分为规则名(转为大写匹配) →
LEN - 后续部分为规则参数 →
[3, 10]
内置规则
| 规则名 | 参数 | 说明 | 验证逻辑 | 错误提示 |
|--------|------|------|---------|---------|
| REQ | 无 | 必填验证 | 值非空(null/undefined/'') | - |
| TEL | 无 | 手机号验证 | 匹配 /^1[0-9]{10}$/ | "请输入有效的手机号" |
| EMAIL | 无 | 邮箱验证 | 匹配邮箱正则 | "请输入有效的邮箱" |
| LEN | min, max | 长度验证 | min ≤ 长度 ≤ max | "%s长度必须大于min/小于max/等于min" |
自定义规则
规则函数接收参数:(value, ...args, { data, field })
// 同步验证规则
rules: {
PASSWORD_STRENGTH: (value, level, { data }) => {
const strength = checkStrength(value);
return {
result: strength >= level,
errMsg: strength >= level ? '' : '密码强度不足',
data: { strength }
};
}
}
// 异步验证规则
rules: {
CHECK_UNIQUE: async (value, { field }) => {
const exists = await api.checkUnique(field.name, value);
return {
result: !exists,
errMsg: exists ? '该值已存在' : ''
};
}
}
// 使用数据联动
rules: {
MATCH_PASSWORD: (value, { data }) => {
return {
result: value === data.password,
errMsg: '两次输入的密码不一致'
};
}
}验证流程
单个字段验证流程:
1. 触发验证
├─ 用户输入 (onChange + 防抖)
├─ 失焦事件 (onBlur)
├─ 手动调用 validateField()
└─ 表单提交 (validateAll())
↓
2. 查找字段
├─ 通过 id 查找
├─ 通过 name 查找
├─ 通过 groupName + groupIndex + name 查找
└─ 未找到则跳过
↓
3. 解析验证规则
├─ 分割规则字符串 (空格分隔)
├─ 匹配内置规则 (REQ, TEL, EMAIL, LEN)
└─ 匹配自定义规则
↓
4. 执行验证
├─ 更新字段状态为 PENDING
├─ 依次执行每个规则
├─ 同步规则立即返回结果
└─ 异步规则等待 Promise
↓
5. 处理验证结果
├─ 全部通过 → 状态设为 PASS
├─ 任意失败 → 状态设为 ERROR,记录错误信息
└─ 更新 formState
↓
6. 触发事件
├─ emit('form-field:validate:{fieldId}')
├─ 触发关联字段的 associations 回调
└─ 计算表单整体 isPass 状态表单整体验证流程:
1. 触发 validateAll()
↓
2. 遍历所有字段
├─ 对每个字段执行单字段验证流程
└─ 收集验证结果
↓
3. 判断表单状态
├─ 所有字段都 PASS → isPass = true
└─ 存在任意 ERROR 或 PENDING → isPass = false
↓
4. 返回验证结果
└─ 提交处理验证规则执行顺序:
规则按照声明顺序依次执行,遇到第一个失败的规则即停止。
示例:rule="REQ LEN-3-20 EMAIL"
- 检查必填 (REQ)
- 检查长度 (LEN-3-20)
- 检查邮箱格式 (EMAIL)
若第 1 步失败,则不会执行后续检查。
字段关联
设计机制
字段关联是 react-form 实现表单字段联动的核心机制,通过监听关联字段的变化,自动更新目标字段的值。
配置结构
{
fields: [{ name: 'field1' }, { name: 'field2' }],
callback: ({ target, origin, openApi }) => {
// 当关联字段变化时,更新目标字段
openApi.setFieldValue(target, computedValue);
}
}| 属性 | 类型 | 必填 | 说明 |
|------|------|------|------|
| fields | Array<FieldReference> | 是 | 关联字段列表,当这些字段变化时触发回调 |
| callback | Function | 是 | 关联回调函数,接收变化信息并更新目标字段 |
FieldReference 引用类型
// 单字段引用
{ name: 'fieldName' }
// 分组字段引用
{ name: 'fieldName', groupName: 'groupName' }
// 分组特定项引用
{
name: 'fieldName',
groupName: 'groupName',
groupIndex: 0 // 索引从 0 开始
}Callback 回调参数
callback: ({ target, origin, openApi, data }) => {
// target: 目标字段的完整对象
// - id: 字段唯一标识
// - name: 字段名称
// - value: 字段当前值
// - groupName: 所属分组(如果有)
// - groupIndex: 分组索引(如果有)
// origin: 源字段的值(仅在单字段关联时有值)
// openApi: 表单 API 对象,提供操作方法
// - getFormData(): 获取表单数据
// - setFieldValue(target, value): 设置字段值
// - setFieldValidate(target, validate): 设置验证状态
// data: 表单完整数据(包含所有字段)
}触发时机
- 关联字段值变化 - 用户输入或程序修改关联字段的值
- 关联字段验证完成 - 字段验证通过后触发
- 表单数据批量设置 - 通过
setFormData()或setFields()批量更新数据
执行顺序
关联更新会递归触发,注意避免循环依赖。
避免循环关联
// ❌ 错误:循环依赖
<Input name="a" associations={{ fields: [{ name: 'b' }], callback: ({ origin }) => origin }} />
<Input name="b" associations={{ fields: [{ name: 'a' }], callback: ({ origin }) => origin }} />
// ✅ 正确:单向依赖
<Input name="fullName" associations={{
fields: [{ name: 'firstName' }, { name: 'lastName' }],
callback: ({ data }) => \`\${data.firstName}\${data.lastName}\`
}} />实用场景
详见 associations.js 示例文件,包含以下场景:
- 单字段值复制 - 将一个字段的值自动复制到另一个字段
- 多字段值拼接 - 将多个字段的值拼接成一个新值
- 数值计算 - 基于多个字段的值进行计算
- 分组字段聚合 - 对分组内的多个字段值进行汇总
条件显示与赋值见 linkage.js:用 useWatch 订阅 form:field:set-value 后条件渲染字段;显示时用 setField 赋值(未挂载会写入 pending),显示后继续用 associations 跟随源字段。
拦截器
拦截器类型
| 类型 | 执行时机 | 用途 | 方向 |
|------|---------|------|------|
| input | 值存入表单前 | 数据清理、转换、规范化 | 外部 → 内部 |
| output | 值从表单取出时 | 数据格式化、转换 | 内部 → 外部 |
注册机制
// 全局拦截器(所有表单共享)
import { interceptors } from 'react-form';
// 注册输入拦截器
interceptors.input.use('trim', value => value.trim());
// 注册输出拦截器
interceptors.output.use('formatDate', value => {
return value ? new Date(value).toISOString() : null;
});表单级拦截器(优先级高于全局)
<Form
interceptors={{
input: [
{
name: 'customTrim',
exec: value => value.trim()
}
],
output: [
{
name: 'uppercase',
exec: value => value.toUpperCase()
}
]
}}
>字段级拦截器配置
<Input
name="email"
rule="EMAIL"
interceptor="trim" // 指定使用的拦截器
/>优先级
优先级从高到低:
1. 字段指定拦截器
2. 表单级拦截器
3. 全局拦截器
同名拦截器:后注册的覆盖先注册的常用拦截器
import { interceptors } from 'react-form';
// 常用输入拦截器
interceptors.input.use('trim', v => v?.trim?.());
interceptors.input.use('toUpperCase', v => v?.toUpperCase?.());
interceptors.input.use('toLowerCase', v => v?.toLowerCase?.());
interceptors.input.use('removeComma', v => v?.replace?.(/,/g, ''));
// 常用输出拦截器
interceptors.output.use('toNumber', v => v ? parseFloat(v) : null);
interceptors.output.use('formatCurrency', v => v ? \`¥\${v.toFixed(2)}\` : '¥0.00');
interceptors.output.use('defaultEmpty', v => v || '');分组管理
GroupList 组件
用于管理动态增删的分组字段。
<GroupList ref={ref} name="groupName" defaultLength={2}>
{({ index, onAdd, onRemove, length }) => (
// 分组项内容
)}
</GroupList>回调参数
| 参数 | 类型 | 说明 |
|------|------|------|
| index | number | 当前项的索引 |
| onAdd | Function | 添加新项的函数 |
| onRemove | Function | 移除当前项的函数 |
| length | number | 分组当前项数 |
表单 API
useSubmit Hook
const { isLoading, isPass, onClick } = useSubmit();| 参数 | 类型 | 说明 |
|------|------|------|
| isLoading | boolean | 提交中状态 |
| isPass | boolean | 表单是否通过验证 |
| onClick | Function | 提交函数 |
useReset Hook
const { onClick } = useReset();| 参数 | 类型 | 说明 |
|------|------|------|
| onClick | Function | 重置函数 |
useFormApi Hook
const { openApi } = useFormApi();openApi 方法
| 类别 | 方法 | 说明 |
|------|------|------|
| 数据操作 | data / setData() | 获取/设置表单数据 |
| | getFormData() / setFormData() | 获取/设置表单数据 |
| | getField() / getFields() | 获取单个/多个字段 |
| | setField() / setFields() | 设置单个/多个字段属性 |
| | setFieldValue() | 设置字段值 |
| 验证操作 | validateField() | 验证单个字段 |
| | validateAll() | 验证所有字段 |
| | setFieldValidate() | 设置字段验证状态 |
| | isPass | 判断表单是否通过验证 |
| 错误处理 | errors | 获取所有错误信息 |
| 表单控制 | submit() | 提交表单 |
| | reset() | 重置表单 |
| | onReady() | 表单就绪回调(已 mount 则立即执行) |
| | onDestroy() | 表单销毁回调 |
事件系统
事件类型
| 事件名称 | 触发时机 | 参数 |
|---------|---------|------|
| form:submit | 表单提交时 | (args) - 提交参数 |
| form:submit:complete | 提交完成时 | - |
| form:reset | 表单重置时 | - |
| form:validate | 表单验证时 | - |
| form:set-data | 设置表单数据时 | ({ data, runValidate }) |
| form:set-fields | 设置字段属性时 | ({ data, runValidate }) |
| form-group:change | 分组数据变化时 | ({ parentId, name, list }) |
| form-group:remove | 分组移除时 | ({ parentId, name }) |
| form-field:validate:{fieldId} | 字段验证时 | - |
| form-field:change:{fieldId} | 字段值变化时 | - |
| form:mount | 表单挂载完成时 | - |
| form:unmount | 表单卸载时 | - |
监听方式
// 在组件内通过 emitter 监听
const { emitter } = useFormContext();
// 监听表单提交
const submitListener = emitter.addListener('form:submit', (args) => {
console.log('表单提交', args);
});
// 监听数据变化
const dataListener = emitter.addListener('form:set-data', ({ data }) => {
console.log('数据变化', data);
});
// 清理监听器
useEffect(() => {
return () => {
submitListener.remove();
dataListener.remove();
};
}, []);生命周期钩子
const formApiRef = useRef();
formApiRef.current.onReady(() => {
console.log('表单已就绪');
// 可以在这里执行初始化操作
});
formApiRef.current.onDestroy(() => {
console.log('表单即将销毁');
// 可以在这里执行清理操作
});生命周期
表单生命周期
| 阶段 | 触发时机 | 执行操作 | |------|---------|---------| | 挂载 | Form 组件首次渲染 | - 初始化 formState (Map 结构)- 创建事件发射器- 初始化 openApi- 设置初始数据- 绑定事件监听器 | | 更新 | Form 组件属性变化 | - 更新 rules 配置- 更新 interceptors 配置- data 只更新后挂载字段的初始值,不重放已挂载字段 | | 卸载 | Form 组件销毁 | - 清理所有事件监听器- 清理异步任务- 释放内存资源 |
字段生命周期
| 阶段 | 状态 | 说明 | 可执行操作 | |------|------|------|-----------| | PRE_INIT | 预初始化 | 字段刚创建,尚未绑定到表单状态 | - | | INIT | 已初始化 | 字段已注册到表单,可以交互 | 获取/设置值、验证、触发事件 |
字段状态转换流程
字段创建
↓
PRE_INIT (useField 初始化)
↓
INIT (字段注册到 formState)
↓
用户交互
↓
PENDING (正在验证)
↓
PASS 或 ERROR (验证结果)
↓
用户修改或手动重置
↓
INIT (回到初始状态)验证状态枚举
| 状态值 | 枚举名 | 说明 | UI 展示建议 | |--------|--------|------|------------| | 0 | INIT | 初始状态 | 不显示错误提示 | | 1 | PASS | 验证通过 | 不显示错误提示 | | 2 | ERROR | 验证失败 | 显示错误信息 | | 3 | PENDING | 验证中 | 显示加载状态 |
示例
示例代码
- 基本示例
- 展示基本的表单使用方式,包括表单绑定、验证规则、提交和重置
- _ReactForm(@kne/current-lib_react-form),antd(antd)
const { default: ReactForm, useField, useSubmit, useReset } = _ReactForm;
const { Input: AntInput, Button, Space, Card, Alert, Typography, message } = antd;
const { Text } = Typography;
// ========================================
// 通用组件
// ========================================
const Input = props => {
const fieldProps = useField(props);
const isError = fieldProps.errState === 2;
const isValidating = fieldProps.errState === 3;
return (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 4 }}>
<Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
</div>
<div>
<AntInput
ref={fieldProps.fieldRef}
type={props.type || 'text'}
value={fieldProps.value || ''}
onChange={e => {
fieldProps.onChange(e.target.value);
fieldProps.triggerValidate();
}}
onBlur={fieldProps.triggerValidate}
placeholder={props.placeholder}
status={isError ? 'error' : undefined}
style={{ width: props.width || 200 }}
/>
{fieldProps.errMsg && (
<Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
{fieldProps.errMsg}
</Text>
)}
{isValidating && (
<Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
验证中...
</Text>
)}
</div>
</div>
);
};
const SubmitButton = ({ children, isPassButton = false }) => {
const { isLoading, isPass, onClick } = useSubmit();
return (
<Button
type="primary"
onClick={onClick}
disabled={isPassButton ? (isLoading || !isPass) : isLoading}
loading={isLoading}
>
{children}
</Button>
);
};
const ResetButton = () => {
const { onClick } = useReset();
return <Button onClick={onClick}>重置</Button>;
};
// ========================================
// 基本表单示例
// ========================================
const BaseExample = () => {
return (
<Card title="基本表单示例" style={{ marginBottom: 24 }}>
<ReactForm
debug
data={{ name: '哈哈哈' }}
onSubmit={async data => {
await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 3000);
});
console.log('submit:', data);
message.success('提交成功: ' + JSON.stringify(data, null, 2));
}}
>
<Input name="name" label="名称" rule="REQ LEN-0-10" />
<Input name="email" label="邮箱" rule="REQ EMAIL" />
<Input name="phone" label="手机号" rule="REQ TEL" />
<Space>
<SubmitButton>提交</SubmitButton>
<ResetButton />
</Space>
</ReactForm>
</Card>
);
};
// ========================================
// isPass 测试示例
// ========================================
const IsPassStatusDisplay = () => {
const { isPass } = useSubmit();
return (
<Alert
message={`表单验证状态:${isPass ? '全部通过' : '存在错误'}`}
description={isPass ? '所有字段验证通过,可以提交' : '请检查并修正错误信息'}
type={isPass ? 'success' : 'error'}
showIcon
style={{ marginBottom: 20 }}
/>
);
};
const IsPassExample = () => {
return (
<Card title="isPass 测试示例" extra={
<Text type="secondary" style={{ fontSize: 12 }}>
所有字段在输入停止后(失焦)触发校验
</Text>
}>
<ReactForm
debug
data={{
username: '',
email: '',
password: '',
confirmPassword: '',
age: ''
}}
onSubmit={async data => {
await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
console.log('submit:', data);
message.success('提交成功: ' + JSON.stringify(data, null, 2));
}}
>
<IsPassStatusDisplay />
<div style={{ marginBottom: 16 }}>
<Text strong>基本信息</Text>
</div>
<Input
name="username"
label="用户名"
rule="REQ LEN-3-20"
placeholder="请输入用户名(3-20字符)"
width={250}
/>
<Input
name="email"
label="邮箱"
rule="REQ EMAIL"
placeholder="请输入邮箱地址"
width={250}
/>
<Input
name="age"
label="年龄"
rule="REQ LEN-1-3"
placeholder="请输入年龄(1-3位数字)"
width={250}
/>
<div style={{ marginBottom: 16, marginTop: 16 }}>
<Text strong>安全信息</Text>
</div>
<Input
name="password"
label="密码"
rule="REQ LEN-6-20"
placeholder="请输入密码(6-20字符)"
type="password"
width={250}
/>
<Input
name="confirmPassword"
label="确认密码"
rule="REQ LEN-6-20"
placeholder="请再次输入密码"
type="password"
width={250}
/>
<SubmitButton isPassButton>提交注册</SubmitButton>
</ReactForm>
</Card>
);
};
// ========================================
// 主组件
// ========================================
const App = () => {
return (
<div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<BaseExample />
<IsPassExample />
</Space>
</div>
);
};
render(<App />);
- defaultValue 初值
- 通过 defaultValue 为表单字段设置初始值,FormInner 组件内部使用 useFormApi 访问表单 API
- _ReactForm(@kne/current-lib_react-form),antd(antd)
const { default: ReactForm, useField, useSubmit, useReset, useFormApi } = _ReactForm;
const { useRef } = React;
const { Button, Space, Card, Input: AntInput, Typography, message, Divider, Descriptions, Select, Row, Col } = antd;
const { Text } = Typography;
// ========================================
// 通用输入组件
// ========================================
const Input = props => {
const fieldProps = useField(props);
const isError = fieldProps.errState === 2;
return (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 4 }}>
<Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
</div>
<div>
<AntInput
ref={fieldProps.fieldRef}
type="text"
value={fieldProps.value || ''}
onChange={e => fieldProps.onChange(e.target.value)}
onBlur={fieldProps.triggerValidate}
placeholder={props.placeholder}
status={isError ? 'error' : undefined}
style={{ width: 200 }}
/>
{fieldProps.errMsg && (
<Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
{fieldProps.errMsg}
</Text>
)}
</div>
</div>
);
};
// ========================================
// Object 类型字段组件 (defaultValue 为对象)
// ========================================
const AddressField = props => {
const fieldProps = useField(props);
const isError = fieldProps.errState === 2;
const value = fieldProps.value || {};
const handleChange = (key, newValue) => {
fieldProps.onChange({
...value,
[key]: newValue
});
};
return (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 4 }}>
<Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
</div>
<Row gutter={8}>
<Col>
<Select
value={value.province || undefined}
onChange={v => handleChange('province', v)}
placeholder="省份"
style={{ width: 120 }}
options={[
{ value: '北京市', label: '北京市' },
{ value: '上海市', label: '上海市' },
{ value: '广东省', label: '广东省' },
{ value: '浙江省', label: '浙江省' }
]}
/>
</Col>
<Col>
<Select
value={value.city || undefined}
onChange={v => handleChange('city', v)}
placeholder="城市"
style={{ width: 120 }}
options={[
{ value: '朝阳区', label: '朝阳区' },
{ value: '海淀区', label: '海淀区' },
{ value: '浦东新区', label: '浦东新区' },
{ value: '天河区', label: '天河区' }
]}
/>
</Col>
<Col>
<AntInput value={value.detail || ''} onChange={e => handleChange('detail', e.target.value)} placeholder="详细地址" style={{ width: 200 }} />
</Col>
</Row>
{fieldProps.errMsg && (
<Text type="danger" style={{ fontSize: 12 }}>
{fieldProps.errMsg}
</Text>
)}
</div>
);
};
const TimeRangeField = props => {
const fieldProps = useField(props);
const isError = fieldProps.errState === 2;
const value = fieldProps.value || { start: '', end: '' };
const handleChange = (key, newValue) => {
fieldProps.onChange({
...value,
[key]: newValue
});
};
return (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 4 }}>
<Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
</div>
<Space>
<AntInput type="time" value={value.start || ''} onChange={e => handleChange('start', e.target.value)} placeholder="开始时间" style={{ width: 150 }} />
<Text>至</Text>
<AntInput type="time" value={value.end || ''} onChange={e => handleChange('end', e.target.value)} placeholder="结束时间" style={{ width: 150 }} />
</Space>
{fieldProps.errMsg && (
<div>
<Text type="danger" style={{ fontSize: 12 }}>
{fieldProps.errMsg}
</Text>
</div>
)}
</div>
);
};
const SubmitButton = ({ children }) => {
const { isLoading, onClick } = useSubmit();
return (
<Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
{children}
</Button>
);
};
const ResetButton = () => {
const { onClick } = useReset();
return <Button onClick={onClick}>重置</Button>;
};
// ========================================
// FormInner 组件:在内部使用 useFormApi
// ========================================
const FormInner = () => {
const { openApi: formApi } = useFormApi();
const formData = formApi.getFormData();
return (
<Card type="inner" title="FormInner 组件 (使用 useFormApi)" style={{ marginBottom: 16 }}>
<Descriptions column={1} size="small" bordered>
<Descriptions.Item label="当前表单数据">
<Text code>{JSON.stringify(formData)}</Text>
</Descriptions.Item>
<Descriptions.Item label="操作">
<Space wrap>
<Button
size="small"
onClick={() => {
const name = formData.name || '默认姓名';
formApi.setField({ name: 'name', value: name + '_modified' });
message.success('已修改 name 字段');
}}>
修改姓名
</Button>
<Button
size="small"
onClick={() => {
formApi.setFields([
{ name: 'email', value: '[email protected]' },
{ name: 'phone', value: '13800138000' }
]);
message.success('已批量设置字段');
}}>
批量设置
</Button>
<Button
size="small"
onClick={() => {
formApi.reset();
message.info('表单已重置');
}}>
重置表单
</Button>
</Space>
</Descriptions.Item>
</Descriptions>
</Card>
);
};
// ========================================
// 主示例
// ========================================
const BaseExample = () => {
return (
<div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
<Card title="defaultValue 初值示例">
<Space direction="vertical" style={{ width: '100%' }}>
<Text type="secondary">通过 defaultValue 为表单字段设置初始值,支持基础类型和对象类型</Text>
<ReactForm
debug
onSubmit={async data => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('submit:', data);
message.success('提交成功');
}}>
<FormInner />
<Divider>基础类型 defaultValue</Divider>
<Input name="name" label="姓名" rule="REQ LEN-0-10" defaultValue="张三" placeholder="请输入姓名" />
<Input name="email" label="邮箱" rule="REQ EMAIL" defaultValue="[email protected]" placeholder="请输入邮箱" />
<Input name="phone" label="手机号" rule="REQ TEL" defaultValue="13900139000" placeholder="请输入手机号" />
<Divider>Object 类型 defaultValue</Divider>
<AddressField
name="address"
label="地址"
rule="REQ"
defaultValue={{
province: '北京市',
city: '朝阳区',
detail: '望京街道'
}}
/>
<TimeRangeField
name="workTime"
label="工作时间"
defaultValue={{
start: '09:00',
end: '18:00'
}}
/>
<Divider />
<Space>
<SubmitButton>提交</SubmitButton>
<ResetButton />
</Space>
</ReactForm>
</Space>
</Card>
</div>
);
};
render(<BaseExample />);
- ref 操作 API
- 通过 ref 访问表单的 openApi,实现数据获取、字段设置、验证控制等操作
- _ReactForm(@kne/current-lib_react-form),antd(antd)
const { default: ReactForm, useField, useSubmit, useReset } = _ReactForm;
const { useRef } = React;
const { Button, Space, Card, Input: AntInput, Divider, Typography, message } = antd;
const { Text } = Typography;
const Input = props => {
const fieldProps = useField(props);
const isError = fieldProps.errState === 2;
const isValidating = fieldProps.errState === 3;
return (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 4 }}>
<Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
</div>
<div>
<AntInput
ref={fieldProps.fieldRef}
type="text"
value={fieldProps.value || ''}
onChange={e => fieldProps.onChange(e.target.value)}
onBlur={fieldProps.triggerValidate}
status={isError ? 'error' : undefined}
style={{ width: 200 }}
/>
{fieldProps.errMsg && (
<Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
{fieldProps.errMsg}
</Text>
)}
{isValidating && (
<Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
验证中...
</Text>
)}
</div>
</div>
);
};
const SubmitButton = ({ children }) => {
const { isLoading, onClick } = useSubmit();
return (
<Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
{children}
</Button>
);
};
const ResetButton = () => {
const { onClick } = useReset();
return <Button onClick={onClick}>重置</Button>;
};
const BaseExample = () => {
const formApiRef = useRef();
return (
<div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
<Card title="ref 操作 API 示例" bordered={false}>
<Card type="inner" title="操作面板" style={{ marginBottom: 16 }}>
<Space wrap>
<Button type="primary" onClick={() => {
const data = formApiRef.current.data;
console.log('表单数据:', data);
message.success('请查看控制台');
}}>
获取表单值
</Button>
<Button onClick={() => {
formApiRef.current.setField({ name: 'name', value: '设置的新名称' });
}}>
设置 name 字段值
</Button>
<Button onClick={() => {
formApiRef.current.setFields([
{ name: 'name', value: '张三' },
{ name: 'email', value: '[email protected]' },
{ name: 'phone', value: '13800138000' }
]);
}}>
批量设置字段值
</Button>
<Button danger onClick={() => {
formApiRef.current.setFieldValidate({
name: 'email',
validate: { status: 2, msg: '邮箱格式不正确' }
});
}}>
设置校验错误
</Button>
<Button onClick={() => {
formApiRef.current.reset();
}}>
重置表单
</Button>
</Space>
</Card>
<ReactForm ref={formApiRef}>
<Input name="name" label="姓名" rule="REQ LEN-0-10" />
<Input name="email" label="邮箱" rule="REQ EMAIL" />
<Input name="phone" label="手机号" rule="REQ TEL" />
<div>
<Space>
<SubmitButton>提交</SubmitButton>
<ResetButton />
</Space>
</div>
</ReactForm>
</Card>
</div>
);
};
render(<BaseExample />);
- useFormApi Hook
- 使用 useFormApi Hook 在表单内部访问和控制表单状态
- _ReactForm(@kne/current-lib_react-form),antd(antd)
const { default: ReactForm, useField, useSubmit, useReset, useFormApi } = _ReactForm;
const { useRef } = React;
const { Button, Space, Card, Input: AntInput, Divider, Typography, message } = antd;
const { Text } = Typography;
const Input = props => {
const fieldProps = useField(props);
const isError = fieldProps.errState === 2;
const isValidating = fieldProps.errState === 3;
return (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 4 }}>
<Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
</div>
<div>
<AntInput
ref={fieldProps.fieldRef}
type="text"
value={fieldProps.value || ''}
onChange={e => fieldProps.onChange(e.target.value)}
onBlur={fieldProps.triggerValidate}
status={isError ? 'error' : undefined}
style={{ width: 200 }}
/>
{fieldProps.errMsg && (
<Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
{fieldProps.errMsg}
</Text>
)}
{isValidating && (
<Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
验证中...
</Text>
)}
</div>
</div>
);
};
const SubmitButton = ({ children }) => {
const { isLoading, onClick } = useSubmit();
return (
<Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
{children}
</Button>
);
};
const ResetButton = () => {
const { onClick } = useReset();
return <Button onClick={onClick}>重置</Button>;
};
const Options = () => {
const { openApi: formApi } = useFormApi();
return (
<Card type="inner" title="操作面板 (useFormApi)" style={{ marginBottom: 16 }}>
<Space wrap>
<Button type="primary" onClick={() => {
const data = formApi.getFormData();
console.log('表单数据:', data);
message.success('请查看控制台');
}}>
获取表单值
</Button>
<Button onClick={() => {
formApi.setField({ name: 'name', value: '修改后的姓名' });
}}>
设置 name 字段
</Button>
<Button onClick={() => {
formApi.setFields([
{ name: 'name', value: '李四' },
{ name: 'email', value: '[email protected]' },
{ name: 'phone', value: '13900139000' }
]);
}}>
批量设置字段
</Button>
<Button danger onClick={() => {
formApi.setFieldValidate({
name: 'phone',
validate: { status: 2, msg: '手机号格式错误' }
});
}}>
设置验证错误
</Button>
<Button onClick={() => {
formApi.reset();
}}>
重置表单
</Button>
</Space>
</Card>
);
};
const BaseExample = () => {
const formApiRef = useRef();
return (
<div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
<Card title="useFormApi Hook 示例" bordered={false}>
<ReactForm ref={formApiRef}>
<Options />
<Input name="name" label="姓名" rule="REQ LEN-0-10" />
<Input name="email" label="邮箱" rule="REQ EMAIL" />
<Input name="phone" label="手机号" rule="REQ TEL" />
<div>
<Space>
<SubmitButton>提交</SubmitButton>
<ResetButton />
</Space>
</div>
</ReactForm>
</Card>
</div>
);
};
render(<BaseExample />);
- 动态分组
- 使用 Group 和 GroupList 实现动态增删的表单分组,支持嵌套分组
- _ReactForm(@kne/current-lib_react-form),antd(antd)
const { default: ReactForm, useField, useSubmit, useReset, GroupList } = _ReactForm;
const { useRef } = React;
const { Button, Space, Card, Input: AntInput, Tag, Typography, message } = antd;
const { Text } = Typography;
const Input = props => {
const fieldProps = useField(props);
const isError = fieldProps.errState === 2;
const isValidating = fieldProps.errState === 3;
return (
<div style={{ marginBottom: 8 }}>
<div style={{ marginBottom: 4 }}>
<Text type={isError ? 'danger' : undefined} style={{ fontSize: 12 }}>
{fieldProps.label}
</Text>
</div>
<div>
<AntInput
ref={fieldProps.fieldRef}
type="text"
value={fieldProps.value || ''}
onChange={e => fieldProps.onChange(e.target.value)}
onBlur={fieldProps.triggerValidate}
status={isError ? 'error' : undefined}
size="small"
style={{ width: 120 }}
/>
{fieldProps.errMsg && (
<Text type="danger" style={{ marginLeft: 4, fontSize: 12 }}>
{fieldProps.errMsg}
</Text>
)}
{isValidating && (
<Text type="secondary" style={{ marginLeft: 4, fontSize: 12 }}>
验证中...
</Text>
)}
</div>
</div>
);
};
const SubmitButton = ({ children }) => {
const { isLoading, onClick } = useSubmit();
return (
<Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
{children}
</Button>
);
};
const ResetButton = () => {
const { onClick } = useReset();
return <Button onClick={onClick}>重置</Button>;
};
const ChildrenGroup = () => {
const ref = useRef(null);
return (
<div style={{ marginTop: 12 }}>
<div style={{ fontSize: 12, color: '#666', marginBottom: 8 }}>子分组:</div>
<GroupList ref={ref} name="inner" defaultLength={0} reverseOrder={false}>
{({ index: innerIndex, onRemove: innerRemove, length: innerLength }) => {
return (
<div
key={innerIndex}
style={{
padding: 12,
marginBottom: 8,
background: '#e8e8e8',
borderRadius: 4
}}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ fontSize: 12 }}>子项 {innerIndex + 1}</span>
<Input name="detail" label="详情" rule="LEN-0-20" />
<Button size="small" danger onClick={innerRemove}>
删除
</Button>
</div>
</div>
);
}}
</GroupList>
<div style={{ marginTop: 8 }}>
<Button size="small" onClick={() => ref.current.onAdd()}>
添加子项
</Button>
</div>
</div>
);
};
const BaseExample = () => {
const ref = useRef();
const formApiRef = useRef();
return (
<div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
<Card title="动态分组示例" bordered={false}>
<Space wrap style={{ marginBottom: 20 }}>
<Button
type="primary"
onClick={() => {
formApiRef.current.setField({
name: 'name',
groupName: 'group',
groupIndex: 0,
value: '第一项名称'
});
}}>
设置第一项名称
</Button>
<Button
onClick={() => {
formApiRef.current.setField({
name: 'name',
groupName: 'group',
value: '所有项名称'
});
}}>
设置所有项名称
</Button>
<Button
onClick={() => {
formApiRef.current.setFormData({
group: [
{ name: '张三', des: '描述1' },
{ name: '李四', des: '描述2' },
{ name: '王五', des: '描述3' }
]
});
}}>
批量设置数据
</Button>
</Space>
<ReactForm
ref={formApiRef}
onSubmit={data => {
console.log('submit:', data);
message.success('提交成功: ' + JSON.stringify(data, null, 2));
}}>
<div style={{ marginBottom: 16 }}>
<Button type="primary" onClick={() => ref.current.onAdd()}>
添加
</Button>
</div>
<GroupList ref={ref} name="group" defaultLength={1}>
{({ index, onRemove, length }) => {
return (
<div
key={index}
style={{
padding: 16,
marginBottom: 16,
border: '1px solid #ddd',
borderRadius: 8,
background: '#fafafa'
}}>
<div style={{ fontWeight: 'bold', marginBottom: 12 }}>
联系人 {index + 1} (共 {length} 项)
</div>
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<Input name="name" label="姓名" rule="REQ LEN-0-10" />
<Input name="phone" label="手机号" rule="TEL" />
<Input name="email" label="邮箱" rule="EMAIL" />
</div>
<ChildrenGroup key={index}/>
<div style={{ marginTop: 12 }}>
<Button danger size="small" onClick={onRemove}>
删除联系人 {index + 1}
</Button>
</div>
</div>
);
}}
</GroupList>
<div style={{ marginTop: 20 }}>
<Space>
<SubmitButton>提交</SubmitButton>
<ResetButton />
</Space>
</div>
</ReactForm>
</Card>
</div>
);
};
render(<BaseExample />);
- 嵌套 GroupList setFormData
- 验收外层+内层 GroupList 一次 setFormData 后内层数据完整(setFormData 同步更新 initFormData)
- _ReactForm(@kne/current-lib_react-form),antd(antd)
/**
* 验证:外层 GroupList + 内层 GroupList,一次 setFormData 后
* 每条外层项的内层列表都应有数据(修复:setFormData 同步更新 initFormData)。
*
* 操作:点「一次 setFormData」→ 三条技能下依据均应有内容;再「读取 getFormData」对照。
*/
const { default: ReactForm, useField, useSubmit, useReset, useFormApi, GroupList } = _ReactForm;
const { useRef, useState } = React;
const { Button, Space, Card, Input: AntInput, Typography, message, Alert, Divider } = antd;
const { Text, Paragraph } = Typography;
const SAMPLE_DATA = {
skill: [
{
name: '技能 A',
contentItems: [
{ title: '依据 A1', description: '描述 A1', source: 'JD' },
{ title: '依据 A2', description: '描述 A2', source: '报告' }
]
},
{
name: '技能 B',
contentItems: [
{ title: '依据 B1', description: '描述 B1', source: 'JD' },
{ title: '依据 B2', description: '描述 B2', source: '报告' }
]
},
{
name: '技能 C',
contentItems: [
{ title: '依据 C1', description: '描述 C1', source: 'JD' },
{ title: '依据 C2', description: '描述 C2', source: '报告' }
]
}
]
};
const Input = props => {
const fieldProps = useField(props);
const isError = fieldProps.errState === 2;
return (
<div style={{ marginBottom: 8, minWidth: 140 }}>
<div style={{ marginBottom: 4 }}>
<Text type={isError ? 'danger' : undefined} style={{ fontSize: 12 }}>
{fieldProps.label}
</Text>
</div>
<AntInput
ref={fieldProps.fieldRef}
value={fieldProps.value ?? ''}
onChange={e => fieldProps.onChange(e.target.value)}
onBlur={fieldProps.triggerValidate}
status={isError ? 'error' : undefined}
size="small"
/>
</div>
);
};
const ContentItems = () => {
const ref = useRef(null);
return (
<div style={{ marginTop: 8, padding: 8, background: '#f0f5ff', borderRadius: 4 }}>
<Text strong style={{ fontSize: 12 }}>
依据(内层 GroupList / contentItems)
</Text>
<GroupList ref={ref} name="contentItems" defaultLength={0} reverseOrder={false}>
{({ index, onRemove }) => (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: 8,
alignItems: 'flex-end',
marginTop: 8,
padding: 8,
background: '#fff',
borderRadius: 4,
border: '1px dashed #91caff'
}}>
<Text style={{ fontSize: 12 }}>依据 {index + 1}</Text>
<Input name="title" label="标题" />
<Input name="description" label="描述" />
<Input name="source" label="来源" />
<Button size="small" danger onClick={onRemove}>
删
</Button>
</div>
)}
</GroupList>
<Button size="small" style={{ marginTop: 8 }} onClick={() => ref.current?.onAdd()}>
添加依据
</Button>
</div>
);
};
const SnapshotPanel = () => {
const { openApi } = useFormApi();
const [snap, setSnap] = useState(null);
return (
<div style={{ marginTop: 12 }}>
<Button
onClick={() => {
const data = openApi.getFormData();
setSnap(data);
console.log('[nested-set-form-data] getFormData', data);
const summary = (data.skill || []).map((s, i) => ({
index: i,
name: s?.name,
contentItemsLen: Array.isArray(s?.contentItems) ? s.contentItems.length : 0,
contentItems: s?.contentItems
}));
console.log('[nested-set-form-data] contentItems summary', summary);
message.info('已打印 getFormData,见控制台');
}}>
读取 getFormData
</Button>
{snap ? (
<pre style={{ marginTop: 8, fontSize: 11, maxHeight: 240, overflow: 'auto', background: '#fafafa', padding: 8 }}>
{JSON.stringify(snap, null, 2)}
</pre>
) : null}
</div>
);
};
const Toolbar = () => {
const { openApi } = useFormApi();
return (
<Space wrap style={{ marginBottom: 16 }}>
<Button
type="primary"
onClick={() => {
console.log('[nested-set-form-data] setFormData once', SAMPLE_DATA);
openApi.setFormData(SAMPLE_DATA, false);
}}>
一次 setFormData(模拟 AI 填充)
</Button>
<Button
onClick={() => {
openApi.setFormData({ skill: [{ name: '', contentItems: [] }] }, false);
}}>
重置为 1 条空技能
</Button>
</Space>
);
};
const SubmitButton = ({ children }) => {
const { isLoading, onClick } = useSubmit();
return (
<Button type="primary" onClick={onClick} loading={isLoading}>
{children}
</Button>
);
};
const ResetButton = () => {
const { onClick } = useReset();
return <Button onClick={onClick}>重置</Button>;
};
const BaseExample = () => {
const skillRef = useRef();
return (
<div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
<Card title="嵌套 GroupList + setFormData 复现" bordered={false}>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message="验收点"
description={
<Paragraph style={{ marginBottom: 0 }}>
初始 1 条外层技能(defaultLength=1),内层依据 defaultLength=0。一次 setFormData 写入 3 条技能、每条 2 条依据后,<strong>三条技能的依据都应有内容</strong>(依赖 setFormData 同步更新 initFormData)。
</Paragraph>
}
/>
<ReactForm
onSubmit={data => {
console.log('submit', data);
message.success('已提交,见控制台');
}}>
<Toolbar />
<div style={{ marginBottom: 12 }}>
<Button type="dashed" onClick={() => skillRef.current?.onAdd()}>
手动添加技能
</Button>
</div>
<GroupList ref={skillRef} name="skill" defaultLength={1} reverseOrder={false}>
{({ index, onRemove, length }) => (
<div
style={{
padding: 16,
marginBottom: 12,
border: '1px solid #d9d9d9',
borderRadius: 8,
background: '#fff'
}}>
<div style={{ fontWeight: 600, marginBottom: 8 }}>
技能 {index + 1} / 共 {length}
</div>
<Input name="name" label="技能名称" rule="REQ" />
<ContentItems />
<Button danger size="small" style={{ marginTop: 8 }} onClick={onRemove}>
删除技能
</Button>
</div>
)}
</GroupList>
<Divider />
<SnapshotPanel />
<Space style={{ marginTop: 16 }}>
<SubmitButton>提交</SubmitButton>
<ResetButton />
</Space>
</ReactForm>
</Card>
</div>
);
};
render(<BaseExample />);
- 字段关联
- 通过 associations 实现字段间的联动关系,支持单字段联动、多字段联动、分组联动和计算联动
- _ReactForm(@kne/current-lib_react-form),antd(antd)
const { default: ReactForm, useField, useSubmit, useReset, GroupList } = _ReactForm;
const { useRef } = React;
const { Button, Space, Card, Input: AntInput, Tag, Typography, message } = antd;
const { Text } = Typography;
const Input = props => {
const fieldProps = useField(props);
const isError = fieldProps.errState === 2;
const isValidating = fieldProps.errState === 3;
return (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 4 }}>
<Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
</div>
<div>
<AntInput
{...fieldProps.associationOptions}
ref={fieldProps.fieldRef}
type="text"
value={fieldProps.value || ''}
onChange={e => fieldProps.onChange(e.target.value)}
onBlur={fieldProps.triggerValidate}
status={isError ? 'error' : undefined}
style={{ width: 200 }}
/>
{fieldProps.errMsg && (
<Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
{fieldProps.errMsg}
</Text>
)}
{isValidating && (
<Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
验证中...
</Text>
)}
</div>
</div>
);
};
const SubmitButton = ({ children }) => {
const { isLoading, onClick } = useSubmit();
return (
<Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
{children}
</Button>
);
};
const ResetButton = () => {
const { onClick } = useReset();
return <Button onClick={onClick}>重置</Button>;
};
const BaseExample = () => {
const ref = useRef(null);
const formApiRef = useRef(null);
return (
<div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
<Card title="字段关联示例" bordered={false}>
<ReactForm
ref={formApiRef}
debug
onSubmit={data => {
console.log('submit:', data);
message.success('提交成功: ' + JSON.stringify(data, null, 2));
}}>
<Card
type="inner"
title={
<Space>
1. 单字段关联<Tag color="blue">描述跟随名称</Tag>
</Space>
}
style={{ marginBottom: 16 }}>
<Input name="name" label="名称" rule="REQ LEN-0-10" />
<Input
name="des"
label="描述"
rule="LEN-0-10"
associations={{
fields: [{ name: 'name' }],
callback: ({ target, origin, openApi }) => {
openApi.setFieldValue(target, origin.value);
}
}}
/>
</Card>
<Card
type="inner"
title={
<Space>
2. 多字段关联<Tag color="green">姓名拼接全名</Tag>
</Space>
}
style={{ marginBottom: 16 }}>
<Space wrap>
<Input name="familyName" label="姓" rule="REQ LEN-0-10" />
<Input name="firstName" label="名" rule="REQ LEN-0-10" />
</Space>
<Input
name="fullName"
label="全名"
rule="LEN-0-20"
associations={{
fields: [{ name: 'familyName' }, { name: 'firstName' }],
callback: ({ target, openApi }) => {
const { firstName, familyName } = openApi.getFormData();
openApi.setFieldValue(target, `${familyName || ''}${firstName || ''}`);
}
}}
/>
</Card>
<Card
type="inner"
title={
<Space>
3. 计算关联<Tag color="orange">金额除以比例</Tag>
</Space>
}
style={{ marginBottom: 16 }}>
<Space wrap>
<Input name="money" label="总金额" />
<Input name="ratio" label="比例" />
</Space>
<Input
name="all"
label="每份金额"
associations={{
fields: [{ name: 'money' }, { name: 'ratio' }],
callback: ({ target, openApi }) => {
const { money, ratio } = openApi.getFormData();
const numMoney = parseFloat(money) || 0;
const numRatio = parseFloat(ratio) || 1;
openApi.setFieldValue(target, numRatio > 0 ? (numMoney / numRatio).toFixed(2) : '');
}
}}
/>
</Card>
<Card
type="inner"
title={
<Space>
4. 分组关联<Tag color="purple">汇总求和</Tag>
</Space>
}
style={{ marginBottom: 16 }}>
<Button type="primary" onClick={() => ref.current.onAdd()} style={{ marginBottom: 12 }}>
添加数量项
</Button>
<GroupList ref={ref} name="group" defaultLength={2}>
{({ index, onRemove }) => (
<Space key={index} style={{ padding: 12, background: '#f0f0f0', borderRadius: 6, marginBottom: 8, width: '100%' }}>
<Tag color="blue">项 {index + 1}</Tag>
<Input name="sum" label="数量" />
<Button danger size="small" onClick={onRemove}>
删除
</Button>
</Space>
)}
</GroupList>
<Input
name="amount"
label="总数"
associations={{
fields: [{ name: 'sum', groupName: 'group' }],
callback: ({ target, openApi }) => {
const { group } = openApi.getFormData();
const total = (group || []).filter(item => item.sum > 0).reduce((a, b) => a + parseInt(b.sum), 0);
openApi.setFieldValue(target, total > 0 ? total.toString() : '');
}
}}
/>
</Card>
<div style={{ marginTop: 16 }}>
<Space>
<SubmitButton>提交</SubmitButton>
<ResetButton />
</Space>
</div>
</ReactForm>
</Card>
</div>
);
};
render(<BaseExample />);
- 联动显示与赋值
- 根据表单数据条件显示字段;用按钮一次 setFields 给不同条件的字段整体赋值
- _ReactForm(@kne/current-lib_react-form),antd(antd)
const { default: ReactForm, useField, useSubmit, useReset, useFormApi } = _ReactForm;
const { useEffect, useState } = React;
const { Button, Space, Card, Input: AntInput, Select: AntSelect, Radio, Switch, Tag, Typography, message } = antd;
const { Text } = Typography;
const Input = props => {
const fieldProps = useField(props);
const isError = fieldProps.errState === 2;
const isValidating = fieldProps.errState === 3;
return (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 4 }}>
<Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
</div>
<div>
<AntInput
{...fieldProps.associationOptions}
ref={fieldProps.fieldRef}
type="text"
value={fieldProps.value || ''}
onChange={e => fieldProps.onChange(e.target.value)}
onBlur={fieldProps.triggerValidate}
status={isError ? 'error' : undefined}
style={{ width: 200 }}
/>
{fieldProps.errMsg && (
<Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
{fieldProps.errMsg}
</Text>
)}
{isValidating && (
<Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
验证中...
</Text>
)}
</div>
</div>
);
};
const SelectField = props => {
const { options, ...rest } = props;
const fieldProps = useField(rest);
const isError = fieldProps.errState === 2;
return (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 4 }}>
<Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
</div>
<AntSelect
{...fieldProps.associationOptions}
value={fieldProps.value}
onChange={value => {
fieldProps.onChange(value);
fieldProps.triggerValidate();
}}
options={options}
status={isError ? 'error' : undefined}
style={{ width: 200 }}
/>
{fieldProps.errMsg && (
<Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
{fieldProps.errMsg}
</Text>
)}
</div>
);
};
const RadioField = props => {
const { options, ...rest } = props;
const fieldProps = useField(rest);
return (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 4 }}>
<Text>{fieldProps.label}</Text>
</div>
<Radio.Group
value={fieldProps.value}
onChange={e => {
fieldProps.onChange(e.target.value);
fieldProps.triggerValidate();
}}>
{options.map(item => (
<Radio key={item.value} value={item.value}>
{item.label}
</Radio>
))}
</Radio.Group>
</div>
);
};
const SwitchField = props => {
const fieldProps = useField(props);
return (
<div style={{ marginBottom: 16 }}>
<Space>
<Switch
checked={!!fieldProps.value}
onChange={checked => {
fieldProps.onChange(checked);
fieldProps.triggerValidate();
}}
/>
<Text>{fieldProps.label}</Text>
</Space>
</div>
);
};
const SubmitButton = ({ children }) => {
const { isLoading, onClick } = useSubmit();
return (
<Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
{children}
</Button>
);
};
const ResetButton = () => {
const { onClick } = useReset();
return <Button onClick={onClick}>重置</Button>;
};
const useWatch = name => {
const { openApi, emitter } = useFormApi();
const [value, setValue] = useState(() => (openApi.getFormData() || {})[name]);
useEffect(() => {
const syncFromForm = () => {
setValue((openApi.getFormData() || {})[name]);
};
const onSetValue = payload => {
if (payload.path === name) {
setValue(payload.value);
}
};
const setToken = emitter.addListener('form:field:set-value', onSetValue);
const resetToken = emitter.addListener('form:reset', syncFromForm);
return () => {
setToken.remove();
resetToken.remove();
};
}, [emitter, name, openApi]);
return value;
};
const Watch = ({ name, children }) => children(useWatch(name));
const toCompanyEmail = name => {
const local = String(name || '').replace(/[^a-zA-Z0-9_.-]/g, '');
return `${local || 'contact'}@example.com`;
};
const FillOnShow = ({ name, from, map }) => {
const { openApi } = useFormApi();
useEffect(() => {
const data = openApi.getFormData() || {};
if (data[name] !== undefined && data[name] !== '') {
return;
}
const source = data[from];
const next = map ? map(source, data) : source;
if (next === undefined || next === '') {
return;
}
openApi.setField({ name, value: next });
}, []);
return null;
};
const AssignButtons = ({ items }) => {
const { openApi } = useFormApi();
return (
<Space wrap style={{ marginBottom: 12 }}>
{items.map(item => (
<Button
key={item.label}
onClick={() => {
const data = openApi.getFormData() || {};
const fields = item.getFields ? item.getFields(data) : item.fields;
openApi.setFields(fields);
message.success(`已赋值:${item.label}`);
}}>
{item.label}
</Button>
))}
</Space>
);
};
const BaseExample = () => {
return (
<div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
<Card title="表单数据联动显示与赋值" bordered={false}>
<ReactForm
debug
onSubmit={data => {
console.log('submit:', data);
message.success('提交成功: ' + JSON.stringify(data, null, 2));
}}>
<Card
type="inner"
title={
<Space>
1. 按表单数据条件显示字段<Tag color="blue">隐藏字段卸载</Tag>
</Space>
}
style={{ marginBottom: 16 }}>
<Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
用 useWatch 读当前表单值,再条件渲染字段。隐藏时卸载,提交不含该字段;再次显示会从 pending 恢复刚才填过的值。
下方按钮一次 setFields 条件字段和对应输入:当前未挂载的字段会进 pending,切过去就能带出。
</Text>
<AssignButtons
items={[
{
label: '填入邮箱方案',
fields: [
{ name: 'contactType', value: 'email' },
{ name: 'email', value: '[email protected]' }
]
},
{
label: '填入手机方案',
fields: [
{ name: 'contactType', value: 'phone' },
{ name: 'phone', value: '13800138000' }
]
}
]}
/>
<RadioField
name="contactType"
label="联系方式"
defaultValue="email"
options={[
{ label: '邮箱', value: 'email' },
{ label: '手机', value: 'phone' }
]}
/>
<Watch name="contactType">
{contactType =>
contactType === 'email' ? (
<Input name="email" label="邮箱" rule="REQ EMAIL" />
) : contactType === 'phone' ? (
<Input name="phone" label="手机" rule="REQ TEL" />
) : null
}
</Watch>
</Card>
<Card
type="inner"
title={
<Space>
2. 显示时赋值,并继续跟随<Tag color="green">setField + associations</Tag>
</Space>
}
style={{ marginBottom: 16 }}>
<Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
打开开关后挂载昵称:FillOnShow 用 setField 把名称抄过去(字段尚未挂载时会写入 pending);之后名称变化由 associations 同步。
</Text>
<AssignButtons
items={[
{
label: '打开昵称并抄名称',
getFields: data => [
{ name: 'sameAsName', value: true },
{ name: 'nickname', value: data.name || '示例昵称' }
]
},
{
label: '关掉昵称只改名称',
fields: [
{ name: 'sameAsName', value: false },
{ name: 'name', value: '王五' }
]
}
]}
/>
<Input name="name" label="名称" rule="REQ LEN-0-10" />
<SwitchField name="sameAsName" label="使用名称作为昵称" />
<Watch name="sameAsName">
{sameAsName =>
sameAsName ? (
<>
<FillOnShow name="nickname" from="name" />
<Input
name="nickname"
label="昵称"
rule="REQ LEN-0-10"
associations={{
fields: [{ name: 'name' }],
callback: ({ target, origin, openApi }) => {
openApi.setFieldValue(target, origin.value);
}
}}
/>
</>
) : null
}
</Watch>
</Card>
<Card
type="inner"
title={
<Space>
3. 类型切换后显示并计算赋值<Tag color="orange">setField 计算值</Tag>
</Space>
}
style={{ marginBottom: 16 }}>
<Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
选「企业」后显示公司名和联系邮箱:公司名默认用名称,邮箱按名称拼出示例地址。提交时仅包含当前显示的字段。
</Text>
<AssignButtons
items={[
{
label: '切到企业并填公司信息',
getFields: data => [
{ name: 'userType', value: 'company' },
{ name: 'companyName', value: data.name ? `${data.name}科技` : '示例科技' },
{ name: 'companyEmail', value: toCompanyEmail(data.name) }
]
},
{
label: '切回个人',
fields: [{ name: 'userType', value: 'person' }]
}
]}
/>
<SelectField
name="userType"
label="用户类型"
defaultValue="person"
options={[
{ label: '个人', value: 'person' },
{ label: '企业', value: 'company' }
]}
/>
<Watch name="userType">
{userType =>
userType === 'company' ? (
<>
<FillOnShow name="companyName" from="name" />
<FillOnShow
name="companyEmail"
from="name"
map={name => toCompanyEmail(name)}
/>
<Input
name="companyName"
label="公司名称"
rule="REQ"
associations={{
fields: [{ name: 'name' }],
callback: ({ target, origin, openApi }) => {
openApi.setFieldValue(target, origin.value);
}
}}
/>
<Input
name="companyEmail"
label="企业邮箱"
rule="REQ EMAIL"
associations={{
fields: [{ name: 'name' }],
callback: ({ target, origin, openApi }) => {
openApi.setFieldValue(target, toCompanyEmail(origin.value));
}
}}
/>
</>
) : null
}
</Watch>
</Card>
<Card
type="inner"
title={
<Space>
4. 按条件整套赋值<Tag color="purple">一次 setFields</Tag>
</Space>
}
style={{ marginBottom: 16 }}>
<Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
一次写入条件开关和对应字段。隐藏中的字段同样能赋值,切到该条件后从 pending 显示。
</Text>
<AssignButtons
items={[
{
label: '填入完整个人示例',
fields: [
{ name: 'contactType', value: 'email' },
{ name: 'email', value: '[email protected]' },
{ name: 'name', value: '张三' },
{ name: 'sameAsName', value: true },
{ name: 'nickname', value: '张三' },
{ name: 'userType', value: 'person' }
]
},
{
label: '填入完整企业示例',
fields: [
{ name: 'contactType', value: 'phone' },
{ name: 'phone', value: '13900139000' },
{ name: 'name', value: '李四' },
{ name: 'sameAsName', value: false },
{ name: 'userType', value: 'company' },
{ name: 'companyName', value: '李四科技' },
{ name: 'companyEmail', value: '[email protected]' }
]
}
]}
/>
</Card>
<div style={{ marginTop: 16 }}>
<Space>
<SubmitButton>提交</SubmitButton>
<ResetButton />
</Space>
</div>
</ReactForm>
</Card>
</div>
);
};
render(<BaseExample />);
- 修复确认
- 确认 onFormDataChange / onReady、data 引用、defaultLength、增删不改外部 data、getFormData 实时、动态 associations
- _ReactForm(@kne/current-lib_react-form),antd(antd)
const { default: ReactForm, useField, useSubmit, useReset, useFormApi, GroupList } = _ReactForm;
const { useEffect, useRef, useState } = React;
const { Button, 