命令式打开弹窗设计

0. 痛点-为什么要使用命令式打开弹层

Modal 模态对话框在中台业务中非常常见。声明式 Modal 是一种常见的定义方式,可以通过组件库内置的组件来实现。在声明式 Modal 中,需要手动控制 Modal 的打开状态,并将状态和 UI 绑定在一起。随着业务的增加,页面上可能会出现多个 Modal 和 Button,导致代码变得冗长且难以维护。为了解决这个问题,可以将 Modal 抽离到外部组件中,但这样会导致父组件的状态堆积。另一种解决方案是将 Modal 和 Button 直接耦合在一起,但这样无法单独复用 Modal。为了解耦 Modal,需要编写大量代码并且状态变得混乱。后续可能还会遇到无法在父组件中直接控制 Modal 的问题,可以通过将状态下沉到外部 Store 或者 Context 中来解决。

2. 代码实现

2.1 创建 layer-context.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147

import { Drawer, Modal, ModalProps, DrawerProps } from 'antd';
import { ModalForm, DrawerForm, ModalFormProps, DrawerFormProps } from '@ant-design/pro-components';
import {
Dispatch,
ReactNode,
SetStateAction,
createContext,
useContext,
useId,
useMemo,
useRef,
} from 'react';
import { LayerInstance } from './layer';

type LayerContentProps = {
close: () => void;
};

export type LayerProps = {
id?: string;
content: ReactNode | ((props: LayerContentProps) => ReactNode);
Component: ReactNode | ((props: LayerContentProps) => ReactNode);
} & Omit<ModalProps, 'open' | 'content'> &
DrawerProps &
ModalFormProps &
DrawerFormProps;

interface LayerStackContextValue extends LayerProps {
id: string;
ins?: LayerInstance;
}

export const LayerStackContext = createContext<LayerStackContextValue[]>([]);
export const LayerActionContext = createContext<Dispatch<SetStateAction<LayerStackContextValue[]>>>(
() => void 0,
);

export const useLayerStack: any = () => {
const id = useId();
const currentCount = useRef(0);
const setLayerStack = useContext(LayerActionContext);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return useMemo(() => {
return {
openModal(props: LayerProps & { id?: string }) {
const layerId = `${id}-Modal-${currentCount.current++}`;
setLayerStack((p = []) => {
const layerProps = {
...props,
id: props?.id ?? layerId,
Component: Modal,
_layerName:'modal'
};
const pp = p?.concat(layerProps);
return pp;
});
const hide = () => {
setLayerStack((p = []) => {
return p?.filter((item) => item.id !== layerId);
});
};
return [layerId, hide];
},
openDrawer(props: LayerProps & { id?: string }) {
const layerId = `${id}-Drawer-${currentCount.current++}`;
setLayerStack((p = []) => {
const layerProps = {
...props,
id: props?.id ?? layerId,
Component: Drawer,
_layerName:'drawer'
};
const pp = p?.concat(layerProps);
return pp;
});
const hide = () => {
setLayerStack((p) => {
return p?.filter((item) => item.id !== layerId);
});
};
return [layerId, hide];
},
openFormModal(props: LayerProps & { id?: string }) {
const layerId = `${id}-ModalForm-${currentCount.current++}`;
setLayerStack((p = []) => {
const layerProps = {
...props,
id: props?.id ?? layerId,
Component: ModalForm,
_layerName:'formModal'
};
const pp = p?.concat(layerProps);
return pp;
});
const hide = () => {
setLayerStack((p = []) => {
return p?.filter((item) => item.id !== layerId);
});
};
return [layerId, hide];
},
openFormDrawer(props: LayerProps & { id?: string }) {
const layerId = `${id}-DrawerForm-${currentCount.current++}`;
setLayerStack((p = []) => {
const layerProps = {
...props,
id: props?.id ?? layerId,
Component: DrawerForm,
_layerNameName:'drawerForm'
};
const pp = p?.concat(layerProps);
return pp;
});
const hide = () => {
setLayerStack((p) => {
return p?.filter((item) => item.id !== layerId);
});
};
return [layerId, hide];
},

closeByLayerId(id: string) {
setLayerStack((p) => {
const m = p.find((item) => item.id === id);
if (m?.ins) {
m?.ins?.close?.();
return p;
}
return p.filter((item) => item.id !== id);
});
},
destroyAll() {
setLayerStack((p) => {
p.forEach((m) => {
if (m?.ins) {
m?.ins?.close?.();
return p;
}
});
return p;
});
},
};
}, [id, setLayerStack]);
};

