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
英文地址,請注明出處:http://en.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;可靠且高效的控制解決方案成為企業提…