此文暂且不聊表格展示和表格删除,主要针对了增加和修改表单场景
前言
最新又开始写CRUD了,功能如图:
写之前就在思考,马上都3202年了,CRUD的最优解是什么?做了一系列调研,大抵是三种方案
- 低代码系统生成CRUD页面
参考# 如何设计低代码平台快速构建页面 | (200+页面) 与阿里的formily
实现思路大概是:可视化操作生成一串JSON,封装好一个渲染组件,依照协议消费此JSON,完成渲染。
优点:
1. 在这种系统支持的业务模式下可快生产CRUD页面
2. 无需手工编码
缺点:
1. 强依赖于低代码平台以及相关的SDK,脱离平台或SDK无法工作
2. 二次需求开发困难
3. 复杂业务逻辑无法支撑
4. 构建一套完善的低代码系统需要不少的人力成本
2. 采用第三方封装好的库
目前并没有搜索到此场景下的完整封装
3. 自己动手、丰衣足食
基于时间成本与后期可维护性的考虑,还是摒弃了低代码系统,选择手动编码。
然后就基于Antd的Modal组件,将新增和修改两个场景的弹出框利用高阶组件来进行了一层封装,基于真实场景沉淀、开箱即用,点击Demo体验
实现思路
流程图
使用
1. 创建一个基于Antd-Form的常规表单组件UserForm,接受form和formProps参数(通过高阶组件传递过来的参数),将其传到Form组件上
import { IPropsFromWithModal } from "./withModal/type";
import { Form, Select, Input } from "antd";
export type IUserFormValue = {
username: string;
password: string;
};
const UserForm: React.FC<IPropsFromWithModal<IUserFormValue>> = ({
form,
formProps,
}) => {
return (
<Form {...formProps} form={form}>
<Form.Item
name="userName"
label="用户名"
rules={[
{
required: true,
message: "请输入名称"
}
]}
>
<Input placeholder="请输入"/>
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[
{
required: true,
message: "请输入"
}
]}
>
<Input.Password placeholder="请输入" />
</Form.Item>
</Form>
);
};
2. 引入withModal,用withModal包裹一下该表单组件,默认导出包裹后的组件
import { withModal } from '@/components/WithModal';
const UserFormModal = withModal<IUserFormValue, any>(UserForm);
export default UserFormModal;
包裹后组件的实例通过actionRef参数暴露了open/close两个方法
对应的参数如下:
export type IFormWrapperOpenProps<FormValues = any> = {
/** Modal的标题 */
title: React.ReactNode;
/** 表单初始值 */
formValues?: FormValues;
/** 请求函数 */
submitRuquest: (...args: any[]) => Promise<unknown>;
/** 请求额外的参数 */
submitExtraParams?: Record<string, any>;
/** 表单模式 */
mode?: IMode;
};
export type IFormWrapperRef<FormValues = any> = {
/** 表单弹窗打开接口 */
open: (openProps: IFormWrapperOpenProps<FormValues>) => void;
/** 表单弹窗关闭接口 */
close: () => void;
};
3.引入该组件即可直接使用
通过actionRef直接调用组件暴露的open与close方法,并传递相关参数以使用,具体例子请参考这里
import { useRef } from "react";
import { Button } from "antd";
import { createUser, editUser } from "./service";
import UserFormModal from "./Form";
export default function Demo() {
const userFormModalRef = useRef<IFormWrapperRef>(null);
return (
<div>
<Button
onClick={() => {
userFormModalRef.current?.open({
title: "新增用户",
submitRuquest: createUser,
mode: "add"
});
}}
>
新增用户弹框
</Button>
<Button
onClick={() => {
userFormModalRef.current?.open({
formValues: {
userName: "用户01",
password: "123456",
userOrgan: "option2"
},
title: "编辑用户",
submitRuquest: editUser,
submitExtraParams: { id: 1 },
mode: "edit"
});
}}
>
修改用户弹框
</Button>
<UserFormModal
actionRef={userFormModalRef}
/>
</div>
);
}
代码
withModal/index.tsx
import { IFormWrappedModalProps, ISaveRef } from "./type";
import React, { useState, useImperativeHandle, FC } from "react";
import { useEffect, useRef } from "react";
import { Modal, Button, Form, Spin } from "antd";
import cloneDeep from "lodash/cloneDeep";
export function withModal<FormValues = any, RequestRes = any>(
Component: React.FunctionComponent<any>
) {
const WrappedComponent: FC<IFormWrappedModalProps<FormValues, RequestRes>> = (
props
) => {
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
const [formLoading, setFormLoading] = useState(false);
const [value, setValue] = useState<FormValues | null>(null);
const [title, setTitle] = useState<React.ReactNode>();
const saveRef = useRef<ISaveRef>({
submitRuquest: null,
submitExtraParams: {},
mode: "add"
});
const {
actionRef,
modalProps = {},
formProps = { labelCol: { span: 6 }, wrapperCol: { span: 14 } },
handleSubmitParams,
successCallback,
errorCallback,
judegeRequestSuccess = (res) => (res as any).success
} = props;
const onCancel = () => {
setVisible(false);
setTimeout(() => {
setValue(null);
}, 100);
};
const onSubmit = () => {
form.validateFields().then(async (value) => {
setBtnLoading(true);
let requstParmas = value;
if (handleSubmitParams) {
requstParmas = handleSubmitParams(cloneDeep(value));
}
try {
const { submitRuquest, submitExtraParams } = saveRef.current;
if (submitRuquest) {
const res = (await submitRuquest({
...requstParmas,
...submitExtraParams
})) as RequestRes;
setBtnLoading(false);
if (judegeRequestSuccess(res)) {
onCancel();
if (successCallback) {
successCallback(res);
}
} else {
if (errorCallback) {
errorCallback(res);
}
}
}
} catch (error) {
setBtnLoading(false);
}
});
};
// 实例挂载表单操作接口
useImperativeHandle(actionRef, () => ({
open: ({
formValues = null,
title,
submitRuquest,
submitExtraParams,
mode = "add"
}) => {
setVisible(true);
setValue(formValues);
setTitle(title);
saveRef.current.submitRuquest = submitRuquest;
saveRef.current.mode = mode;
if (submitExtraParams) {
saveRef.current.submitExtraParams = submitExtraParams;
}
},
close: onCancel
}));
useEffect(() => {
if (value) {
form.setFieldsValue(value);
} else {
form.resetFields();
}
}, [value]);
const { mode } = saveRef.current;
return (
<Modal
title={title}
maskClosable={true}
visible={visible}
onCancel={onCancel}
destroyOnClose={false}
width={660}
footer={[
<Button type="ghost" key="cancel" onClick={onCancel}>
取消
</Button>,
<Button
type="primary"
key="submit"
onClick={onSubmit}
loading={btnLoading}
>
确定
</Button>
]}
{...modalProps}
>
<Spin spinning={formLoading}>
{React.createElement(Component, {
form,
setFormLoading,
formProps,
mode,
...props
})}
</Spin>
</Modal>
);
};
return WrappedComponent;
}
withModal/type.ts
import { ModalProps, FormProps, FormInstance } from "antd";
export type IModalProps = Omit<ModalProps, "onOk" | "onCancel">;
export type func = (...args: any[]) => void;
export enum EFormMode {
add = "add",
edit = "edit",
readOnly = "readOnly"
}
export type IMode = "add" | "edit" | "readOnly";
export interface IFormWrappedModalProps<U, T> {
/** 透传给Modal */
modalProps?: IModalProps;
/** 透传给Form */
formProps?: FormProps;
/** 暴露操作方法的ref */
actionRef?: React.RefObject<IFormWrapperRef<U>>;
/** 提交前处理参数 */
handleSubmitParams?: (parmas: U) => void;
/** 请求成功的判定条件 */
judegeRequestSuccess?: (res: T) => boolean;
/** 成功的回调 */
successCallback?: (res: T) => void;
/** 请求失败的回调 */
errorCallback?: (res: T) => void;
/** 允许透传其他值 */
[index: string]: any;
}
export type IFormWrapperOpenProps<FormValues = any> = {
title: React.ReactNode;
/** 表单值 */
formValues?: FormValues;
/** 请求函数 */
submitRuquest: (...args: any[]) => Promise<unknown>;
/** 请求额外的参数 */
submitExtraParams?: Record<string, any>;
/** 表单模式 */
mode?: IMode;
};
export type IFormWrapperRef<FormValues = any> = {
/** 表单弹窗打开接口 */
open: (openProps: IFormWrapperOpenProps<FormValues>) => void;
/** 表单弹窗关闭接口 */
close: () => void;
};
export type ISaveRef = {
submitRuquest: IFormWrapperOpenProps["submitRuquest"] | null;
submitExtraParams: IFormWrapperOpenProps["submitExtraParams"];
mode: IFormWrapperOpenProps["mode"];
};
// Pick<IFormWrapperOpenProps, 'submitRuquest' | 'submitExtraParams' | 'mode'>
export interface IPropsFromWithModal<T>
extends IFormWrappedModalProps<any, any> {
/** 表单的实例ref */
form: FormInstance<T>;
/** Modal的loading控制方法 */
setFormLoading: func;
/** 透传给Form */
formProps: FormProps;
/** 表单模式 */
mode: IMode;
}
遇到的问题
封装的组件需要向外暴露open与close两个方法,首当其冲的思路便是使用React.forwardRef与useImperativeHandle结合,但在TS类型的书写上遇到了问题,在使用forwardRef时,组件接受参数除了通用已知的一些参数以外,还需要透传一些未知参数到真实的Form表单组件中,所以Props中需要有[index:string]:any,比如:
export interface IFormWrappedModalProps<U, T> {
/** 透传给Modal */
modalProps?: IModalProps;
/** 透传给Form */
formProps?: FormProps;
/** ProTable的ref */
tableRef?: MutableRefObject<ActionType | undefined>;
/** 提交前处理参数 */
handleSubmitParams?: (parmas: U) => void;
/** 请求成功的判定条件 */
judegeRequestSuccess?: (res: T) => boolean;
/** 成功的回调 */
successCallback?: (res: T) => void;
/** 请求失败的回调 */
errorCallback?: (res: T) => void;
/** 允许透传其他值 */
[index: string]: any;
}
但在@types/react中定义的forwardRef类型在增加[index:string]:any便丢失了已定义的所有属性的类型提示,比如modalProps等...,更详细的情况可查看这里,该情况已向@types/React提了issue,不知能否被解决
目前的解决方案就是不用forwardRef,直接接受一个属性actionRef,通过这个属性结合useImperativeHandle来向外暴露方法,具体实现可见代码
结语
如有谬误,欢迎大家指正。
对于CURD,你的最优解是什么呢?欢迎讨论~