2.2 创建 layer-stack-provider.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { FC, PropsWithChildren, useState } from 'react';
import { LayerActionContext, LayerStackContext } from './layer-context';
import LayerStack from './layer-stack';

export const LayerStackProvider: FC<PropsWithChildren> = ({ children }) => {
const [layerStack, setLayerStack] = useState([] as React.ContextType<typeof LayerStackContext>);
return (
<LayerActionContext.Provider value={setLayerStack}>
{children}
<LayerStackContext.Provider value={layerStack}>
<LayerStack />
</LayerStackContext.Provider>
</LayerActionContext.Provider>
);
};

2.3 创建 layer-stack.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { FC, PropsWithChildren, useContext } from 'react';
import { LayerStackContext } from './layer-context';
import Layer from './layer';

const LayerStack: FC<PropsWithChildren> = ({}) => {
const layerStack = useContext(LayerStackContext);

return (
<>
{layerStack?.map?.((item, index) => {
return <Layer key={item?.id} item={item} index={index} />;
})}
</>
);
};

export default LayerStack;

2.4 创建 layer.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import {
memo,
useContext,
useCallback,
useRef,
useState,
useEffect,
createElement,
MouseEvent,
} from 'react';
import { LayerActionContext, LayerProps } from './layer-context';
import { ProFormInstance } from '@ant-design/pro-components';

export type LayerInstance = {
close: () => void;
};

const Layer = memo<{
item: LayerProps & { id: string; onOk: (e: any, close: any) => void };
index: number;
}>(({ item, index }) => {
const setLayerStack = useContext(LayerActionContext);

const close = useCallback(() => {
setTimeout(() => {
setLayerStack((p) => {
return p?.filter((layer) => layer?.id !== item.id);
});
}, 500);
}, [item.id]);

const { content, title, centered = true, Component, onOk, modalProps, footer, _layerName, ...rest } = item;


const [open, setOpen] = useState(false);
const formRef = useRef<ProFormInstance>();
const dataRef = useRef<any>();
const instanceRef = useRef<LayerInstance>({
close: () => {
setOpen(false);
},
});
useEffect(() => {
setOpen(true);

setLayerStack((p) => {
const newStack = [...p];
newStack[index].ins = instanceRef.current;
return newStack;
});
}, []);
const handleClose = useCallback(() => {
setOpen(false);
}, []);
const afterClose = useCallback(() => {
item?.afterClose?.();
close();
}, []);
const onCancel = useCallback((e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>) => {
item?.onCancel?.(e);
handleClose();
}, []);
const afterOpenChange = useCallback((open: boolean) => {
if (!open) {
item?.afterClose?.();
handleClose();
close();
}
}, []);
const handleOnOk = useCallback(() => {
const data = dataRef.current;
onOk?.(data, handleClose);
}, []);

let handleFooter = footer;
if (typeof handleFooter === 'function') {
// @ts-ignore
handleFooter = handleFooter.bind(null, handleClose);
}

let layerSelfProps = {}
if(_layerName === 'modal' || _layerName === 'drawer') {
layerSelfProps ={
centered,
afterClose,
afterOpenChange,
onOk: handleOnOk,
onCancel,
}
}

return (
<Component
open={open}
{...layerSelfProps}
formRef={formRef}
onClose={onCancel}
onOpenChange={afterOpenChange}
title={title}
modalProps={{ centered, ...modalProps }}
footer={handleFooter}
{...rest}
>
{createElement(content, {
close: handleClose,
formRef: formRef,
dataRef: dataRef,
})}
</Component>
);
});

