概述
在現代前端應用中,表單是用戶交互的核心部分。本文將深入分析一個基于 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;}
}
總結
這個動態表單容器組件展示了如何構建一個高度可配置、可擴展的表單解決方案。通過合理的組件設計、狀態管理和性能優化,它能夠滿足大多數復雜表單場景的需求。開發者可以根據實際業務需求進一步擴展其功能,如表單驗證規則、動態表單項、異步數據加載等。
這種組件化思維不僅提高了代碼的復用性,也使得表單的維護和迭代變得更加簡單高效。
希望這篇技術博客對您理解和實現高級表單組件有所幫助。如果您有任何問題或建議,歡迎在評論區留言討論。