概述
在现代前端应用中,表单是用户交互的核心部分。本文将深入分析一个基于 React 和 Ant Design 的高级动态表单容器组件,它提供了强大的可配置性、灵活的布局选项和丰富的功能扩展能力。
组件核心特性
1. 高度可配置的表单结构
javascript
interface FormContainerProps {
formData?: FormValues; // 表单初始数据
formList?: FormItem[]; // 表单配置项数组
canCollapse?: boolean; // 是否可折叠
labelWidth?: string | number; // 标签宽度
clearable?: boolean; // 是否可清空
horizontal?: boolean; // 是否水平布局
defaultShow?: number; // 默认显示表单项数量
onReset?: (data: FormValues) => void; // 重置回调
onSearch?: (values: FormValues) => void; // 搜索回调
// ... 其他配置项
}
2. 多样化的表单项类型支持
组件支持多种表单项类型,包括:
-
文本输入框 (input)
-
选择器 (select)
-
级联选择器 (cascader)
-
日期范围选择器 (daterange)
-
数值范围输入 (range)
-
自定义插槽 (slot)
实现细节解析
智能标签宽度计算
javascript
const computedLabelWidth = useMemo(() => {
if (labelWidth) return labelWidth;
if (!formList.length) return '100px';
// 根据最长标签文本自动计算合适宽度
const maxLength = Math.max(...formList.map(item => item.label?.length || 0));
if (maxLength <= 4) return '80px';
if (maxLength <= 6) return '110px';
if (maxLength < 10) return '120px';
return '100px';
}, [formList, labelWidth]);
动态表单渲染机制
javascript
const renderFormItem = useCallback((item: FormItem) => {
const commonProps = {
placeholder: item.placeholder,
allowClear: clearable || item.clearable,
style: { width: item.width || 240 },
disabled: disabled || item.disabled,
'aria-label': item.label
};
switch (item.type) {
case 'input':
return <Input {...commonProps} />;
case 'select':
return (
<Select
{...commonProps}
mode={item.multiple ? 'multiple' : undefined}
onChange={(value) => handleSelectChange(value, item)}
>
{/* 选项渲染 */}
</Select>
);
// 其他类型处理...
case 'slot':
// 插槽机制实现自定义内容
return Children.toArray(children).find(
(child): child is ReactElement =>
isValidElement(child) && child.props?.slot === `${item.prop}_slot`
);
}
}, [dependencies]);
折叠功能实现
javascript
// 折叠状态管理
const [isCollapse, setIsCollapse] = React.useState(false);
// 折叠样式计算
const collapseStyle = useMemo(() => {
if (isCollapse || !canCollapse) return {};
return {
height: `${48 * Math.max(1, defaultShow)}px`,
overflow: 'hidden'
};
}, [isCollapse, defaultShow, canCollapse]);
// 折叠切换
const toggleCollapse = useCallback(() => {
setIsCollapse(prev => !prev);
}, []);
表单实例暴露与回调处理
javascript
// 暴露form实例给父组件
useImperativeHandle(ref, () => form, [form]);
// 表单值变化处理
const handleValuesChange = useCallback((changedValues: FormValues, allValues: FormValues) => {
onFormDataChange?.(allValues);
}, [onFormDataChange]);
// 搜索提交
const handleSearch = useCallback(async () => {
try {
const values = await form.validateFields();
onSearch?.(values);
} catch (error) {
console.error('Form validation failed:', error);
}
}, [form, onSearch]);
使用示例
html
import React, { useRef } from 'react';
import FormContainer from '/@/components/searchForm/index';
import { FormInstance } from 'antd';
const ExampleComponent: React.FC = () => {
const formRef = useRef<FormInstance>(null);
const formList = [
{
label: '姓名',
prop: 'name',
type: 'input',
placeholder: '请输入姓名'
},
{
label: '性别',
prop: 'gender',
type: 'select',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
]
},
{
label: '日期范围',
prop: 'dateRange',
type: 'daterange'
},
{
label: "责任人",
type: "select",
prop: "personId",
placeholder: "请选择",
options: [
],
},
{
label: "部门",
type: "input",
prop: "organizeList",
checkStrictly: true,
placeholder: "级联多选",
options: [],
},
{
label: "标签",
type: "select",
prop: "userTagIdList",
multiple: true,
collapsetags: true,
collapseTagsTooltip: true,
placeholder: "请选择",
options: [],
},
];
const handleSearch = (formData: any) => {
console.log('查询参数:', formData);
};
const handleReset = (formData: any) => {
console.log('重置表单:', formData);
};
return (
<FormContainer
ref={formRef}
formList={formList}
onSearch={handleSearch}
onReset={handleReset}
/>
);
};
export default ExampleComponent;
组件完整代码实现
html
import React, {
useMemo,
useCallback,
useImperativeHandle,
forwardRef,
ReactElement,
cloneElement,
isValidElement,
Children
} from 'react';
import {
Form,
Input,
Select,
Cascader,
DatePicker,
Button,
Space,
FormInstance,
FormProps
} from 'antd';
import {
UpOutlined,
DownOutlined
} from '@ant-design/icons';
import { FormContainerProps, FormItem, FormValues } from './types';
import './index.css';
const { RangePicker } = DatePicker;
const { Option } = Select;
const FormContainer = forwardRef<FormInstance, FormContainerProps>((props, ref) => {
const {
formData = {},
formList = [],
canCollapse = true,
labelWidth,
clearable = false,
horizontal = true,
defaultShow = 1,
onReset,
onSearch,
onSelectChange,
onCascaderChange,
onFormDataChange,
children,
loading = false,
disabled = false
} = props;
const [form] = Form.useForm();
const [isCollapse, setIsCollapse] = React.useState(false);
// 暴露form实例给父组件
useImperativeHandle(ref, () => form, [form]);
// 计算标签宽度
const computedLabelWidth = useMemo(() => {
if (labelWidth) return labelWidth;
if (!formList.length) return '100px';
const maxLength = Math.max(...formList.map(item => item.label?.length || 0));
if (maxLength <= 4) return '80px';
if (maxLength <= 6) return '110px';
if (maxLength < 10) return '120px';
return '100px';
}, [formList, labelWidth]);
// 折叠样式
const collapseStyle = useMemo(() => {
if (isCollapse || !canCollapse) return {};
return {
height: `${48 * Math.max(1, defaultShow)}px`,
overflow: 'hidden'
};
}, [isCollapse, defaultShow, canCollapse]);
// 表单值变化处理
const handleValuesChange = useCallback((changedValues: FormValues, allValues: FormValues) => {
onFormDataChange?.(allValues);
}, [onFormDataChange]);
// 选择器变化事件
const handleSelectChange = useCallback((value: unknown, item: FormItem) => {
const currentValues = form.getFieldsValue();
onSelectChange?.(item, currentValues);
}, [form, onSelectChange]);
// 级联选择变化事件
const handleCascaderChange = useCallback((value: unknown, item: FormItem) => {
const currentValues = form.getFieldsValue();
onCascaderChange?.(item, currentValues);
}, [form, onCascaderChange]);
// 重置表单
const handleReset = useCallback(() => {
try {
form.resetFields();
const resetData = form.getFieldsValue();
onReset?.(resetData);
} catch (error) {
console.error('Form reset failed:', error);
}
}, [form, onReset]);
// 查询提交
const handleSearch = useCallback(async () => {
try {
const values = await form.validateFields();
onSearch?.(values);
} catch (error) {
console.error('Form validation failed:', error);
}
}, [form, onSearch]);
// 切换折叠状态
const toggleCollapse = useCallback(() => {
setIsCollapse(prev => !prev);
}, []);
// 通用属性
const getCommonProps = useCallback((item: FormItem) => ({
placeholder: item.placeholder,
allowClear: clearable || item.clearable,
style: { width: item.width || 240 },
disabled: disabled || item.disabled,
'aria-label': item.label
}), [clearable, disabled]);
// 渲染表单项
const renderFormItem = useCallback((item: FormItem) => {
const commonProps = getCommonProps(item);
switch (item.type) {
case 'input':
return (
<Input
{...commonProps}
type={item.inputType || 'text'}
maxLength={item.maxLength}
/>
);
case 'select':
return (
<Select
{...commonProps}
mode={item.multiple ? 'multiple' : undefined}
maxTagCount={item.collapseTags ? 1 : undefined}
showSearch={item.filterable}
optionFilterProp="children"
onChange={(value) => handleSelectChange(value, item)}
notFoundContent={loading ? '加载中...' : '暂无数据'}
>
{item.options?.map((option, idx) => {
const value = option.value ?? option.itemValue ?? option.id;
const label = option.label ?? option.itemText;
return (
<Option key={`${value}-${idx}`} value={value}>
{label}
</Option>
);
})}
</Select>
);
case 'cascader':
return (
<Cascader
{...commonProps}
options={item.options || []}
fieldNames={item.props}
multiple={item.multiple}
showArrow
changeOnSelect={!item.showAllLevels}
maxTagCount={item.collapseTags ? 1 : undefined}
showSearch={item.filterable}
onChange={(value) => handleCascaderChange(value, item)}
notFoundContent={loading ? '加载中...' : '暂无数据'}
/>
);
case 'daterange':
return (
<RangePicker
{...commonProps}
format={item.format || 'YYYY-MM-DD'}
placeholder={item.placeholder ? [item.placeholder, item.placeholder] : ['开始时间', '结束时间']}
/>
);
case 'range':
return (
<Space>
<Input
{...commonProps}
style={{ width: item.width || 110 }}
type={item.inputType || 'text'}
min={item.min}
addonAfter={item.unit}
aria-label={`${item.label}最小值`}
/>
<span aria-hidden="true">-</span>
<Input
{...commonProps}
style={{ width: item.width || 110 }}
type={item.inputType || 'text'}
min={item.min}
addonAfter={item.unit}
aria-label={`${item.label}最大值`}
/>
</Space>
);
case 'slot':
const slot = Children.toArray(children).find(
(child): child is ReactElement =>
isValidElement(child) && child.props?.slot === `${item.prop}_slot`
);
return slot ? cloneElement(slot, {
data: form.getFieldsValue(),
disabled: disabled || item.disabled
}) : null;
default:
console.warn(`Unknown form item type: ${item.type}`);
return null;
}
}, [getCommonProps, loading, children, form, handleSelectChange, handleCascaderChange]);
// 表单配置
const formLayout: FormProps = useMemo(() => ({
layout: 'inline',
labelAlign: 'right',
labelWrap: true,
style: collapseStyle,
form,
initialValues: formData,
onValuesChange: handleValuesChange,
disabled: disabled || loading
}), [collapseStyle, form, formData, handleValuesChange, disabled, loading]);
// 是否显示折叠按钮
const shouldShowCollapseButton = useMemo(() =>
canCollapse && formList.length > defaultShow,
[canCollapse, formList.length, defaultShow]
);
// 渲染的表单项列表
const renderedFormItems = useMemo(() =>
formList.map((item, index) => {
if (!item.prop || !item.type) {
console.warn(`Form item at index ${index} missing required prop or type`);
return null;
}
return (
<Form.Item
key={`${item.prop}-${index}`}
label={`${item.label || ''}:`}
name={item.prop}
rules={item.rules}
labelCol={{ style: { width: computedLabelWidth } }}
>
{renderFormItem(item)}
</Form.Item>
);
}),
[formList, computedLabelWidth, renderFormItem]
);
return (
<div
className="search-form-container"
role="search"
aria-label="搜索表单"
>
<div className="search-form-layout">
<div className="form-content">
<Form {...formLayout}>
{renderedFormItems}
</Form>
</div>
<div className="form-actions">
<Space>
<Button
type="primary"
onClick={handleSearch}
loading={loading}
aria-label="搜索"
>
搜索
</Button>
<Button
onClick={handleReset}
disabled={loading}
aria-label="重置"
>
重置
</Button>
{shouldShowCollapseButton && (
<Button
type="link"
onClick={toggleCollapse}
icon={isCollapse ? <UpOutlined /> : <DownOutlined />}
aria-label={isCollapse ? '收起' : '展开'}
aria-expanded={isCollapse}
>
{isCollapse ? '收起' : '展开'}
</Button>
)}
</Space>
</div>
</div>
{children}
</div>
);
});
FormContainer.displayName = 'FormContainer';
export default FormContainer;
javascript
import { Rule } from 'antd/es/form';
import { ReactNode } from 'react';
export type FormValues = Record<string, unknown>;
export interface OptionItem {
label?: string;
value?: string | number;
itemText?: string;
itemValue?: string | number;
id?: string | number;
}
export interface FormItem {
label: string;
prop: string;
type: 'input' | 'select' | 'cascader' | 'daterange' | 'range' | 'slot';
placeholder?: string;
width?: string | number;
clearable?: boolean;
disabled?: boolean;
multiple?: boolean;
collapseTags?: boolean;
filterable?: boolean;
options?: OptionItem[];
props?: Record<string, string>;
showAllLevels?: boolean;
dateObj?: boolean;
time?: string;
format?: string;
start?: string;
end?: string;
unit?: string;
min?: number;
maxLength?: number;
inputType?: 'text' | 'number' | 'password' | 'email' | 'tel' | 'url';
formatter?: (value: string) => string;
rules?: Rule[];
}
export interface FormContainerProps {
formData?: FormValues;
formList: FormItem[];
canCollapse?: boolean;
labelWidth?: string;
clearable?: boolean;
horizontal?: boolean;
defaultShow?: number;
loading?: boolean;
disabled?: boolean;
onReset?: (form: FormValues) => void;
onSearch?: (form: FormValues) => void;
onSelectChange?: (item: FormItem, form: FormValues) => void;
onCascaderChange?: (item: FormItem, form: FormValues) => void;
onFormDataChange?: (form: FormValues) => void;
children?: ReactNode;
}
css
.search-form-container {
width: 100%;
border-bottom: 1px solid #ebeef5;
margin-bottom: 24px;
padding-bottom: 8px;
}
.search-form-layout {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.form-content {
flex: 1;
min-width: 0;
}
.form-content .ant-form-item {
display: inline-block;
margin-right: 16px;
margin-bottom: 16px;
vertical-align: top;
}
.form-actions {
flex-shrink: 0;
padding-top: 4px;
}
@media (max-width: 768px) {
.search-form-layout {
flex-direction: column;
align-items: stretch;
}
.form-content .ant-form-item {
display: block;
width: 100%;
margin-right: 0;
}
.form-actions {
align-self: flex-end;
}
}
总结
这个动态表单容器组件展示了如何构建一个高度可配置、可扩展的表单解决方案。通过合理的组件设计、状态管理和性能优化,它能够满足大多数复杂表单场景的需求。开发者可以根据实际业务需求进一步扩展其功能,如表单验证规则、动态表单项、异步数据加载等。
这种组件化思维不仅提高了代码的复用性,也使得表单的维护和迭代变得更加简单高效。
希望这篇技术博客对您理解和实现高级表单组件有所帮助。如果您有任何问题或建议,欢迎在评论区留言讨论。