export default Layer;

2.5 创建导出 index.tsx

1
2
3
4
5
import Layer from './layer';
import LayerStack from './layer-stack';
import { LayerStackProvider } from './layer-stack-provider';
export * from './layer-context';
export { Layer, LayerStack, LayerStackProvider };

3. 如何使用

  1. 在根节点引入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

import { Button } from 'antd';
import React from 'react';
import { LayerStackProvider } from '@/components/Layer';

const AppFC: React.FC = () => {

return (
<div>
<LayerStackProvider>
<div>{children}<div/>
</LayerStackProvider>
</div>
);
};

export default TestFC;

  1. 在其他地方使用消费
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import { Button } from 'antd';
import React from 'react';
import { useLayerStack } from '@/components/Layer';

const TestFC: React.FC = () => {

const { openDrawer, openModal, destroyAll, openFormDrawer, openFormModal } = useLayerStack();
const openM = () => {
openModal({
title: 'openModal',
content: ({ close }) => {
return (
<div className="">
<div
className=""
onClick={() => {
close();
}}
>
关闭
</div>
<div className="" onClick={() => {
openD();
}} >再开一个</div>
<div
className=""
onClick={() => {
destroyAll();
}}
>关闭所有</div>
</div>
);
},
});
};
const openFM = () => {
openFormModal({
title: 'openFormModal',
content: ({ close, formRef }) => {
return (
<div className="">
<ProFormText
width="md"
name="name"
label="签约客户名称"
tooltip="最长为 24 位"
placeholder="请输入名称"
rules={[{ required: true }]}
/>

<ProFormText width="md" name="company" label="我方公司名称" placeholder="请输入名称" />
<Button
onClick={() => {
console.log('清楚签约客户名称', formRef);

formRef?.current?.setFieldsValue?.({ name: '' });
}}
>
清楚签约客户名称
</Button>
</div>
);
},
onFinish: async (values) => {
console.log(values);
return true;
},
});
};
const openFD = async () => {
const [layerId, hide] = await openFormDrawer({
title: 'openFormDrawer',
content: ({ close, formRef }) => {
return (
<div className="">
<ProFormText
width="md"
name="name"
label="签约客户名称"
tooltip="最长为 24 位"
placeholder="请输入名称"
rules={[{ required: true }]}
/>

<ProFormText width="md" name="company" label="我方公司名称" placeholder="请输入名称" />
<Button
onClick={() => {
console.log('清楚签约客户名称', formRef);
formRef?.current?.setFieldsValue?.({ name: '' });
}}
>
清楚签约客户名称
</Button>
</div>
);
},
onFinish: async (values) => {
console.log(values);
return true;
},
});
};
const openD = () => {
openDrawer({
title: 'openDrawer',
content: ({ close }) => {
return (
<div className="">
<div
className=""
onClick={() => {
close();
}}
>
关闭
</div>
<div className="" onClick={() => {
openD();
}} >再开一个</div>
<div
className=""
onClick={() => {
destroyAll();
}}
>关闭所有</div>
</div>
);
},
});
};

return (
<div className={styles.container}>
<Button type="primary" key="new" size="small" onClick={openM}>打开 Modal</Button>
<Button type="primary" key="new" size="small" onClick={openD}>打开 Drawer</Button>
<Button type="primary" key="new" size="small" onClick={openFM}>打开 FormModal</Button>
<Button type="primary" key="new" size="small" onClick={openFD}>打开 FormDrawer</Button>
</div>
);
};

export default TestFC;

4. 参考引用

为什么我更推荐命令式 Modal