React + Antd+TS 动态表单容器组件技术解析与实现

概述

在现代前端应用中,表单是用户交互的核心部分。本文将深入分析一个基于 React 和 Ant Design 的高级动态表单容器组件,它提供了强大的可配置性、灵活的布局选项和丰富的功能扩展能力。

组件核心特性

1. 高度可配置的表单结构

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)

实现细节解析

智能标签宽度计算

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 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]);

折叠功能实现

// 折叠状态管理
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);
}, []);

表单实例暴露与回调处理

// 暴露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]);

使用示例

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 (<FormContainerref={formRef}formList={formList}onSearch={handleSearch}onReset={handleReset}/>);
};export default ExampleComponent;

组件完整代码实现

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}showArrowchangeOnSelect={!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.Itemkey={`${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;
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;
}
.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;}
}

总结

这个动态表单容器组件展示了如何构建一个高度可配置、可扩展的表单解决方案。通过合理的组件设计、状态管理和性能优化,它能够满足大多数复杂表单场景的需求。开发者可以根据实际业务需求进一步扩展其功能,如表单验证规则、动态表单项、异步数据加载等。

这种组件化思维不仅提高了代码的复用性,也使得表单的维护和迭代变得更加简单高效。


希望这篇技术博客对您理解和实现高级表单组件有所帮助。如果您有任何问题或建议,欢迎在评论区留言讨论。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/94098.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/94098.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

51c自动驾驶~合集16

自己的原文哦~ https://blog.51cto.com/whaosoft/11739891 #CLIP系列模型如何补短板再升级 CLIP&#xff08;Contrastive Language–Image Pre-training&#xff09;模型自推出以来&#xff0c;在图像-文本跨模态理解和生成领域取得了显著成果。然而&#xff0c;经…

分级设色地图/标注式统计地图-中国地图绘制

分级设色地图/标注式统计地图‌1. 这种图长什么样&#xff1f;‌2. 核心应用场景‌3. 工具3.1 自己找数据3.2 智图小易司3.2 Flourish3.3 镝数图表注意事项当你看到一张中国地图&#xff0c;各省份颜色深浅不一&#xff0c;旁边还标注着具体数值时&#xff0c;这种图就是‌分级…

2025最新华为云国际版注册图文流程-不用绑定海外信用卡注册

说到华为云&#xff0c;很多人第一反应就是“大厂可靠、服务全”。确实&#xff0c;作为全球知名的云计算服务商&#xff0c;华为云在企业级项目和个人开发者中都挺受欢迎。今天我就带你一步一步走一遍华为云国际版的注册流程&#xff0c;让新手也能轻松上手。下面是最简单的注…

Android 人脸识别技术全解析

人脸识别作为生物识别技术的核心分支&#xff0c;已广泛应用于考勤打卡、身份验证、支付安全等场景。在 Android 平台&#xff0c;实现人脸识别需要兼顾准确性、实时性和设备兼容性三大挑战。本文将系统讲解 Android 人脸识别的技术选型、核心实现、性能优化及安全加固&#xf…

STM32项目分享:基于STM32单片机驾驶安全监测系统设计

“我们不做一锤子买卖&#xff0c;只做技术成长的长期伙伴&#xff01;” 目录 一、视频展示 二、项目简介 三、原理图设计 四、PCB硬件设计 五、程序设计 六、资料分享 一、视频展示 基于stm32单片机驾驶行为监测系统设计 -视频分享二、项目简介 题目&#xff1a;基于s…

【GaussDB】使用gdb定位GaussDB编译package报错

【GaussDB】使用gdb定位GaussDB编译package报错 背景 在某次迁移Oracle到GaussDB时&#xff0c;应用开发人员将改好的package在GaussDB里进行创建&#xff0c;没有ERROR也没有WARNING&#xff0c;但是编译无效对象的时候报错了。虽然已经找到了是哪个包编译报错&#xff0c;但…

One Commander:强大的Windows文件管理器

在日常使用电脑的过程中&#xff0c;文件管理和浏览是必不可少的任务。One Commander作为一款功能强大的Windows文件管理器&#xff0c;提供了丰富的功能和便捷的操作方式&#xff0c;帮助用户更高效地管理和浏览文件。它不仅支持多种文件操作&#xff0c;还提供了丰富的自定义…

SPUpDate Application 程序卸载

我安装了 EzvizStudioSetups.exe 软件&#xff0c;卸载后会在电脑遗留 SPUpDate Application 程序&#xff1b;在某一时刻会占用 CPU 资源&#xff1b;应用卸载方法一&#xff1a;在任务管理器搜索 SPUpDate Application&#xff1b;定位到文件位置&#xff1b;我的路径如下C:\…

算法题(187):程序自动分析

审题&#xff1a; 本题需要我们判断是否可以同时满足题目给定的若干等式或不等式&#xff0c;判断出后根据结果输出YES或NO 思路&#xff1a; 方法一&#xff1a;离散化并查集 使用并查集&#xff1a;其实题目中只存在两者相等或不等两种情况&#xff0c;而等于具有传递性&…

strcasecmp函数详解

strcasecmp 是 C 语言中用于不区分大小写比较两个字符串的函数&#xff0c;主要用于忽略字符大小写差异的场景&#xff08;如用户输入验证、不区分大小写的字符串匹配等&#xff09;。它属于 POSIX 标准库&#xff0c;定义在 <string.h> 头文件中。 一、函数原型与参数 函…

Voronoi图

本文将详细解释 Voronoi 图&#xff0c;它在空间分析和插值中非常常用。1. 概念 Voronoi 图是一种空间划分方法&#xff0c;它把平面&#xff08;或空间&#xff09;划分成若干个区域&#xff0c;使得每个区域内的任意一点都比该区域外的任何一点更靠近该区域的“生成点”&…

BioScientist Agent:用于药物重定位和作用机制解析的知识图谱增强型 LLM 生物医学代理技术报告

BioScientist Agent:用于药物重定位和作用机制解析的知识图谱增强型 LLM 生物医学代理技术报告 一、项目概述 药物研发是一个周期长、成本高的过程,平均需要超过 10 年时间和 20 亿美元才能将一种新药推向市场,且 90% 以上的候选药物最终失败(1)。这种低成功率主要归因于对…

5G视频终端详解 无人机图传 无线图传 便携式5G单兵图传

前言单兵图传设备&#xff0c;是一种集视频采集、编码压缩、无线传输等多种功能于一体的便携式通信终端。它以嵌入式系统为基础&#xff0c;搭载高性能 H.265 编解码处理器&#xff0c;能够将现场的音视频信息进行高效处理后&#xff0c;通过无线网络快速稳定地传输至后端指挥中…

【苹果软件】Prism Mac 9.4苹果系统免费安装包英文版 Graphpad Prism for Mac 9.4软件免费下载与详细图文教程!!

软件下载与系统要求 软件&#xff1a;Prism9.4 语言&#xff1a;英文 大小&#xff1a;103.41M 安装环境&#xff1a;MacOS12.0&#xff08;或更高&#xff0c;支持IntelM芯片&#xff09; MacOS苹果系统GraphPad Prism&#xff08;科学数据分析与图形绘制&#xff09;&am…

Redis 奇葩问题

先贴错误码Unexpected exception while processing command这个奇葩的问题查了很久&#xff0c;后面突然顿悟&#xff0c;应该是Redis记住了第一次的数据类型&#xff0c;后面即使换了数据类型也不会改变之前的数据类型。跟代码发现是codec变成了默认的了后续public RedissonBa…

C ++代码学习笔记(一)

1、GetStringUTFChars用于将 Java 字符串&#xff08;jstring&#xff09;转换为 UTF-8 编码的 C 风格字符串&#xff08;const char*&#xff09;。必须在使用完后调用 ReleaseStringUTFChars 释放内存&#xff0c;否则可能导致内存泄漏。std::string data_converter::convert…

【学习嵌入式day-29-网络】

进程和线程的区别&#xff1a;都是系统执行的任务进程是资源分配的基本单位线程是调度执行的最小单位进程的创建和切换的开销大&#xff0c;速度慢&#xff0c;效率低空间独立、----- 安全&#xff0c;稳定进程间通信不方便线程创建和切换的开销小&#xff0c;速度快&#xff0…

Eino 框架组件协作指南 - 以“智能图书馆建设手册”方式理解

Eino 框架组件关系 - 形象比喻指南 &#x1f3d7;️ 项目概览&#xff1a;构建一个智能图书馆 想象一下&#xff0c;你要建设一个现代化的智能图书馆&#xff0c;能够帮助用户快速找到所需信息并提供智能问答服务。Eino 框架就像是这个智能图书馆的建设工具包&#xff0c;每个组…

网络打印机自动化部署脚本

下面是一个全面的、交互式的PowerShell脚本&#xff0c;用于自动化网络打印机部署过程。这个脚本提供了图形化界面&#xff0c;让用户可以轻松地搜索、选择和安装网络打印机。 备注&#xff1a;这个脚本未在生产环境测试过&#xff0c;请大家测试一下&#xff0c;有问题或优化&…

探索工业自动化核心:ZMC 系列 EtherCAT 主站控制器

ZLG致远电子的ZMC系列EtherCAT主站控制器&#xff0c;凭借多元内核、丰富接口、卓越通信能力及开放开发环境&#xff0c;为工业自动化提供全方位解决方案&#xff0c;助力企业智能化升级。 前言在工业自动化领域不断演进的今天&#xff0c;可靠且高效的控制解决方案成为企业提…