Skip to content

Commit 6eb4d8f

Browse files
authored
Feat(use): add useMessage useNotification (#6527)
* feat(Message): add useMessage hook * feat(Notification): add useNotification hook * feat(Message): add Hook demo * feat(Notification): add Hook demo * test(Message): update demo snap * test(Notification): update demo snap * docs(Message): update docs with FAQ * docs(Notification): update docs with FAQ
1 parent b61c88e commit 6eb4d8f

24 files changed

+1465
-6
lines changed

components/_util/util.ts

+19
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,24 @@ export function renderHelper<T = Record<string, any>>(
7878
}
7979
return v ?? defaultV;
8080
}
81+
export function wrapPromiseFn(openFn: (resolve: VoidFunction) => VoidFunction) {
82+
let closeFn: VoidFunction;
83+
84+
const closePromise = new Promise<boolean>(resolve => {
85+
closeFn = openFn(() => {
86+
resolve(true);
87+
});
88+
});
89+
90+
const result: any = () => {
91+
closeFn?.();
92+
};
93+
94+
result.then = (filled: VoidFunction, rejected: VoidFunction) =>
95+
closePromise.then(filled, rejected);
96+
result.promise = closePromise;
97+
98+
return result;
99+
}
81100

82101
export { isOn, cacheStringFunction, camelize, hyphenate, capitalize, resolvePropValue };

components/message/PurePanel.tsx

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import Notice from '../vc-notification/Notice';
2+
import type { NoticeProps } from '../vc-notification/Notice';
3+
import useStyle from './style';
4+
import type { NoticeType } from './interface';
5+
import {
6+
CheckCircleFilled,
7+
CloseCircleFilled,
8+
ExclamationCircleFilled,
9+
InfoCircleFilled,
10+
LoadingOutlined,
11+
} from '@ant-design/icons-vue';
12+
import type { VueNode } from '../_util/type';
13+
import classNames from '../_util/classNames';
14+
import { useConfigContextInject } from '../config-provider/context';
15+
import { computed, defineComponent } from 'vue';
16+
17+
export const TypeIcon = {
18+
info: <InfoCircleFilled />,
19+
success: <CheckCircleFilled />,
20+
error: <CloseCircleFilled />,
21+
warning: <ExclamationCircleFilled />,
22+
loading: <LoadingOutlined />,
23+
};
24+
25+
export interface PureContentProps {
26+
prefixCls: string;
27+
type?: NoticeType;
28+
icon?: VueNode;
29+
children: VueNode;
30+
}
31+
32+
export const PureContent = defineComponent({
33+
name: 'PureContent',
34+
inheritAttrs: false,
35+
props: ['prefixCls', 'type', 'icon'] as any,
36+
37+
setup(props, { slots }) {
38+
return () => (
39+
<div
40+
class={classNames(`${props.prefixCls}-custom-content`, `${props.prefixCls}-${props.type}`)}
41+
>
42+
{props.icon || TypeIcon[props.type!]}
43+
<span>{slots.default?.()}</span>
44+
</div>
45+
);
46+
},
47+
});
48+
49+
export interface PurePanelProps
50+
extends Omit<NoticeProps, 'prefixCls' | 'eventKey'>,
51+
Omit<PureContentProps, 'prefixCls' | 'children'> {
52+
prefixCls?: string;
53+
}
54+
55+
/** @private Internal Component. Do not use in your production. */
56+
57+
export default defineComponent({
58+
name: 'PurePanel',
59+
inheritAttrs: false,
60+
props: ['prefixCls', 'class', 'type', 'icon', 'content'] as any,
61+
setup(props, { slots, attrs }) {
62+
const { getPrefixCls } = useConfigContextInject();
63+
const prefixCls = computed(() => props.staticPrefixCls || getPrefixCls('message'));
64+
const [, hashId] = useStyle(prefixCls);
65+
return (
66+
<Notice
67+
{...attrs}
68+
prefixCls={prefixCls.value}
69+
class={classNames(hashId, `${prefixCls.value}-notice-pure-panel`)}
70+
noticeKey="pure"
71+
duration={null}
72+
>
73+
<PureContent prefixCls={props.prefixCls} type={props.type} icon={props.icon}>
74+
{slots.default?.()}
75+
</PureContent>
76+
</Notice>
77+
);
78+
},
79+
});

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

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ exports[`renders ./components/message/demo/duration.vue correctly 1`] = `
1212
</button>
1313
`;
1414

