Skip to content

Commit 69640c0

Browse files
authored
feat(modal): add useModal (#6517)
* feat(modal): add useModal hook * feat(modal): add HookModal demo * test(modal): update HookModal demo snap * feat(modal): update modal docs * chore(modal): update modal type
1 parent 0df6abe commit 69640c0

File tree

11 files changed

+418
-28
lines changed

11 files changed

+418
-28
lines changed

components/modal/Modal.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,6 @@ export interface ModalLocale {
149149
justOkText: string;
150150
}
151151

152-
export const destroyFns = [];
153-
154152
export default defineComponent({
155153
compatConfig: { MODE: 3 },
156154
name: 'AModal',
@@ -167,6 +165,7 @@ export default defineComponent({
167165
props,
168166
);
169167
const [wrapSSR, hashId] = useStyle(prefixCls);
168+
170169
warning(
171170
props.visible === undefined,
172171
'Modal',

components/modal/__tests__/__snapshots__/demo.test.js.snap

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`renders ./components/modal/demo/HookModal.vue correctly 1`] = `
4+
<div><button class="ant-btn ant-btn-default" type="button">
5+
<!----><span>Confirm</span>
6+
</button><button class="ant-btn ant-btn-default" type="button">
7+
<!----><span>With promise</span>
8+
</button><button class="ant-btn ant-btn-dashed" type="button">
9+
<!----><span>Delete</span>
10+
</button><button class="ant-btn ant-btn-dashed" type="button">
11+
<!----><span>With extra props</span>
12+
</button></div>
13+
`;
14+
315
exports[`renders ./components/modal/demo/async.vue correctly 1`] = `
416
<div><button class="ant-btn ant-btn-primary" type="button">
517
<!----><span>Open Modal with async logic</span>

components/modal/confirm.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { createVNode, render as vueRender } from 'vue';
22
import ConfirmDialog from './ConfirmDialog';
33
import type { ModalFuncProps } from './Modal';
4-
import { destroyFns } from './Modal';
54
import ConfigProvider, { globalConfigForApi } from '../config-provider';
65
import omit from '../_util/omit';
76

87
import { getConfirmLocale } from './locale';
8+
import destroyFns from './destroyFns';
99

1010
type ConfigUpdate = ModalFuncProps | ((prevConfig: ModalFuncProps) => ModalFuncProps);
11+
export type ModalStaticFunctions<T = ModalFunc> = Record<NonNullable<ModalFuncProps['type']>, T>;
1112

1213
export type ModalFunc = (props: ModalFuncProps) => {
1314
destroy: () => void;

components/modal/demo/HookModal.vue

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<docs>
2+
---
3+
order: 12
4+
title:
5+
zh-CN: 使用useModal获取上下文
6+
en-US: Use useModal to get context
7+
---
8+
9+
## zh-CN
10+
11+
通过 `Modal.useModal` 创建支持读取 context 的 `contextHolder`。
12+
13+
## en-US
14+
15+
Use `Modal.useModal` to get `contextHolder` with context accessible issue.
16+
17+
</docs>
18+
19+
<template>
20+
<div>
21+
<a-button @click="showConfirm">Confirm</a-button>
22+
<a-button @click="showPromiseConfirm">With promise</a-button>
23+
<a-button type="dashed" @click="showDeleteConfirm">Delete</a-button>
24+
<a-button type="dashed" @click="showPropsConfirm">With extra props</a-button>
25+
<contextHolder />
26+
</div>
27+
</template>
28+
29+
<script lang="ts" setup>
30+
import { Modal } from 'ant-design-vue';
31+
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
32+
import { createVNode } from 'vue';
33+
const [modal, contextHolder] = Modal.useModal();
34+
const showConfirm = () => {
35+
modal.confirm({
36+
title: 'Do you Want to delete these items?',
37+
icon: createVNode(ExclamationCircleOutlined),
38+
content: createVNode('div', { style: 'color:red;' }, 'Some descriptions'),
39+
onOk() {
40+
console.log('OK');
41+
},
42+
onCancel() {
43+
console.log('Cancel');
44+
},
45+
class: 'test',
46+
});
47+
};
48+
const showDeleteConfirm = () => {
49+
modal.confirm({
50+
title: 'Are you sure delete this task?',
51+
icon: createVNode(ExclamationCircleOutlined),
52+
content: 'Some descriptions',
53+
okText: 'Yes',
54+
okType: 'danger',
55+
cancelText: 'No',
56+
onOk() {
57+
console.log('OK');
58+
},
59+
onCancel() {
60+
console.log('Cancel');
61+
},
62+
});
63+
};
64+
const showPropsConfirm = () => {
65+
modal.confirm({
66+
title: 'Are you sure delete this task?',
67+
icon: createVNode(ExclamationCircleOutlined),
68+
content: 'Some descriptions',
69+
okText: 'Yes',
70+
okType: 'danger',
71+
okButtonProps: {
72+
disabled: true,
73+
},
74+
cancelText: 'No',
75+
onOk() {
76+
console.log('OK');
77+
},
78+
onCancel() {
79+
console.log('Cancel');
80+
},
81+
});
82+
};
83+
84+
function showPromiseConfirm() {
85+
modal.confirm({
86+
title: 'Do you want to delete these items?',
87+
icon: createVNode(ExclamationCircleOutlined),
88+
content: 'When clicked the OK button, this dialog will be closed after 1 second',
89+
async onOk() {
90+
try {
91+
return await new Promise((resolve, reject) => {
92+
setTimeout(Math.random() > 0.5 ? resolve : reject, 1000);
93+
});
94+
} catch {
95+
return console.log('Oops errors!');
96+
}
97+
},
98+
onCancel() {},
99+
});
100+
}
101+
</script>

components/modal/demo/index.vue

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<custom-footer />
66
<confirm />
77
<info />
8+
<HookModal />
89
<locale />
910
<manual />
1011
<position />
@@ -31,6 +32,7 @@ import Width from './width.vue';
3132
import Fullscreen from './fullscreen.vue';
3233
import ButtonProps from './button-props.vue';
3334
import modalRenderVue from './modal-render.vue';
35+
import HookModal from './HookModal.vue';
3436
import CN from '../index.zh-CN.md';
3537
import US from '../index.en-US.md';
3638
import { defineComponent } from 'vue';
@@ -52,6 +54,7 @@ export default defineComponent({
5254
ButtonProps,
5355
Fullscreen,
5456
modalRenderVue,
57+
HookModal,
5558
},
5659
setup() {
5760
return {};

components/modal/destroyFns.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const destroyFns: Array<Function> = [];
2+
export default destroyFns;

components/modal/index.en-US.md

+34-11
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,44 @@ router.beforeEach((to, from, next) => {
116116
})
117117
```
118118

119+
### Modal.useModal()
120+
121+
When you need using Context, you can use `contextHolder` which created by `Modal.useModal` to insert into children. Modal created by hooks will get all the context where `contextHolder` are. Created `modal` has the same creating function with `Modal.method`.
122+
123+
```html
124+
<template>
125+
<contextHolder />
126+
<!-- <component :is='contextHolder'/> -->
127+
</template>
128+
<script setup>
129+
import { Modal } from 'ant-design-vue';
130+
const [modal, contextHolder] = Modal.useModal();
131+
132+
modal.confirm({
133+
// ...
134+
});
135+
</script>
136+
```
137+
119138
## FAQ
120139

121140
### Why can't the Modal method obtain global registered components, context, vuex, etc. and ConfigProvider `locale/prefixCls/theme` configuration, and can't update data responsively?
122141

123142
Call the Modal method directly, and the component will dynamically create a new Vue entity through `Vue.render`. Its context is not the same as the context where the current code is located, so the context information cannot be obtained.
124143

125-
When you need context information (for example, using a globally registered component), you can pass the current component context through the `appContext` property. When you need to keep the property responsive, you can use the function to return:
126-
127-
```tsx
128-
import { getCurrentInstance } from 'vue';
129-
130-
const appContext = getCurrentInstance().appContext;
131-
const title = ref('some message');
132-
Modal.confirm({
133-
title: () => title.value, // the change of title will update the title in confirm synchronously
134-
appContext,
135-
});
144+
When you need context information (for example, using a globally registered component), you can use `Modal.useModal` to get `modal` instance and `contextHolder` node. And put it in your children:
145+
146+
```html
147+
<template>
148+
<contextHolder />
149+
<!-- <component :is='contextHolder'/> -->
150+
</template>
151+
<script setup>
152+
import { Modal } from 'ant-design-vue';
153+
const [modal, contextHolder] = Modal.useModal();
154+
155+
modal.confirm({
156+
// ...
157+
});
158+
</script>
136159
```

components/modal/index.tsx

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import type { App, Plugin } from 'vue';
22
import type { ModalFunc, ModalFuncProps } from './Modal';
3-
import Modal, { destroyFns } from './Modal';
3+
import Modal from './Modal';
44
import confirm, { withWarn, withInfo, withSuccess, withError, withConfirm } from './confirm';
5-
5+
import useModal from './useModal';
6+
import destroyFns from './destroyFns';
67
export type { ActionButtonProps } from '../_util/ActionButton';
78
export type { ModalProps, ModalFuncProps } from './Modal';
89

910
function modalWarn(props: ModalFuncProps) {
1011
return confirm(withWarn(props));
1112
}
12-
13+
Modal.useModal = useModal;
1314
Modal.info = function infoFn(props: ModalFuncProps) {
1415
return confirm(withInfo(props));
1516
};
@@ -60,4 +61,6 @@ export default Modal as typeof Modal &
6061
readonly confirm: ModalFunc;
6162

6263
readonly destroyAll: () => void;
64+
65+
readonly useModal: typeof useModal;
6366
};

components/modal/index.zh-CN.md

+34-11
Original file line numberDiff line numberDiff line change
@@ -120,21 +120,44 @@ router.beforeEach((to, from, next) => {
120120
})
121121
```
122122

123+
### Modal.useModal()
124+
125+
当你需要使用 Context 时,可以通过 `Modal.useModal` 创建一个 `contextHolder` 插入子节点中。通过 hooks 创建的临时 Modal 将会得到 `contextHolder` 所在位置的所有上下文。创建的 `modal` 对象拥有与 [`Modal.method`](#modalmethod) 相同的创建通知方法。
126+
127+
```html
128+
<template>
129+
<contextHolder />
130+
<!-- <component :is='contextHolder'/> -->
131+
</template>
132+
<script setup>
133+
import { Modal } from 'ant-design-vue';
134+
const [modal, contextHolder] = Modal.useModal();
135+
136+
modal.confirm({
137+
// ...
138+
});
139+
</script>
140+
```
141+
123142
## FAQ
124143

125144
### 为什么 Modal 方法不能获取 全局注册组件、context、vuex 等内容和 ConfigProvider `locale/prefixCls/theme` 配置, 以及不能响应式更新数据 ?
126145

127146
直接调用 Modal 方法,组件会通过 `Vue.render` 动态创建新的 Vue 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
128147

129-
当你需要 context 信息(例如使用全局注册的组件)时,可以通过 `appContext` 属性传递当前组件 context, 当你需要保留属性响应式时,你可以使用函数返回:
130-
131-
```tsx
132-
import { getCurrentInstance } from 'vue';
133-
134-
const appContext = getCurrentInstance().appContext;
135-
const title = ref('some message');
136-
Modal.confirm({
137-
title: () => title.value, // 此时 title 的改变,会同步更新 confirm 中的 title
138-
appContext,
139-
});
148+
当你需要 context 信息(例如使用全局注册的组件)时,可以通过 Modal.useModal 方法会返回 modal 实体以及 contextHolder 节点。将其插入到你需要获取 context 位置即可:
149+
150+
```html
151+
<template>
152+
<contextHolder />
153+
<!-- <component :is='contextHolder'/> -->
154+
</template>
155+
<script setup>
156+
import { Modal } from 'ant-design-vue';
157+
const [modal, contextHolder] = Modal.useModal();
158+
159+
modal.confirm({
160+
// ...
161+
});
162+
</script>
140163
```
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { PropType } from 'vue';
2+
import { computed, defineComponent } from 'vue';
3+
import { useConfigContextInject } from '../../config-provider/context';
4+
import { useLocaleReceiver } from '../../locale-provider/LocaleReceiver';
5+
import defaultLocale from '../../locale/en_US';
6+
import ConfirmDialog from '../ConfirmDialog';
7+
import type { ModalFuncProps } from '../Modal';
8+
import initDefaultProps from '../../_util/props-util/initDefaultProps';
9+
export interface HookModalProps {
10+
afterClose: () => void;
11+
config: ModalFuncProps;
12+
destroyAction: (...args: any[]) => void;
13+
open: boolean;
14+
}
15+
16+
export interface HookModalRef {
17+
destroy: () => void;
18+
update: (config: ModalFuncProps) => void;
19+
}
20+
21+
const comfirmFuncProps = () => ({
22+
config: Object as PropType<ModalFuncProps>,
23+
afterClose: Function as PropType<() => void>,
24+
destroyAction: Function as PropType<(e: any) => void>,
25+
open: Boolean,
26+
});
27+
28+
export default defineComponent({
29+
name: 'HookModal',
30+
inheritAttrs: false,
31+
props: initDefaultProps(comfirmFuncProps(), {
32+
config: {
33+
width: 520,
34+
okType: 'primary',
35+
},
36+
}),
37+
setup(props: HookModalProps, { expose }) {
38+
const open = computed(() => props.open);
39+
const innerConfig = computed(() => props.config);
40+
const { direction, getPrefixCls } = useConfigContextInject();
41+
const prefixCls = getPrefixCls('modal');
42+
const rootPrefixCls = getPrefixCls();
43+
44+
const afterClose = () => {
45+
props?.afterClose();
46+
innerConfig.value.afterClose?.();
47+
};
48+
49+
const close = (...args: any[]) => {
50+
props.destroyAction(...args);
51+
};
52+
53+
expose({ destroy: close });
54+
const mergedOkCancel = innerConfig.value.okCancel ?? innerConfig.value.type === 'confirm';
55+
const [contextLocale] = useLocaleReceiver('Modal', defaultLocale.Modal);
56+
return () => (
57+
<ConfirmDialog
58+
prefixCls={prefixCls}
59+
rootPrefixCls={rootPrefixCls}
60+
{...innerConfig.value}
61+
close={close}
62+
open={open.value}
63+
afterClose={afterClose}
64+
okText={
65+
innerConfig.value.okText ||
66+
(mergedOkCancel ? contextLocale?.value.okText : contextLocale?.value.justOkText)
67+
}
68+
direction={innerConfig.value.direction || direction.value}
69+
cancelText={innerConfig.value.cancelText || contextLocale?.value.cancelText}
70+
/>
71+
);
72+
},
73+
});

0 commit comments

Comments
 (0)