React表單容器的通用解決方案
1. 前話(huà)
提問(wèn):ToB中臺(tái)類(lèi)系統(tǒng)的后端開(kāi)發(fā)主要做啥?
??♂?:CRUD
再次提問(wèn):那前端開(kāi)發(fā)呢?
??♂?:增刪查改
開(kāi)個(gè)玩笑哈啊哈哈哈??????
雖然沒(méi)有具體數(shù)據(jù)統(tǒng)計(jì),但作者仍主觀地認(rèn)為中臺(tái)類(lèi)的系統(tǒng)的前端內(nèi)容至少一半都是增刪查改??????,其對(duì)應(yīng)的前端頁(yè)面類(lèi)型就是列表頁(yè)面和表單頁(yè)面。
對(duì)于列表頁(yè)面的通用實(shí)現(xiàn),如果讀者有看過(guò)《React通用解決方案——組件數(shù)據(jù)請(qǐng)求》一文應(yīng)該會(huì)根據(jù)自身實(shí)際業(yè)務(wù)場(chǎng)景得出較好的解決方案,提升實(shí)際業(yè)務(wù)的列表頁(yè)面的開(kāi)發(fā)效率。
而對(duì)于表單頁(yè)面,又該如何實(shí)現(xiàn)以作為通用開(kāi)發(fā)模版進(jìn)行提效?大致有兩種:
- 一種「配置表單」,也就是定義表單DSL,通過(guò)JSON配置生成表單頁(yè)面,這也是業(yè)界低代碼平臺(tái)的表單實(shí)現(xiàn)。優(yōu)點(diǎn)是顯而易見(jiàn)的,配置簡(jiǎn)單,快速實(shí)現(xiàn)。缺點(diǎn)是靈活性受限于DSL的完整性,對(duì)于特殊場(chǎng)景需進(jìn)行表單組件底層實(shí)現(xiàn)的定制化。
- 另一種是「原生表單」,也就是直接使用表單組件。其優(yōu)缺陷大致與「配置表單」相反。
本篇由于主題定義就不講解表單的通用實(shí)現(xiàn)只分享表單的通用呈現(xiàn)哈??♂???♂???♂?下面開(kāi)始正文。
2. 正文
常見(jiàn)的表單的呈現(xiàn)有兩種模式,分別是頁(yè)面和浮層
。
首先是「頁(yè)面表單」,也就是以頁(yè)面的形式呈現(xiàn)表單。示例代碼如下:
const FormPage: React.FC = () => { const [form] = useForm(); const handleSubmit = useCallback((value) => { // TODO 表單提交邏輯 console.log(value); }, []); return ( <div className="test-page"> <h2>新建用戶(hù)</h2> <Form form={form} onSubmit={handleSubmit} layout="inline"> <Form.Item field="name" label="名稱(chēng)" rules={[ { required: true, message: "請(qǐng)輸入名稱(chēng)", }, ]} > <Input placeholder="請(qǐng)輸入" /> </Form.Item> <Form.Item> <Button htmlType="submit">提交</Button> </Form.Item> </Form> </div> ); };
瀏覽器展現(xiàn)如下:
某一天,產(chǎn)品為了優(yōu)化交互體驗(yàn)改成「以彈窗呈現(xiàn)表單」,這時(shí)便會(huì)用到表單的另一種呈現(xiàn)——「浮層表單」。在原「頁(yè)面表單」的實(shí)現(xiàn)中進(jìn)行修改,修改后的示例代碼如下:
const FormPage: React.FC = () => { const [form] = useForm(); const visible = useBoolean(false); const handleSubmit = useCallback(() => { form.validate((error, value) => { if (error) { return; } // TODO 表單提交邏輯 console.log(value); visible.setFalse(); }); }, []); return ( <div className="test-page"> <h2>新建用戶(hù)</h2> <Button onClick={visible.setTrue}>點(diǎn)擊新建</Button> <Modal visible={visible.state} title="新建用戶(hù)" okText="提交" onOk={handleSubmit} onCancel={visible.setFalse} > <Form form={form} layout="inline"> <Form.Item field="name" label="名稱(chēng)" rules={[ { required: true, message: "請(qǐng)輸入名稱(chēng)", }, ]} > <Input placeholder="請(qǐng)輸入" /> </Form.Item> </Form> </Modal> </div> ); };
瀏覽器展現(xiàn)如下:
某一天,產(chǎn)品提了個(gè)新需求,另一個(gè)「用戶(hù)新建頁(yè)面表單」。某一天,產(chǎn)品提了個(gè)新需求,另一個(gè)「用戶(hù)新建彈窗表單」。某一天,產(chǎn)品提了個(gè)新需求,另一個(gè)「用戶(hù)新建抽屜表單」。某一天。。。
這時(shí)RD糾結(jié)了,為了快速完成需求直接是拷貝一個(gè)新的「FormPage」組件完成交付最終的結(jié)局肯定就是「禿頭」,亟需總結(jié)個(gè)通用的解決方案應(yīng)對(duì)表單不同呈現(xiàn)的場(chǎng)景的實(shí)現(xiàn)。
切入點(diǎn)是對(duì)表單和呈現(xiàn)進(jìn)行拆分,避免表單和呈現(xiàn)的耦合
。
那該如何拆分?我們先明確下表單和呈現(xiàn)各自的關(guān)注點(diǎn),表單主要關(guān)注表單值和表單動(dòng)作,而呈現(xiàn)主要關(guān)注自身的樣式
。如果表單的動(dòng)作需要呈現(xiàn)進(jìn)行觸發(fā),例如彈窗的確定按鈕觸發(fā)表單的提交動(dòng)作呢?這就需要表單與呈現(xiàn)之間需存在連接的橋梁
。
作者根據(jù)這個(gè)思路最終拆分的結(jié)果是,實(shí)現(xiàn)了個(gè)「表單容器」。「表單」+「表單容器」,讓表單的實(shí)現(xiàn)不關(guān)注呈現(xiàn),從而實(shí)現(xiàn)表單的復(fù)用,提升了開(kāi)發(fā)效率。
2.1 表單容器定義
表單容器的定義基于浮層容器拓展,定義如下:
- 表單容器支持各種呈現(xiàn)(彈窗和抽屜等);
- 表單容器只關(guān)注浮層的標(biāo)題、顯隱狀態(tài)和顯隱狀態(tài)變更處理邏輯,不關(guān)注浮層內(nèi)容;
- 表單容器組件提供接口控制浮層容器的標(biāo)題和顯隱狀態(tài);
- 任何內(nèi)容被表單容器包裹即可獲得浮層的能力;
- 表單容器提供向浮層內(nèi)容透?jìng)鲗傩缘哪芰?,?nèi)置透?jìng)鱂orm實(shí)例、表單模式和只讀狀態(tài)的屬性;
- 表單容器的浮層確認(rèn)邏輯自動(dòng)觸發(fā)Form實(shí)例的提交邏輯
基于上面的定義實(shí)現(xiàn)的TS類(lèi)型定義如下:
import React from "react"; import { ModalProps, DrawerProps, FormInstance } from "@arco-design/web-react"; import { EFormMode, IBaseFormProps } from "@/hooks/use-common-form"; export type IFormWrapperBaseProps = { /** 標(biāo)題 */ title?: React.ReactNode; }; export type IFormWrapperOpenProps<T = any, P = {}> = IFormWrapperBaseProps & { /** 表單模式 */ mode?: EFormMode; /** 表單值 */ value?: T; /** 內(nèi)容屬性 */ props?: P; }; export type IFormWrapperProps<T = any, P = {}> = IFormWrapperBaseProps & { /** 表單彈窗提交回調(diào)函數(shù) */ onSubmit?: ( /** 提交表單值 */ formValue: T, /** 當(dāng)前表單值 */ currentValue: T, /** 表單模式 */ formMode: EFormMode, /** 內(nèi)容屬性 */ componentProps?: P ) => Promise<void>; /** 表單彈窗提交回調(diào)函數(shù) */ onOk?: (result: any, componentProps?: P) => void | Promise<void>; /** 表單彈窗提交回調(diào)函數(shù) */ onCancel?: () => void; /** 內(nèi)容屬性 */ componentProps?: P; }; export type IFormWrappedModalProps<T = any, P = {}> = Omit< ModalProps, "onOk" | "onCancel" > & IFormWrapperProps<T, P>; export type IFormWrappedDrawerProps<T = any, P = {}> = Omit< DrawerProps, "onOk" | "onCancel" > & IFormWrapperProps<T, P> & { operation?: React.ReactNode; }; export type IFormWrapperRef<T = any, P = {}> = { /** 表單彈窗打開(kāi)接口 */ open: (openProps?: IFormWrapperOpenProps<T, P>) => void; /** 表單彈窗關(guān)閉接口 */ close: () => void; }; export type IWithFormWrapperOptions<T = any, P = {}> = { /** 默認(rèn)值 */ defaultValue: T; /** 默認(rèn)屬性 */ defaultProps?: Partial<IFormWrapperProps<T, P>>; }; export type IWithFormWrapperProps<T = any, P = {}> = IBaseFormProps & { /** 表單實(shí)例 */ form: FormInstance<T>; } & P;
2.2 表單容器定義實(shí)現(xiàn)
基于上面的表單容器定義,我們這里實(shí)現(xiàn)一個(gè)Hook,實(shí)現(xiàn)代碼如下:
/** * 表單容器Hook * @param ref 浮層實(shí)例 * @param wrapperProps 浮層屬性 * @param defaultValue 默認(rèn)值 * @returns */ export function useFormWrapper<T = any, P = {}>( ref: ForwardedRef<IFormWrapperRef<T, P>>, wrapperProps: IFormWrapperProps<T, P>, defaultValue: T, ) { const [form] = Form.useForm(); const visible = useBoolean(false); const loading = useBoolean(false); const [title, setTitle] = useState<React.ReactNode>(); const [componentProps, setComponentProps] = useState<P>(); const [value, setValue] = useState(defaultValue); const [mode, setMode] = useState(EFormMode.view); // 計(jì)算是否只讀 const readOnly = useReadOnly(mode); // 提交處理邏輯 const onOk = async () => { loading.setTrue(); const targetComponentProps = wrapperProps.componentProps ?? componentProps; try { // 校驗(yàn)表單 const formValue = await form.validate(); // 提交表單 const result = await wrapperProps?.onSubmit?.( formValue, value, mode, targetComponentProps, ); await wrapperProps.onOk?.(result, targetComponentProps); visible.setFalse(); } catch (err) { console.error(err); } finally { loading.setFalse(); } }; // 取消處理邏輯 const onCancel = () => { wrapperProps.onCancel?.(); visible.setFalse(); }; // 實(shí)例掛載表單操作接口 useImperativeHandle( ref, (): IFormWrapperRef<T, P> => ({ open: openProps => { const { title: newTitle, mode: newMode = EFormMode.view, value: newValue = defaultValue, } = openProps ?? {}; setMode(newMode); setTitle(newTitle); setValue(newValue); form.resetFields(); form.setFieldsValue(newValue); visible.setTrue(); }, close: onCancel, }), ); // 初始化表單默認(rèn)值 useEffect(() => { form.setFieldsValue(defaultValue); }, []); const ret = [ { visible, loading, title, componentProps, form, value, mode, readOnly, }, { onOk, onCancel, setTitle, setComponentProps, setValue, setMode, }, ] as const; return ret; }
2.3 表單容器呈現(xiàn)實(shí)現(xiàn)
表單容器的呈現(xiàn)有多種,常見(jiàn)的為彈窗和抽屜。下面我使用Arco對(duì)應(yīng)組件進(jìn)行呈現(xiàn)實(shí)現(xiàn) ?? 。
2.3.1 彈窗表單容器
/** * 表單彈窗容器 * @param options 表單配置 * @returns */ function withModal<T = any, P = {}>(options: IWithFormWrapperOptions<T, P>) { const { defaultValue, defaultProps } = options; return function (Component: any) { const WrappedComponent = ( props: IFormWrappedModalProps<T, P>, ref: ForwardedRef<IFormWrapperRef<T, P>>, ) => { const wrapperProps = { ...defaultProps, ...props, }; const { componentProps, title, visible, okButtonProps, cancelButtonProps, okText = 'Submit', cancelText = 'Cancel', maskClosable = false, unmountOnExit = true, ...restProps } = wrapperProps; const [ { form, mode, readOnly, visible: currentVisible, title: currentTitle, componentProps: currentComponentProps, }, { onOk, onCancel }, ] = useFormWrapper<T, P>(ref, wrapperProps, defaultValue); return ( <Modal {...restProps} maskClosable={maskClosable} visible={visible ?? currentVisible.state} onOk={onOk} okText={okText} okButtonProps={{ hidden: readOnly, ...okButtonProps, }} onCancel={onCancel} cancelText={cancelText} cancelButtonProps={{ hidden: readOnly, ...cancelButtonProps, }} title={title ?? currentTitle} unmountOnExit={unmountOnExit}> {React.createElement(Component, { form, mode, readOnly, ...(componentProps ?? currentComponentProps), })} </Modal> ); }; WrappedComponent.displayName = `FormWrapper.withModal(${getDisplayName( Component, )})`; const ForwardedComponent = forwardRef< IFormWrapperRef<T, P>, IFormWrappedModalProps<T, P> >(WrappedComponent); return ForwardedComponent; }; }
2.3.1 抽屜表單容器
/** * 表單抽屜容器 * @param options 表單配置 * @returns */ function withDrawer<T = any, P = {}>(options: IWithFormWrapperOptions<T, P>) { const { defaultValue, defaultProps } = options; return function (Component: any) { const WrappedComponent = ( props: IFormWrappedDrawerProps<T, P>, ref: ForwardedRef<IFormWrapperRef<T, P>>, ) => { const wrapperProps = { ...defaultProps, ...props, }; const { title, visible, componentProps, okText = 'Submit', okButtonProps, cancelText = 'Cancel', cancelButtonProps, maskClosable = false, unmountOnExit = true, operation, ...restProps } = wrapperProps; const [ { form, mode, readOnly, loading, visible: currentVisible, title: currentTitle, componentProps: currentComponentProps, }, { onOk, onCancel }, ] = useFormWrapper<T, P>(ref, wrapperProps, defaultValue); const footerNode = useMemo( () => ( <div style={{ textAlign: 'right' }}> {operation} {!readOnly && ( <> <Button type="default" onClick={onCancel} {...cancelButtonProps}> {cancelText} </Button> <Button type="primary" loading={loading.state} onClick={onOk} style={{ marginLeft: '8px' }} {...okButtonProps}> {okText} </Button> </> )} </div> ), [ loading.state, onOk, onCancel, okText, cancelText, readOnly, okButtonProps, cancelButtonProps, ], ); const showFooter = useMemo( () => !(readOnly && !operation), [readOnly, operation], ); return ( <Drawer {...restProps} maskClosable={maskClosable} visible={visible ?? currentVisible.state} title={title ?? currentTitle} footer={showFooter ? footerNode : null} unmountOnExit={unmountOnExit} onCancel={onCancel}> {React.createElement(Component, { form, mode, readOnly, ...(componentProps ?? currentComponentProps), })} </Drawer> ); }; WrappedComponent.displayName = `FormWrapper.withDrawer(${getDisplayName( Component, )})`; const ForwardedComponent = forwardRef< IFormWrapperRef<T, P>, IFormWrappedDrawerProps<T, P> >(WrappedComponent); return ForwardedComponent; }; }
2.4 表單容器用例
對(duì)于上面的代碼示例我們進(jìn)行以下改造,將頁(yè)面的表單抽離成單獨(dú)的表單組件,代碼如下:
type IUserFormValue = { name?: string; }; const UserForm: React.FC<IWithFormWrapperProps<IUserFormValue>> = ({ form, }) => { return ( <Form form={form} layout="inline"> <Form.Item field="name" label="名稱(chēng)" rules={[ { required: true, message: "請(qǐng)輸入名稱(chēng)", }, ]} > <Input placeholder="請(qǐng)輸入" /> </Form.Item> </Form> ); };
下面我們就可以使用上面實(shí)現(xiàn)的表單容器進(jìn)行包裹生成彈窗表單組件,代碼如下:
const submitForm = async (formValue: IUserFormValue) => { // TODO 表單提交邏輯 console.log(formValue); }; const UserFormModal = FormWrapper.withModal<IUserFormValue>({ defaultValue: { name: "", }, defaultProps: { onSubmit: submitForm, }, })(UserForm);
在實(shí)際業(yè)務(wù)場(chǎng)景中,彈窗表單和頁(yè)面表單都能復(fù)用一個(gè)表單組件,代碼如下:
const FormPage: React.FC = () => { const [form] = useForm<IUserFormValue>(); const userFormRef = useRef<IFormWrapperRef<IUserFormValue>>(null); const handleSubmit = useCallback(() => { form.validate((error, formValue) => { if (error || !formValue) { return; } submitForm(formValue); }); }, []); return ( <div className="test-page"> <h2>新建用戶(hù)</h2> {/* 頁(yè)面表單 */} <UserForm form={form} /> <Button onClick={handleSubmit}>頁(yè)面新建</Button> {/* 彈窗表單 */} <UserFormModal ref={userFormRef} /> <Button onClick={() => { userFormRef.current?.open({ title: "新建用戶(hù)", mode: EFormMode.add, value: { name: "", }, }); }} > 彈窗新建 </Button> </div> ); };
3. 最后
表單容器的基于浮層容器進(jìn)行實(shí)現(xiàn),作者在實(shí)際業(yè)務(wù)開(kāi)發(fā)過(guò)程中也廣泛應(yīng)用到了這兩類(lèi)容器,本篇也只是對(duì)簡(jiǎn)單表單場(chǎng)景進(jìn)行實(shí)現(xiàn),更為復(fù)雜的表單場(chǎng)景可以在評(píng)論區(qū)交流哈。
到此這篇關(guān)于React表單容器的通用解決方案的文章就介紹到這了,更多相關(guān)React表單容器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- React如何利用Antd的Form組件實(shí)現(xiàn)表單功能詳解
- react使用antd的上傳組件實(shí)現(xiàn)文件表單一起提交功能(完整代碼)
- react?表單數(shù)據(jù)形式配置化設(shè)計(jì)
- react實(shí)現(xiàn)動(dòng)態(tài)表單
- React事件處理和表單的綁定詳解
- React?Hook?Form?優(yōu)雅處理表單使用指南
- react表單受控的實(shí)現(xiàn)方案
- React實(shí)現(xiàn)表單提交防抖功能的示例代碼
- React中重新實(shí)現(xiàn)強(qiáng)制實(shí)施表單的流程步驟
- react實(shí)現(xiàn)動(dòng)態(tài)增減表單項(xiàng)的示例代碼
- React 實(shí)現(xiàn)表單組件的示例代碼
相關(guān)文章
react?hooks深拷貝后無(wú)法保留視圖狀態(tài)解決方法
這篇文章主要為大家介紹了react?hooks深拷貝后無(wú)法保留視圖狀態(tài)解決示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06React簡(jiǎn)便獲取經(jīng)緯度信息的方法詳解
在現(xiàn)代的Web應(yīng)用程序中,獲取用戶(hù)的地理位置信息是一項(xiàng)常見(jiàn)的需求,本文我們將介紹如何在React應(yīng)用程序中簡(jiǎn)便地獲取用戶(hù)的經(jīng)緯度信息,需要的可以參考下2023-11-11React?createRef循環(huán)動(dòng)態(tài)賦值ref問(wèn)題
這篇文章主要介紹了React?createRef循環(huán)動(dòng)態(tài)賦值ref問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01React報(bào)錯(cuò)Too many re-renders解決
這篇文章主要為大家介紹了React報(bào)錯(cuò)Too many re-renders解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12