15+
exports[`renders ./components/message/demo/hook.vue correctly 1`] = `
16+
<!--teleport start-->
17+
<!--teleport end-->
18+
<button class="ant-btn ant-btn-primary" type="button">
19+
<!----><span>Display normal message</span>
20+
</button>
21+
`;
22+
1523
exports[`renders ./components/message/demo/info.vue correctly 1`] = `
1624
<button class="ant-btn ant-btn-primary" type="button">
1725
<!----><span>Display normal message</span>

components/message/demo/hook.vue

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<docs>
2+
---
3+
order: 10
4+
title:
5+
zh-CN: Hooks 调用(推荐)
6+
en-US: Hooks Usage (Recommend)
7+
---
8+
9+
## zh-CN
10+
11+
通过 `message.useMessage` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。
12+
13+
## en-US
14+
15+
Use `message.useMessage` to get `contextHolder` with context accessible issue. Please note that, we recommend to use top level registration instead of `message` static method, because static method cannot consume context, and ConfigProvider data will not work.
16+
17+
</docs>
18+
19+
<template>
20+
<contextHolder />
21+
<a-button type="primary" @click="info">Display normal message</a-button>
22+
</template>
23+
24+
<script lang="ts" setup>
25+
import { message } from 'ant-design-vue';
26+
const [messageApi, contextHolder] = message.useMessage();
27+
28+
const info = () => {
29+
messageApi.info('Hello, Ant Design!');
30+
};
31+
</script>

components/message/demo/index.vue

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<template>
22
<demo-sort>
3+
<Hook />
34
<info />
45
<duration />
56
<other />
@@ -20,6 +21,7 @@ import customStyleVue from './custom-style.vue';
2021
import CN from '../index.zh-CN.md';
2122
import US from '../index.en-US.md';
2223
import { defineComponent } from 'vue';
24+
import Hook from './hook.vue';
2325
export default defineComponent({
2426
CN,
2527
US,
@@ -31,6 +33,7 @@ export default defineComponent({
3133
Thenable,
3234
Update,
3335
customStyleVue,
36+
Hook,
3437
},
3538
setup() {
3639
return {};

components/message/index.en-US.md

+31
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Methods for global configuration and destruction are also provided:
6464

6565
- `message.config(options)`
6666
- `message.destroy()`
67+
- `message.useMessage()`
6768

6869
#### message.config
6970

@@ -85,3 +86,33 @@ message.config({
8586
| prefixCls | The prefix className of message node | string | `ant-message` | 3.0 |
8687
| rtl | Whether to enable RTL mode | boolean | false | 3.0 |
8788
| top | distance from top | string | `8px` | |
89+
90+
## FAQ
91+
92+
### Why I can not access context, Pinia, ConfigProvider `locale/prefixCls/theme` in message?
93+
94+
antdv will dynamic create Vue instance by `Vue.render` when call message methods. Whose context is different with origin code located context.
95+
96+
When you need context info (like ConfigProvider context), you can use `message.useMessage` to get `api` instance and `contextHolder` node. And put it in your children:
97+
98+
```html
99+
<template>
100+
<contextHolder />
101+
<!-- <component :is='contextHolder'/> -->
102+
</template>
103+
<script setup>
104+
import { message } from 'ant-design-vue';
105+
const [messageApi, contextHolder] = message.useMessage();
106+
messageApi.open({
107+
// ...
108+
});
109+
</script>
110+
```
111+
112+
**Note:** You must insert `contextHolder` into your children with hooks. You can use origin method if you do not need context connection.
113+
114+
> [App Package Component](/components/app) can be used to simplify the problem of `useMessage` and other methods that need to manually implant contextHolder.
115+
116+
### How to set static methods prefixCls ?
117+
118+
You can config with [`ConfigProvider.config`](/components/config-provider#configproviderconfig-4130)

components/message/index.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { Key, VueNode } from '../_util/type';
99
import type { NotificationInstance } from '../vc-notification/Notification';
1010
import classNames from '../_util/classNames';
1111
import useStyle from './style';
12-
12+
import useMessage from './useMessage';
1313
let defaultDuration = 3;
1414
let defaultTop: string;
1515
let messageInstance: NotificationInstance;
@@ -70,6 +70,7 @@ function getMessageInstance(args: MessageArgsProps, callback: (i: NotificationIn
7070
callback(messageInstance);
7171
return;
7272
}
73+
7374
Notification.newInstance(
7475
{
7576
appContext: args.appContext,
@@ -225,14 +226,15 @@ export function attachTypeApi(originalApi: MessageApi, type: NoticeType) {
225226
typeList.forEach(type => attachTypeApi(api, type));
226227

227228
api.warn = api.warning;
228-
229+
api.useMessage = useMessage;
229230
export interface MessageInstance {
230231
info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
231232
success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
232233
error(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
233234
warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
234235
loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
235236
open(args: MessageArgsProps): MessageType;
237+
useMessage: typeof useMessage;
236238
}
237239

238240
export interface MessageApi extends MessageInstance {

components/message/index.zh-CN.md

+31
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*7qMTRoq3ZGkAAA
6767

6868
- `message.config(options)`
6969
- `message.destroy()`
70+
- `message.useMessage()`
7071

7172
#### message.config
7273

@@ -88,3 +89,33 @@ message.config({
8889
| prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 3.0 | |
8990
| rtl | 是否开启 RTL 模式 | boolean | false | | |
9091
| top | 消息距离顶部的位置 | string | `8px` | | |
92+
93+
## FAQ
94+
95+
### 为什么 message 不能获取 context、Pinia 的内容和 ConfigProvider 的 `locale/prefixCls/theme` 等配置?
96+
97+
直接调用 message 方法,antdv 会通过 `Vue.render` 动态创建新的 Vue 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
98+
99+
当你需要 context 信息(例如 ConfigProvider 配置的内容)时,可以通过 `message.useMessage` 方法会返回 `api` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可:
100+
101+
```html
102+
<template>
103+
<contextHolder />
104+
<!-- <component :is='contextHolder'/> -->
105+
</template>
106+
<script setup>
107+
import { message } from 'ant-design-vue';
108+
const [messageApi, contextHolder] = message.useMessage();
109+
messageApi.open({
110+
// ...
111+
});
112+
</script>
113+
```
114+
115+
**异同**:通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。
116+
117+
> 可通过 [App 包裹组件](/components/app-cn) 简化 `useMessage` 等方法需要手动植入 contextHolder 的问题。
118+
119+
### 静态方法如何设置 prefixCls ?
120+
121+
你可以通过 [`ConfigProvider.config`](/components/config-provider-cn#configproviderconfig-4130) 进行设置。

components/message/interface.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { CSSProperties } from 'vue';
2+
import type { Key, VueNode } from '../_util/type';
3+
4+
export type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
5+
6+
export interface ConfigOptions {
7+
top?: number;
8+
duration?: number;
9+
prefixCls?: string;
10+
getContainer?: () => HTMLElement;
11+
transitionName?: string;
12+
maxCount?: number;
13+
rtl?: boolean;
14+
}
15+
16+
export interface ArgsProps {
17+
content: VueNode;
18+
duration?: number;
19+
type?: NoticeType;
20+
onClose?: () => void;
21+
icon?: VueNode;
22+
key?: string | number;
23+
style?: CSSProperties;
24+
className?: string;
25+
onClick?: (e: Event) => void;
26+
}
27+
28+
export type JointContent = VueNode | ArgsProps;
29+
30+
export interface MessageType extends PromiseLike<boolean> {
31+
(): void;
32+
}
33+
34+
export type TypeOpen = (
35+
content: JointContent,
36+
duration?: number | VoidFunction, // Also can use onClose directly
37+
onClose?: VoidFunction,
38+
) => MessageType;
39+
40+
export interface MessageInstance {
41+
info: TypeOpen;
42+
success: TypeOpen;
43+
error: TypeOpen;
44+
warning: TypeOpen;
45+
loading: TypeOpen;
46+
open(args: ArgsProps): MessageType;
47+
destroy(key?: Key): void;
48+
}

0 commit comments

Comments
 (0)