React表單容器的通用解決方案
1. 前話
提問:ToB中臺類系統(tǒng)的后端開發(fā)主要做啥?
??♂?:CRUD
再次提問:那前端開發(fā)呢?
??♂?:增刪查改
開個玩笑哈啊哈哈哈??????
雖然沒有具體數(shù)據(jù)統(tǒng)計,但作者仍主觀地認(rèn)為中臺類的系統(tǒng)的前端內(nèi)容至少一半都是增刪查改??????,其對應(yīng)的前端頁面類型就是列表頁面和表單頁面。
對于列表頁面的通用實現(xiàn),如果讀者有看過《React通用解決方案——組件數(shù)據(jù)請求》一文應(yīng)該會根據(jù)自身實際業(yè)務(wù)場景得出較好的解決方案,提升實際業(yè)務(wù)的列表頁面的開發(fā)效率。
而對于表單頁面,又該如何實現(xiàn)以作為通用開發(fā)模版進(jìn)行提效?大致有兩種:
- 一種「配置表單」,也就是定義表單DSL,通過JSON配置生成表單頁面,這也是業(yè)界低代碼平臺的表單實現(xiàn)。優(yōu)點是顯而易見的,配置簡單,快速實現(xiàn)。缺點是靈活性受限于DSL的完整性,對于特殊場景需進(jìn)行表單組件底層實現(xiàn)的定制化。
- 另一種是「原生表單」,也就是直接使用表單組件。其優(yōu)缺陷大致與「配置表單」相反。
本篇由于主題定義就不講解表單的通用實現(xiàn)只分享表單的通用呈現(xiàn)哈??♂???♂???♂?下面開始正文。
2. 正文
常見的表單的呈現(xiàn)有兩種模式,分別是頁面和浮層。
首先是「頁面表單」,也就是以頁面的形式呈現(xiàn)表單。示例代碼如下:
const FormPage: React.FC = () => {
const [form] = useForm();
const handleSubmit = useCallback((value) => {
// TODO 表單提交邏輯
console.log(value);
}, []);
return (
<div className="test-page">
<h2>新建用戶</h2>
<Form form={form} onSubmit={handleSubmit} layout="inline">
<Form.Item
field="name"
label="名稱"
rules={[
{
required: true,
message: "請輸入名稱",
},
]}
>
<Input placeholder="請輸入" />
</Form.Item>
<Form.Item>
<Button htmlType="submit">提交</Button>
</Form.Item>
</Form>
</div>
);
};瀏覽器展現(xiàn)如下:

某一天,產(chǎn)品為了優(yōu)化交互體驗改成「以彈窗呈現(xiàn)表單」,這時便會用到表單的另一種呈現(xiàn)——「浮層表單」。在原「頁面表單」的實現(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>新建用戶</h2>
<Button onClick={visible.setTrue}>點擊新建</Button>
<Modal
visible={visible.state}
title="新建用戶"
okText="提交"
onOk={handleSubmit}
onCancel={visible.setFalse}
>
<Form form={form} layout="inline">
<Form.Item
field="name"
label="名稱"
rules={[
{
required: true,
message: "請輸入名稱",
},
]}
>
<Input placeholder="請輸入" />
</Form.Item>
</Form>
</Modal>
</div>
);
};瀏覽器展現(xiàn)如下:

某一天,產(chǎn)品提了個新需求,另一個「用戶新建頁面表單」。某一天,產(chǎn)品提了個新需求,另一個「用戶新建彈窗表單」。某一天,產(chǎn)品提了個新需求,另一個「用戶新建抽屜表單」。某一天。。。
這時RD糾結(jié)了,為了快速完成需求直接是拷貝一個新的「FormPage」組件完成交付最終的結(jié)局肯定就是「禿頭」,亟需總結(jié)個通用的解決方案應(yīng)對表單不同呈現(xiàn)的場景的實現(xiàn)。
切入點是對表單和呈現(xiàn)進(jìn)行拆分,避免表單和呈現(xiàn)的耦合。
那該如何拆分?我們先明確下表單和呈現(xiàn)各自的關(guān)注點,表單主要關(guān)注表單值和表單動作,而呈現(xiàn)主要關(guān)注自身的樣式。如果表單的動作需要呈現(xiàn)進(jìn)行觸發(fā),例如彈窗的確定按鈕觸發(fā)表單的提交動作呢?這就需要表單與呈現(xiàn)之間需存在連接的橋梁。
作者根據(jù)這個思路最終拆分的結(jié)果是,實現(xiàn)了個「表單容器」?!副韱巍?「表單容器」,讓表單的實現(xiàn)不關(guān)注呈現(xiàn),從而實現(xiàn)表單的復(fù)用,提升了開發(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)容透傳屬性的能力,內(nèi)置透傳Form實例、表單模式和只讀狀態(tài)的屬性;
- 表單容器的浮層確認(rèn)邏輯自動觸發(fā)Form實例的提交邏輯
基于上面的定義實現(xiàn)的TS類型定義如下:
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 = {}> = {
/** 表單彈窗打開接口 */
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 & {
/** 表單實例 */
form: FormInstance<T>;
} & P;2.2 表單容器定義實現(xiàn)
基于上面的表單容器定義,我們這里實現(xiàn)一個Hook,實現(xiàn)代碼如下:
/**
* 表單容器Hook
* @param ref 浮層實例
* @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);
// 計算是否只讀
const readOnly = useReadOnly(mode);
// 提交處理邏輯
const onOk = async () => {
loading.setTrue();
const targetComponentProps = wrapperProps.componentProps ?? componentProps;
try {
// 校驗表單
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();
};
// 實例掛載表單操作接口
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)實現(xiàn)
表單容器的呈現(xiàn)有多種,常見的為彈窗和抽屜。下面我使用Arco對應(yīng)組件進(jìn)行呈現(xiàn)實現(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 表單容器用例
對于上面的代碼示例我們進(jìn)行以下改造,將頁面的表單抽離成單獨的表單組件,代碼如下:
type IUserFormValue = {
name?: string;
};
const UserForm: React.FC<IWithFormWrapperProps<IUserFormValue>> = ({
form,
}) => {
return (
<Form form={form} layout="inline">
<Form.Item
field="name"
label="名稱"
rules={[
{
required: true,
message: "請輸入名稱",
},
]}
>
<Input placeholder="請輸入" />
</Form.Item>
</Form>
);
};下面我們就可以使用上面實現(xiàn)的表單容器進(jìn)行包裹生成彈窗表單組件,代碼如下:
const submitForm = async (formValue: IUserFormValue) => {
// TODO 表單提交邏輯
console.log(formValue);
};
const UserFormModal = FormWrapper.withModal<IUserFormValue>({
defaultValue: {
name: "",
},
defaultProps: {
onSubmit: submitForm,
},
})(UserForm);在實際業(yè)務(wù)場景中,彈窗表單和頁面表單都能復(fù)用一個表單組件,代碼如下:
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>新建用戶</h2>
{/* 頁面表單 */}
<UserForm form={form} />
<Button onClick={handleSubmit}>頁面新建</Button>
{/* 彈窗表單 */}
<UserFormModal ref={userFormRef} />
<Button
onClick={() => {
userFormRef.current?.open({
title: "新建用戶",
mode: EFormMode.add,
value: {
name: "",
},
});
}}
>
彈窗新建
</Button>
</div>
);
};3. 最后
表單容器的基于浮層容器進(jìn)行實現(xiàn),作者在實際業(yè)務(wù)開發(fā)過程中也廣泛應(yīng)用到了這兩類容器,本篇也只是對簡單表單場景進(jìn)行實現(xiàn),更為復(fù)雜的表單場景可以在評論區(qū)交流哈。
到此這篇關(guān)于React表單容器的通用解決方案的文章就介紹到這了,更多相關(guān)React表單容器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- React如何利用Antd的Form組件實現(xiàn)表單功能詳解
- react使用antd的上傳組件實現(xiàn)文件表單一起提交功能(完整代碼)
- react?表單數(shù)據(jù)形式配置化設(shè)計
- react實現(xiàn)動態(tài)表單
- React事件處理和表單的綁定詳解
- React?Hook?Form?優(yōu)雅處理表單使用指南
- react表單受控的實現(xiàn)方案
- React實現(xiàn)表單提交防抖功能的示例代碼
- React中重新實現(xiàn)強(qiáng)制實施表單的流程步驟
- react實現(xiàn)動態(tài)增減表單項的示例代碼
- React 實現(xiàn)表單組件的示例代碼
相關(guān)文章
react?hooks深拷貝后無法保留視圖狀態(tài)解決方法
這篇文章主要為大家介紹了react?hooks深拷貝后無法保留視圖狀態(tài)解決示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
React?createRef循環(huán)動態(tài)賦值ref問題
這篇文章主要介紹了React?createRef循環(huán)動態(tài)賦值ref問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01

