在后台系统的日常开发中,form表单是我们最常用的组件之一,不同的业务模块需求有不同的信息表单。一个复杂的表单往往由十数个不同的表单项组成,逻辑重复复杂,代码重复率也很高。因此设计一个能够通过json配置生成对应表单的组件,想必可以提升开发的体验。
需求分析
DynamicForm组件的设计需求大概可以分为以下几点
- 配置化驱动:通过 JSON 配置生成表单
- 丰富的表单项类型:支持各种常见的表单控件
- 灵活的布局系统:支持多列布局和自适应
- 强大的联动能力:支持表单项之间的依赖关系
- 易扩展:支持自定义渲染和事件处理
- 易操作:开发人员可以非常方便的使用
组件设计
JSON配置
- layout:布局配置
- cols:每行显示的表单数量,值为1、2、3、4
- fullItems:独占整行的表单项,接受一个数组,值为独占整行的表单项name
- initialValues:初始值,为整个表单赋初始值
- items:表单配置项数组
- formType:表单项类型
- name:字段名
- label:标签文本
- rules:校验规则
- visible:可见性
- props:组件属性
- dependencies:动态依赖配置
- events:事件处理器
在表单项的配置中,除了常规的formItem配置和组件内的props属性配置外,重点是dependencies和events两个参数。dependencies提供了表单项之间的联动功能,events中可以写事件处理函数,方便用户进行事件监听
整个组件采用分层设计,进行职责分离
UI层
- dynamicForm:负责状态管理和对外接口
- FormContent:负责表单布局和表单项组织
- FormItem:负责具体表单项的渲染
逻辑层
- useDynamicForm:负责表单的核心逻辑
- useFormDependencies:负责表单项动态的依赖逻辑
组件实现
DynamicForm作为组件的入口,提供组件的对外接口
可以通过getFormInstance()来获取form实例
let formInstance: DynamicFormMethods | null = null;
export const getFormInstance = () => formInstance;
const DynamicForm: React.FC<DynamicFormProps> = ({ config: rawConfig, onMount, onChange, onValuesChange, onSubmit, onValidateFailed, children }) => {
const config = useMemo(() => {
const parsedConfig = typeof rawConfig === 'string' ? JSON.parse(rawConfig) : rawConfig;
const errors = validateConfig(parsedConfig);
if (errors.length > 0) {
console.error('表单配置错误:', errors);
return processConfig({ items: [] });
}
return processConfig(parsedConfig);
}, [rawConfig]);
const [form, methods, items] = useDynamicForm(config);
formInstance = methods;
const handleDependencies = useFormDependencies(config, methods);
const handleValuesChange = useCallback(
(changedValues: Record<string, any>, allValues: Record<string, any>) => {
console.log('表单值变化:', changedValues);
handleDependencies(changedValues);
onValuesChange?.(changedValues, allValues);
onChange?.(allValues);
},
[handleDependencies, onChange, onValuesChange],
);
useEffect(() => {
onMount?.(methods);
}, [methods, onMount]);
return (
<Form form={form} initialValues={config.initialValues} onFinish={onSubmit} onFinishFailed={onValidateFailed} onValuesChange={handleValuesChange}>
<FormContent config={{ ...config, items }} />
{children}
</Form>
);
};
export type { DynamicFormMethods, FormConfig };
export default DynamicForm;
FormContent 负责表单层面的UI渲染工作
在这里提供了一个独占整行的设计,方便用户使某项表单项或自定义表单项独占整行(后续示例中有展示)
interface FormContentProps {
config: FormConfig;
}
interface GroupedItems {
normalItems: FormItemConfig[];
fullWidthItems: FormItemConfig[];
}
const FormContent: React.FC<FormContentProps> = React.memo(({ config }) => {
const { items = [], layout = {} } = config;
const { cols = 3, fullItems = [], labelAlign = 'right' } = layout;
const form = Form.useFormInstance();
// 计算栅格布局参数
const { colSpan, rowGutter } = useMemo(
() => ({
colSpan: 24 / cols,
rowGutter: { xs: 8, sm: 16, md: 24 },
}),
[cols],
);
// 分组表单项并过滤掉不可见的项
const { normalItems, fullWidthItems } = useMemo(() => {
return items.reduce<GroupedItems>(
(acc, item) => {
// 如果项目不可见,则不添加到布局中
if (item.visible === false) {
return acc;
}
if (fullItems.includes(item.name)) {
acc.fullWidthItems.push(item);
} else {
acc.normalItems.push(item);
}
return acc;
},
{ normalItems: [], fullWidthItems: [] },
);
}, [items, fullItems]);
return (
<div>
{/* 普通宽度表单项 */}
<Row gutter={rowGutter}>
{normalItems.map((item) => (
<Col key={item.name} span={colSpan}>
<FormItem {...item} labelAlign={labelAlign} form={form} />
</Col>
))}
</Row>
{/* 全宽表单项 */}
{fullWidthItems.length > 0 && (
<Row className={styles.fullItemRow}>
{fullWidthItems.map((item) => (
<Col key={item.name} span={24} className={styles.fullItemCol}>
<FormItem {...item} labelAlign={labelAlign} wrapperCol={{ span: 20 }} form={form} />
</Col>
))}
</Row>
)}
</div>
);
});
FormContent.displayName = 'FormContent';
export default FormContent;
FormItem负责对具体的表单项进行渲染
日常开发中的常规表单项,都进行了兼容,另外提供了三种特殊的表单项
- space:一个空占位项,不进行数据处理
- hidden:一个隐藏不显示的项,但会进行相关的数据处理
- render:用户自定义表单项,可以自定义渲染,允许用户决定是否参与数据处理
// 组件渲染函数映射
const FormComponentMap: Record<FormType, (props: any) => React.ReactNode> = {
input: (props) => <Input {...DEFAULT_COMPONENT_PROPS.input} {...props} />,
textarea: (props) => <Input.TextArea {...DEFAULT_COMPONENT_PROPS.textarea} {...props} />,
select: (props) => <Select {...DEFAULT_COMPONENT_PROPS.select} {...props} />,
radio: ({ options, ...rest }) => (
<Radio.Group {...DEFAULT_COMPONENT_PROPS.radio} {...rest}>
{options?.map((opt) => (
<Radio key={opt.value} value={opt.value}>
{opt.label}
</Radio>
))}
</Radio.Group>
),
checkbox: ({ options, ...rest }) => <Checkbox.Group {...DEFAULT_COMPONENT_PROPS.checkbox} options={options} {...rest} />,
switch: (props) => <Switch {...DEFAULT_COMPONENT_PROPS.switch} {...props} />,
datePicker: (props) => <DatePicker {...DEFAULT_COMPONENT_PROPS.datePicker} {...props} />,
rangePicker: (props) => <RangePicker {...DEFAULT_COMPONENT_PROPS.rangePicker} {...props} />,
cascader: (props) => <Cascader {...DEFAULT_COMPONENT_PROPS.cascader} {...props} />,
number: (props) => <InputNumber {...DEFAULT_COMPONENT_PROPS.number} {...props} />,
space: () => null,
hidden: () => null,
render: ({ render, form }) => (render ? render(form) : null),
};
在FormItem中添加了事件处理器,方便用户对事件进行监听
const FormItem: React.FC<ExtendedFormItemProps> = React.memo(
({ formType, name, label, rules, visible = true, props = {}, labelAlign = 'right', events, form }) => {
const Component = FormComponentMap[formType];
if (!Component || !visible) return null;
const mergedProps = useMemo(() => {
const defaultProps = DEFAULT_COMPONENT_PROPS[formType] || {};
const placeholder = props.placeholder || '';
// 处理事件处理器
const eventHandlers: EventHandlers = {};
if (events) {
Object.entries(events).forEach(([eventName, handler]) => {
eventHandlers[eventName] = (...args: any[]) => {
if (typeof handler === 'function') {
handler(...args);
}
if (typeof props[eventName] === 'function') {
(props[eventName] as EventHandler)(...args);
}
};
});
}
return {
...defaultProps,
...props,
...eventHandlers,
placeholder,
};
}, [formType, label, props, events]);
// 优化表单项样式
const formItemStyle = useMemo(
() => ({
marginBottom: 24,
...(props.style || {}),
}),
[props.style],
);
// space 类型只渲染一个空的 Form.Item
if (formType === 'space') {
return <Form.Item />;
}
// hidden 类型只渲染一个隐藏的 Form.Item
if (formType === 'hidden') {
return (
<Form.Item name={name} hidden>
<Input type="hidden" />
</Form.Item>
);
}
// render 类型的特殊处理
if (formType === 'render') {
const { render, collectData = true } = props;
if (!render) return null;
// 如果不需要收集数据,直接渲染
if (!collectData) {
return render(form);
}
// 需要收集数据时,包装在 Form.Item 中
return (
<Form.Item name={name} label={label} rules={rules} labelCol={{ flex: '120px' }} wrapperCol={{ flex: 'auto' }} labelAlign={labelAlign}>
{render(form)}
</Form.Item>
);
}
// 获取组件渲染函数
const renderComponent = FormComponentMap[formType];
if (!renderComponent) return null;
return (
<Form.Item
name={name}
label={label}
rules={rules}
labelCol={{ flex: '120px' }}
wrapperCol={{ flex: 'auto' }}
labelAlign={labelAlign}
style={formItemStyle}
required={rules?.some((rule) => 'required' in rule && rule.required)}
>
{renderComponent(mergedProps)}
</Form.Item>
);
},
);
FormItem.displayName = 'FormItem';
export default FormItem;
UI方面的处理基本结束,接下来是核心逻辑
useDynamicForm为组件提供了操作接口,分为数据操作和UI操作。数据方面操作依旧沿用了form本身的方法。UI方面则提供了新增表单项、更新表单项、删除表单项的方法,方便用户对表单进行动态操作
export const useDynamicForm = (config: FormConfig): [any, DynamicFormMethods, FormItemConfig[]] => {
const [form] = Form.useForm();
const [state, setState] = useState<FormState>({
items: config.items,
itemStates: {},
});
// 缓存初始配置
const initialConfigRef = useRef<FormConfig>(config);
/**
* 重置表单状态到初始配置
*/
const resetFormState = useCallback(() => {
setState({
items: initialConfigRef.current.items,
itemStates: {},
});
}, []);
/**
* 更新表单项配置
* @param name 表单项名称
* @param config 要更新的表单项配置
*/
const updateFormItem = (name: string, config: Partial<FormItemConfig>) => {
setState((prev) => {
const index = prev.items.findIndex((item) => item.name === name);
if (index === -1) return prev;
const newItems = [...prev.items];
newItems[index] = {
...newItems[index],
...config,
props: {
...newItems[index].props,
...config.props,
},
};
// 如果设置为隐藏,自动清空字段值
if (config.visible === false) {
form.setFieldValue(name, undefined);
}
return {
...prev,
items: newItems,
};
});
};
/**
* 添加新的表单项
* @param itemConfig 新表单项的配置
* @param afterItemName 插入位置的参考表单项名称,不指定则添加到末尾
*/
const addFormItem = useCallback((itemConfig: FormItemConfig, afterItemName?: string) => {
setState((prev) => {
if (prev.items.some((item) => item.name === itemConfig.name)) {
return prev;
}
const newItems = [...prev.items];
const insertIndex = afterItemName ? newItems.findIndex((item) => item.name === afterItemName) + 1 : newItems.length;
newItems.splice(insertIndex, 0, itemConfig);
return { ...prev, items: newItems };
});
}, []);
/**
* 移除表单项
* @param name 要移除的表单项名称
*/
const removeFormItem = useCallback((name: string) => {
setState((prev) => ({
...prev,
items: prev.items.filter((item) => item.name !== name),
}));
}, []);
/**
* 重置表单
* @param names 要重置的字段名列表,不传则重置所有字段
*/
const resetFields = useCallback(
(names?: string[]) => {
form.resetFields(names);
if (!names) {
resetFormState();
}
},
[form, resetFormState],
);
// 导出方法
const methods: DynamicFormMethods = {
updateFormItem,
addFormItem,
removeFormItem,
setFieldValue: form.setFieldValue,
setFieldsValue: form.setFieldsValue,
getFieldValue: form.getFieldValue,
getFieldsValue: form.getFieldsValue,
validateFields: form.validateFields,
resetFields,
};
// 更新初始配置的缓存
useEffect(() => {
initialConfigRef.current = config;
}, [config]);
/**
* 处理表单项的可见性
* 根据 itemStates 中的状态更新表单项的可见性
*/
const visibleItems = useMemo(() => {
return state.items.map((item) => ({
...item,
visible: state.itemStates[item.name]?.visible ?? item.visible,
}));
}, [state.items, state.itemStates]);
return [form, methods, visibleItems];
};
useFormDependencies 负责表单项之间的动态依赖逻辑处理
export const useFormDependencies = (config: FormConfig, methods: DynamicFormMethods) => {
const currentFieldRef = useRef<string>();
const processingRef = useRef<boolean>(false);
return useCallback(
(changedValues: Record<string, any>) => {
// 防止依赖处理过程中的循环调用
if (processingRef.current) {
return;
}
try {
processingRef.current = true;
const changedFields = Object.keys(changedValues);
// 按照表单项顺序处理依赖
for (const item of config.items) {
if (item.dependencies?.fields && item.dependencies.fields.some((field) => changedFields.includes(field))) {
currentFieldRef.current = item.name;
// 包装 methods,处理 addFormItem 的默认行为
const wrappedMethods = {
...methods,
addFormItem: (itemConfig, afterItemName) => {
methods.addFormItem(itemConfig, afterItemName || currentFieldRef.current);
},
};
// 执行依赖处理器
item.dependencies.handler(methods.getFieldsValue(), wrappedMethods);
}
}
} catch (error) {
console.error('处理表单依赖时发生错误:', error);
} finally {
processingRef.current = false;
currentFieldRef.current = undefined;
}
},
[config.items, methods],
);
};
一些工具函数
import type { FormConfig, FormItemConfig } from '../types';
// 验证表单配置
export const validateConfig = (config: FormConfig): string[] => {
const errors: string[] = [];
const names = new Set<string>();
if (!Array.isArray(config.items)) {
errors.push('配置项 items 必须是数组');
return errors;
}
config.items.forEach((item, index) => {
// space 类型只需要 name 和 formType
if (item.formType === 'space') {
// if (!item.name) {
// errors.push(`第 ${index + 1} 项缺少 name 字段`);
// }
return;
}
// 检查必填字段
if (!item.name) {
errors.push(`第 ${index + 1} 项缺少 name 字段`);
}
if (!item.formType) {
errors.push(`第 ${index + 1} 项缺少 formType 字段`);
}
// if (!item.label) {
// errors.push(`第 ${index + 1} 项缺少 label 字段`);
// }
// 检查字段名重复
if (names.has(item.name)) {
errors.push(`字段名 ${item.name} 重复`);
}
names.add(item.name);
// 检查依赖项
if (item.dependencies) {
const { fields, handler } = item.dependencies;
if (!Array.isArray(fields)) {
errors.push(`字段 ${item.name} 的依赖项 fields 必须是数组`);
}
if (typeof handler !== 'function') {
errors.push(`字段 ${item.name} 的依赖项 handler 必须是函数`);
}
}
});
return errors;
};
// 处理表单配置的默认值
export const processConfig = (config: FormConfig): FormConfig => {
const { items = [], layout = {}, initialValues = {} } = config;
// 处理布局默认值
const processedLayout = {
column: 3,
labelCol: 8,
wrapperCol: 16,
fullRow: [],
...layout,
};
// 处理表单项默认值
const processedItems = items.map((item): FormItemConfig => {
const { formType, props = {}, visible = true, rules = [] } = item;
// space 类型不需要处理 placeholder
if (formType !== 'space' && !props.placeholder && ['input', 'select', 'datePicker', 'cascader'].includes(formType)) {
props.placeholder = `请${formType === 'input' ? '输入' : '选择'}${item.label}`;
}
return {
...item,
props,
visible,
rules,
};
});
return {
initialValues,
layout: processedLayout,
items: processedItems,
};
};
// 获取依赖关系图
export const getDependencyGraph = (items: FormItemConfig[]) => {
const graph = new Map<string, Set<string>>();
items.forEach((item) => {
if (item.dependencies?.fields) {
item.dependencies.fields.forEach((field) => {
if (!graph.has(field)) {
graph.set(field, new Set());
}
graph.get(field)?.add(item.name);
});
}
});
return graph;
};
使用示例
主要展示dependencies部分的使用
layout: {
cols: 3,
fullItems: ['submit'],
},
items:[
{
formType: 'select',
name: 'subject',
label: '主教学科',
rules: [{ required: true, message: '请选择主教学科' }],
props: {
options: SUBJECT_OPTIONS,
},
dependencies: {
fields: ['subject'],
handler: (values, form) => {
const subject = values.subject;
if (subject) {
// 获取该学科对应的课程类型
const courseTypes = SUBJECT_COURSE_MAP[subject] || [];
// 更新任教课程类型的选项和值
form.setFieldValue('courseTypes', courseTypes);
} else {
// 重置为默认状态
form.setFieldValue('courseTypes', undefined);
}
},
},
},
// 根据上一个主教课程自动选择课程类型
{
formType: 'select',
name: 'courseTypes',
label: '任教课程类型',
rules: [{ required: true, message: '请选择任教课程类型' }],
props: {
disabled: true,
placeholder: '请选择任教课程',
options: COURSE_TYPE_OPTIONS,
},
},
// ... 其他配置项
{
formType: 'render',
name: 'submit',
props: {
collectData: false,
render: (form) => (
<div style={{ textAlign: 'right', marginTop: 24 }}>
<Space>
<Button onClick={() => form.resetFields()}>重置</Button>
<Button type="primary" onClick={() => form.validateFields()}>
提交
</Button>
</Space>
</div>
),
},
},
]
layout配置项中,通过设置submit表单项为独占整行,实现重置、提交按钮在表单右下方
这个配置主要是为了使该组件不仅在信息新增、编辑方面使用,也可以在筛选搜索区域使用,更方便的进行布局控制。
测试组件使用时的实现效果如下:
后续优化
目前组件还在测试阶段,未来将着重于对组件的功能扩展方面做进一步的开发和优化
如有问题还请各位大佬批评指正