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
pull/6538/head
Cherry7 2023-05-05 09:59:57 +08:00 committed by GitHub
parent b61c88e5df
commit 6eb4d8f5c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1465 additions and 6 deletions

View File

@ -78,5 +78,24 @@ export function renderHelper<T = Record<string, any>>(
}
return v ?? defaultV;
}
export function wrapPromiseFn(openFn: (resolve: VoidFunction) => VoidFunction) {
let closeFn: VoidFunction;
const closePromise = new Promise<boolean>(resolve => {
closeFn = openFn(() => {
resolve(true);
});
});
const result: any = () => {
closeFn?.();
};
result.then = (filled: VoidFunction, rejected: VoidFunction) =>
closePromise.then(filled, rejected);
result.promise = closePromise;
return result;
}
export { isOn, cacheStringFunction, camelize, hyphenate, capitalize, resolvePropValue };

View File

@ -0,0 +1,79 @@
import Notice from '../vc-notification/Notice';
import type { NoticeProps } from '../vc-notification/Notice';
import useStyle from './style';
import type { NoticeType } from './interface';
import {
CheckCircleFilled,
CloseCircleFilled,
ExclamationCircleFilled,
InfoCircleFilled,
LoadingOutlined,
} from '@ant-design/icons-vue';
import type { VueNode } from '../_util/type';
import classNames from '../_util/classNames';
import { useConfigContextInject } from '../config-provider/context';
import { computed, defineComponent } from 'vue';
export const TypeIcon = {
info: <InfoCircleFilled />,
success: <CheckCircleFilled />,
error: <CloseCircleFilled />,
warning: <ExclamationCircleFilled />,
loading: <LoadingOutlined />,
};
export interface PureContentProps {
prefixCls: string;
type?: NoticeType;
icon?: VueNode;
children: VueNode;
}
export const PureContent = defineComponent({
name: 'PureContent',
inheritAttrs: false,
props: ['prefixCls', 'type', 'icon'] as any,
setup(props, { slots }) {
return () => (
<div
class={classNames(`${props.prefixCls}-custom-content`, `${props.prefixCls}-${props.type}`)}
>
{props.icon || TypeIcon[props.type!]}
<span>{slots.default?.()}</span>
</div>
);
},
});
export interface PurePanelProps
extends Omit<NoticeProps, 'prefixCls' | 'eventKey'>,
Omit<PureContentProps, 'prefixCls' | 'children'> {
prefixCls?: string;
}
/** @private Internal Component. Do not use in your production. */
export default defineComponent({
name: 'PurePanel',
inheritAttrs: false,
props: ['prefixCls', 'class', 'type', 'icon', 'content'] as any,
setup(props, { slots, attrs }) {
const { getPrefixCls } = useConfigContextInject();
const prefixCls = computed(() => props.staticPrefixCls || getPrefixCls('message'));
const [, hashId] = useStyle(prefixCls);
return (
<Notice
{...attrs}
prefixCls={prefixCls.value}
class={classNames(hashId, `${prefixCls.value}-notice-pure-panel`)}
noticeKey="pure"
duration={null}
>
<PureContent prefixCls={props.prefixCls} type={props.type} icon={props.icon}>
{slots.default?.()}
</PureContent>
</Notice>
);
},
});

View File

@ -12,6 +12,14 @@ exports[`renders ./components/message/demo/duration.vue correctly 1`] = `
</button>
`;
exports[`renders ./components/message/demo/hook.vue correctly 1`] = `
<!--teleport start-->
<!--teleport end-->
<button class="ant-btn ant-btn-primary" type="button">
<!----><span>Display normal message</span>
</button>
`;
exports[`renders ./components/message/demo/info.vue correctly 1`] = `
<button class="ant-btn ant-btn-primary" type="button">
<!----><span>Display normal message</span>

View File

@ -0,0 +1,31 @@
<docs>
---
order: 10
title:
zh-CN: Hooks 调用推荐
en-US: Hooks Usage (Recommend)
---
## zh-CN
通过 `message.useMessage` 创建支持读取 context `contextHolder`请注意我们推荐通过顶层注册的方式代替 `message` 静态方法因为静态方法无法消费上下文因而 ConfigProvider 的数据也不会生效
## en-US
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.
</docs>
<template>
<contextHolder />
<a-button type="primary" @click="info">Display normal message</a-button>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue';
const [messageApi, contextHolder] = message.useMessage();
const info = () => {
messageApi.info('Hello, Ant Design!');
};
</script>

View File

@ -1,5 +1,6 @@
<template>
<demo-sort>
<Hook />
<info />
<duration />
<other />
@ -20,6 +21,7 @@ import customStyleVue from './custom-style.vue';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
import { defineComponent } from 'vue';
import Hook from './hook.vue';
export default defineComponent({
CN,
US,
@ -31,6 +33,7 @@ export default defineComponent({
Thenable,
Update,
customStyleVue,
Hook,
},
setup() {
return {};

View File

@ -64,6 +64,7 @@ Methods for global configuration and destruction are also provided:
- `message.config(options)`
- `message.destroy()`
- `message.useMessage()`
#### message.config
@ -85,3 +86,33 @@ message.config({
| prefixCls | The prefix className of message node | string | `ant-message` | 3.0 |
| rtl | Whether to enable RTL mode | boolean | false | 3.0 |
| top | distance from top | string | `8px` | |
## FAQ
### Why I can not access context, Pinia, ConfigProvider `locale/prefixCls/theme` in message?
antdv will dynamic create Vue instance by `Vue.render` when call message methods. Whose context is different with origin code located context.
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:
```html
<template>
<contextHolder />
<!-- <component :is='contextHolder'/> -->
</template>
<script setup>
import { message } from 'ant-design-vue';
const [messageApi, contextHolder] = message.useMessage();
messageApi.open({
// ...
});
</script>
```
**Note:** You must insert `contextHolder` into your children with hooks. You can use origin method if you do not need context connection.
> [App Package Component](/components/app) can be used to simplify the problem of `useMessage` and other methods that need to manually implant contextHolder.
### How to set static methods prefixCls
You can config with [`ConfigProvider.config`](/components/config-provider#configproviderconfig-4130)

View File

@ -9,7 +9,7 @@ import type { Key, VueNode } from '../_util/type';
import type { NotificationInstance } from '../vc-notification/Notification';
import classNames from '../_util/classNames';
import useStyle from './style';
import useMessage from './useMessage';
let defaultDuration = 3;
let defaultTop: string;
let messageInstance: NotificationInstance;
@ -70,6 +70,7 @@ function getMessageInstance(args: MessageArgsProps, callback: (i: NotificationIn
callback(messageInstance);
return;
}
Notification.newInstance(
{
appContext: args.appContext,
@ -225,7 +226,7 @@ export function attachTypeApi(originalApi: MessageApi, type: NoticeType) {
typeList.forEach(type => attachTypeApi(api, type));
api.warn = api.warning;
api.useMessage = useMessage;
export interface MessageInstance {
info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
@ -233,6 +234,7 @@ export interface MessageInstance {
warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
open(args: MessageArgsProps): MessageType;
useMessage: typeof useMessage;
}
export interface MessageApi extends MessageInstance {

View File

@ -67,6 +67,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*7qMTRoq3ZGkAAA
- `message.config(options)`
- `message.destroy()`
- `message.useMessage()`
#### message.config
@ -88,3 +89,33 @@ message.config({
| prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 3.0 | |
| rtl | 是否开启 RTL 模式 | boolean | false | | |
| top | 消息距离顶部的位置 | string | `8px` | | |
## FAQ
### 为什么 message 不能获取 context、Pinia 的内容和 ConfigProvider 的 `locale/prefixCls/theme` 等配置?
直接调用 message 方法antdv 会通过 `Vue.render` 动态创建新的 Vue 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
当你需要 context 信息(例如 ConfigProvider 配置的内容)时,可以通过 `message.useMessage` 方法会返回 `api` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可:
```html
<template>
<contextHolder />
<!-- <component :is='contextHolder'/> -->
</template>
<script setup>
import { message } from 'ant-design-vue';
const [messageApi, contextHolder] = message.useMessage();
messageApi.open({
// ...
});
</script>
```
**异同**:通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。
> 可通过 [App 包裹组件](/components/app-cn) 简化 `useMessage` 等方法需要手动植入 contextHolder 的问题。
### 静态方法如何设置 prefixCls
你可以通过 [`ConfigProvider.config`](/components/config-provider-cn#configproviderconfig-4130) 进行设置。

View File

@ -0,0 +1,48 @@
import type { CSSProperties } from 'vue';
import type { Key, VueNode } from '../_util/type';
export type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
export interface ConfigOptions {
top?: number;
duration?: number;
prefixCls?: string;
getContainer?: () => HTMLElement;
transitionName?: string;
maxCount?: number;
rtl?: boolean;
}
export interface ArgsProps {
content: VueNode;
duration?: number;
type?: NoticeType;
onClose?: () => void;
icon?: VueNode;
key?: string | number;
style?: CSSProperties;
className?: string;
onClick?: (e: Event) => void;
}
export type JointContent = VueNode | ArgsProps;
export interface MessageType extends PromiseLike<boolean> {
(): void;
}
export type TypeOpen = (
content: JointContent,
duration?: number | VoidFunction, // Also can use onClose directly
onClose?: VoidFunction,
) => MessageType;
export interface MessageInstance {
info: TypeOpen;
success: TypeOpen;
error: TypeOpen;
warning: TypeOpen;
loading: TypeOpen;
open(args: ArgsProps): MessageType;
destroy(key?: Key): void;
}

View File

@ -0,0 +1,228 @@
import { shallowRef, computed, defineComponent } from 'vue';
import { useNotification as useVcNotification } from '../vc-notification';
import type { NotificationAPI } from '../vc-notification';
import CloseOutlined from '@ant-design/icons-vue';
import useStyle from './style';
import type {
MessageInstance,
ArgsProps,
MessageType,
ConfigOptions,
NoticeType,
TypeOpen,
} from './interface';
import { PureContent } from './PurePanel';
import { getMotion } from '../vc-trigger/utils/motionUtil';
import type { Key } from '../_util/type';
import { wrapPromiseFn } from '../_util/util';
import type { VNode } from 'vue';
import useConfigInject from '../config-provider/hooks/useConfigInject';
import classNames from '../_util/classNames';
const DEFAULT_OFFSET = 8;
const DEFAULT_DURATION = 3;
// ==============================================================================
// == Holder ==
// ==============================================================================
type HolderProps = ConfigOptions & {
onAllRemoved?: VoidFunction;
};
interface HolderRef extends NotificationAPI {
prefixCls: string;
hashId: string;
}
const Holder = defineComponent({
name: 'Holder',
inheritAttrs: false,
props: [
'top',
'prefixCls',
'getContainer',
'maxCount',
'duration',
'rtl',
'transitionName',
'onAllRemoved',
] as any,
setup(props, { expose }) {
const { getPrefixCls, getPopupContainer } = useConfigInject('message', props);
const prefixCls = computed(() => getPrefixCls('message', props.prefixCls));
const [, hashId] = useStyle(prefixCls);
// =============================== Style ===============================
const getStyles = () => ({
left: '50%',
transform: 'translateX(-50%)',
top: top ?? DEFAULT_OFFSET,
});
const getClassName = () => classNames(hashId.value, props.rtl ? `${prefixCls.value}-rtl` : '');
// ============================== Motion ===============================
const getNotificationMotion = () =>
getMotion({
prefixCls: prefixCls.value,
animation: props.animation ?? `move-up`,
transitionName: props.transitionName,
});
// ============================ Close Icon =============================
const mergedCloseIcon = (
<span class={`${prefixCls.value}-close-x`}>
<CloseOutlined class={`${prefixCls.value}-close-icon`} />
</span>
);
// ============================== Origin ===============================
const [api, holder] = useVcNotification({
//@ts-ignore
getStyles,
prefixCls: prefixCls.value,
getClassName,
motion: getNotificationMotion,
closable: false,
closeIcon: mergedCloseIcon,
duration: props.duration ?? DEFAULT_DURATION,
getContainer: () =>
props.staticGetContainer?.() || getPopupContainer.value?.() || document.body,
maxCount: props.maxCount,
onAllRemoved: props.onAllRemoved,
});
// ================================ Ref ================================
expose({
...api,
prefixCls,
hashId,
});
return holder;
},
});
// ==============================================================================
// == Hook ==
// ==============================================================================
let keyIndex = 0;
export function useInternalMessage(
messageConfig?: HolderProps,
): readonly [MessageInstance, () => VNode] {
const holderRef = shallowRef<HolderRef>(null);
// ================================ API ================================
const wrapAPI = computed(() => {
// Wrap with notification content
// >>> close
const close = (key: Key) => {
holderRef.value?.close(key);
};
// >>> Open
const open = (config: ArgsProps): MessageType => {
if (!holderRef.value) {
const fakeResult: any = () => {};
fakeResult.then = () => {};
return fakeResult;
}
const { open: originOpen, prefixCls, hashId } = holderRef.value;
const noticePrefixCls = `${prefixCls}-notice`;
const { content, icon, type, key, className, onClose, ...restConfig } = config;
let mergedKey: Key = key!;
if (mergedKey === undefined || mergedKey === null) {
keyIndex += 1;
mergedKey = `antd-message-${keyIndex}`;
}
return wrapPromiseFn(resolve => {
originOpen({
...restConfig,
key: mergedKey,
content: (
<PureContent prefixCls={prefixCls} type={type} icon={icon}>
{content}
</PureContent>
),
placement: 'top',
// @ts-ignore
class: classNames(type && `${noticePrefixCls}-${type}`, hashId, className),
onClose: () => {
onClose?.();
resolve();
},
});
// Return close function
return () => {
close(mergedKey);
};
});
};
// >>> destroy
const destroy = (key?: Key) => {
if (key !== undefined) {
close(key);
} else {
holderRef.value?.destroy();
}
};
const clone = {
open,
destroy,
} as MessageInstance;
const keys: NoticeType[] = ['info', 'success', 'warning', 'error', 'loading'];
keys.forEach(type => {
const typeOpen: TypeOpen = (jointContent, duration, onClose) => {
let config: ArgsProps;
if (jointContent && typeof jointContent === 'object' && 'content' in jointContent) {
config = jointContent;
} else {
config = {
content: jointContent as VNode,
};
}
// Params
let mergedDuration: number | undefined;
let mergedOnClose: VoidFunction | undefined;
if (typeof duration === 'function') {
mergedOnClose = duration;
} else {
mergedDuration = duration;
mergedOnClose = onClose;
}
const mergedConfig = {
onClose: mergedOnClose,
duration: mergedDuration,
...config,
type,
};
return open(mergedConfig);
};
clone[type] = typeOpen;
});
return clone;
});
// ============================== Return ===============================
return [
wrapAPI.value,
() => <Holder key="message-holder" {...messageConfig} ref={holderRef} />,
] as const;
}
export default function useMessage(messageConfig?: ConfigOptions) {
return useInternalMessage(messageConfig);
}

View File

@ -0,0 +1,109 @@
import { computed } from 'vue';
import useStyle from './style';
import useConfigInject from '../config-provider/hooks/useConfigInject';
import type { IconType } from './interface';
import Notice from '../vc-notification/Notice';
import classNames from '../_util/classNames';
import type { NoticeProps } from '../vc-notification/Notice';
import type { VueNode } from '../_util/type';
import {
CheckCircleOutlined,
CloseCircleOutlined,
CloseOutlined,
ExclamationCircleOutlined,
InfoCircleOutlined,
} from '@ant-design/icons-vue';
import { renderHelper } from '../_util/util';
export function getCloseIcon(prefixCls: string, closeIcon?: VueNode) {
return (
closeIcon || (
<span class={`${prefixCls}-close-x`}>
<CloseOutlined class={`${prefixCls}-close-icon`} />
</span>
)
);
}
export interface PureContentProps {
prefixCls: string;
icon?: VueNode;
message?: VueNode;
description?: VueNode;
btn?: VueNode;
type?: IconType;
}
const typeToIcon = {
success: CheckCircleOutlined,
info: InfoCircleOutlined,
error: CloseCircleOutlined,
warning: ExclamationCircleOutlined,
};
export function PureContent({
prefixCls,
icon,
type,
message,
description,
btn,
}: PureContentProps) {
let iconNode = null;
if (icon) {
iconNode = <span class={`${prefixCls}-icon`}>{renderHelper(icon)}</span>;
} else if (type) {
const Icon = typeToIcon[type];
iconNode = <Icon class={`${prefixCls}-icon ${prefixCls}-icon-${type}`} />;
}
return (
<div
class={classNames({
[`${prefixCls}-with-icon`]: iconNode,
})}
role="alert"
>
{iconNode}
<div class={`${prefixCls}-message`}>{message}</div>
<div class={`${prefixCls}-description`}>{description}</div>
{btn && <div class={`${prefixCls}-btn`}>{btn}</div>}
</div>
);
}
export interface PurePanelProps
extends Omit<NoticeProps, 'prefixCls' | 'eventKey'>,
Omit<PureContentProps, 'prefixCls' | 'children'> {
prefixCls?: string;
}
/** @private Internal Component. Do not use in your production. */
export default function PurePanel(props: PurePanelProps) {
const { getPrefixCls } = useConfigInject('notification', props);
const prefixCls = computed(() => props.prefixCls || getPrefixCls('notification'));
const noticePrefixCls = `${prefixCls.value}-notice`;
const [, hashId] = useStyle(prefixCls);
return (
<Notice
{...props}
prefixCls={prefixCls.value}
class={classNames(hashId.value, `${noticePrefixCls}-pure-panel`)}
noticeKey="pure"
duration={null}
closable={props.closable}
closeIcon={getCloseIcon(prefixCls.value, props.closeIcon)}
>
<PureContent
prefixCls={noticePrefixCls}
icon={props.icon}
type={props.type}
message={props.message}
description={props.description}
btn={props.btn}
/>
</Notice>
);
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,61 @@
<docs>
---
order: 10
title:
zh-CN: Hooks 调用推荐
en-US: Hooks Usage (Recommend)
---
## zh-CN
通过 `notification.useNotification` 创建支持读取 context `contextHolder`请注意我们推荐通过顶层注册的方式代替 `message` 静态方法因为静态方法无法消费上下文因而 ConfigProvider 的数据也不会生效
## en-US
Use `notification.useNotification` to get `contextHolder` with context accessible issue. Please note that, we recommend to use top level registration instead of `notification` static method, because static method cannot consume context, and ConfigProvider data will not work.
</docs>
<template>
<a-space>
<a-button type="primary" @click="() => open('topLeft')">
<RadiusUpleftOutlined />
topLeft
</a-button>
<a-button type="primary" @click="() => open('topRight')">
<RadiusUprightOutlined />
topRight
</a-button>
</a-space>
<a-divider />
<a-space>
<a-button type="primary" @click="() => open('bottomLeft')">
<RadiusBottomleftOutlined />
bottomLeft
</a-button>
<a-button type="primary" @click="() => open('bottomRight')">
<RadiusBottomrightOutlined />
bottomRight
</a-button>
</a-space>
<contextHolder />
</template>
<script lang="ts" setup>
import {
RadiusBottomleftOutlined,
RadiusBottomrightOutlined,
RadiusUpleftOutlined,
RadiusUprightOutlined,
} from '@ant-design/icons-vue';
import { NotificationPlacement, notification } from 'ant-design-vue';
const [api, contextHolder] = notification.useNotification();
const open = (placement: NotificationPlacement) => openNotification(placement);
const openNotification = (placement: NotificationPlacement) => {
api.info({
message: `Notification ${placement}`,
description:
'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
placement,
});
};
</script>

View File

@ -1,5 +1,6 @@
<template>
<demo-sort>
<hook />
<basic />
<duratioin />
<with-icon />
@ -12,6 +13,7 @@
</template>
<script lang="ts">
import Basic from './basic.vue';
import Hook from './hook.vue';
import Duratioin from './duration.vue';
import WithIcon from './with-icon.vue';
import CustomIcon from './custom-icon.vue';
@ -35,6 +37,7 @@ export default defineComponent({
CustomStyle,
Placement,
Update,
Hook,
},
setup() {
return {};

View File

@ -26,6 +26,7 @@ To display a notification message at any of the four corners of the viewport. Ty
- `notification.open(config)`
- `notification.close(key: String)`
- `notification.destroy()`
- `notification.useNotification()`
The properties of config are as follows:
@ -74,3 +75,33 @@ notification.config({
| placement | Position of Notification, can be one of `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` | |
| rtl | Whether to enable RTL mode | boolean | false | |
| top | Distance from the top of the viewport, when `placement` is `topRight` or `topLeft` (unit: pixels). | string | `24px` | |
## FAQ
### Why I can not access context, Pinia, ConfigProvider `locale/prefixCls/theme` in notification?
antdv will dynamic create Vue instance by `Vue.render` when call notification methods. Whose context is different with origin code located context.
When you need context info (like ConfigProvider context), you can use `notification.useNotification` to get `api` instance and `contextHolder` node. And put it in your children:
```html
<template>
<contextHolder />
<!-- <component :is='contextHolder'/> -->
</template>
<script setup>
import { notification } from 'ant-design-vue';
const [notificationApi, contextHolder] = notification.useNotification();
notificationApi.open({
// ...
});
</script>
```
**Note:** You must insert `contextHolder` into your children with hooks. You can use origin method if you do not need context connection.
> [App Package Component](/components/app) can be used to simplify the problem of `useNotification` and other methods that need to manually implant contextHolder.
### How to set static methods prefixCls
You can config with [`ConfigProvider.config`](/components/config-provider#configproviderconfig-4130)

View File

@ -11,6 +11,8 @@ import { globalConfig } from '../config-provider';
import type { NotificationInstance as VCNotificationInstance } from '../vc-notification/Notification';
import classNames from '../_util/classNames';
import useStyle from './style';
import useNotification from './useNotification';
export type NotificationPlacement =
| 'top'
| 'topLeft'
@ -284,6 +286,7 @@ iconTypes.forEach(type => {
});
api.warn = api.warning;
api.useNotification = useNotification;
export interface NotificationInstance {
success(args: NotificationArgsProps): void;
@ -298,6 +301,7 @@ export interface NotificationApi extends NotificationInstance {
close(key: string): void;
config(options: ConfigProps): void;
destroy(): void;
useNotification: typeof useNotification;
}
/** @private test Only function. Not work on production */

View File

@ -27,6 +27,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*9hTIToR-3YYAAA
- `notification.open(config)`
- `notification.close(key: String)`
- `notification.destroy()`
- `notification.useNotification()`
config 参数如下:
@ -75,3 +76,33 @@ notification.config({
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | |
| rtl | 是否开启 RTL 模式 | boolean | false | 3.0 |
| top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` | |
## FAQ
### 为什么 notification 不能获取 context、Pinia 的内容和 ConfigProvider 的 `locale/prefixCls/theme` 等配置?
直接调用 notification 方法antdv 会通过 `Vue.render` 动态创建新的 Vue 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
当你需要 context 信息(例如 ConfigProvider 配置的内容)时,可以通过 `notification.useNotification` 方法会返回 `api` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可:
```html
<template>
<contextHolder />
<!-- <component :is='contextHolder'/> -->
</template>
<script setup>
import { notification } from 'ant-design-vue';
const [notificationApi, contextHolder] = notification.useNotification();
notificationApi.open({
// ...
});
</script>
```
**异同**:通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。
> 可通过 [App 包裹组件](/components/app-cn) 简化 `useNotification` 等方法需要手动植入 contextHolder 的问题。
### 静态方法如何设置 prefixCls
你可以通过 [`ConfigProvider.config`](/components/config-provider-cn#configproviderconfig-4130) 进行设置。

View File

@ -0,0 +1,61 @@
import type { CSSProperties } from 'vue';
import type { Key, VueNode } from '../_util/type';
export type NotificationPlacement =
| 'top'
| 'topLeft'
| 'topRight'
| 'bottom'
| 'bottomLeft'
| 'bottomRight';
export type IconType = 'success' | 'info' | 'error' | 'warning';
export interface ArgsProps {
message: VueNode;
description?: VueNode;
btn?: VueNode;
key?: Key;
onClose?: () => void;
duration?: number | null;
icon?: VueNode;
placement?: NotificationPlacement;
style?: CSSProperties;
className?: string;
readonly type?: IconType;
onClick?: () => void;
closeIcon?: VueNode;
}
type StaticFn = (args: ArgsProps) => void;
export interface NotificationInstance {
success: StaticFn;
error: StaticFn;
info: StaticFn;
warning: StaticFn;
open: StaticFn;
destroy(key?: Key): void;
}
export interface GlobalConfigProps {
top?: number;
bottom?: number;
duration?: number;
prefixCls?: string;
getContainer?: () => HTMLElement;
placement?: NotificationPlacement;
closeIcon?: VueNode;
rtl?: boolean;
maxCount?: number;
}
export interface NotificationConfig {
top?: number;
bottom?: number;
prefixCls?: string;
getContainer?: () => HTMLElement;
placement?: NotificationPlacement;
maxCount?: number;
rtl?: boolean;
}

View File

@ -0,0 +1,154 @@
import type { VNode } from 'vue';
import { shallowRef, computed, defineComponent } from 'vue';
import { useNotification as useVcNotification } from '../vc-notification';
import type { NotificationAPI } from '../vc-notification';
import type {
NotificationInstance,
ArgsProps,
NotificationPlacement,
NotificationConfig,
} from './interface';
import useStyle from './style';
import { getCloseIcon, PureContent } from './PurePanel';
import { getMotion, getPlacementStyle } from './util';
import useConfigInject from '../config-provider/hooks/useConfigInject';
import classNames from '../_util/classNames';
import type { Key } from '../_util/type';
const DEFAULT_OFFSET = 24;
const DEFAULT_DURATION = 4.5;
// ==============================================================================
// == Holder ==
// ==============================================================================
type HolderProps = NotificationConfig & {
onAllRemoved?: VoidFunction;
getPopupContainer?: () => HTMLElement;
};
interface HolderRef extends NotificationAPI {
prefixCls: string;
hashId: string;
}
const Holder = defineComponent({
name: 'Holder',
inheritAttrs: false,
props: ['prefixCls', 'class', 'type', 'icon', 'content', 'onAllRemoved'],
setup(props: HolderProps, { expose }) {
const { getPrefixCls, getPopupContainer } = useConfigInject('notification', props);
const prefixCls = computed(() => props.prefixCls || getPrefixCls('notification'));
// =============================== Style ===============================
const getStyles = (placement: NotificationPlacement) =>
getPlacementStyle(placement, props.top ?? DEFAULT_OFFSET, props.bottom ?? DEFAULT_OFFSET);
// Style
const [, hashId] = useStyle(prefixCls);
const getClassName = () => classNames(hashId.value, { [`${prefixCls.value}-rtl`]: props.rtl });
// ============================== Motion ===============================
const getNotificationMotion = () => getMotion(prefixCls.value);
// ============================== Origin ===============================
const [api, holder] = useVcNotification({
prefixCls: prefixCls.value,
getStyles,
getClassName,
motion: getNotificationMotion,
closable: true,
closeIcon: getCloseIcon(prefixCls.value),
duration: DEFAULT_DURATION,
getContainer: () =>
props.getPopupContainer?.() || getPopupContainer.value?.() || document.body,
maxCount: props.maxCount,
hashId: hashId.value,
onAllRemoved: props.onAllRemoved,
});
// ================================ Ref ================================
expose({
...api,
prefixCls: prefixCls.value,
hashId,
});
return holder;
},
});
// ==============================================================================
// == Hook ==
// ==============================================================================
export function useInternalNotification(
notificationConfig?: HolderProps,
): readonly [NotificationInstance, () => VNode] {
const holderRef = shallowRef<HolderRef>(null);
// ================================ API ================================
const wrapAPI = computed(() => {
// Wrap with notification content
// >>> Open
const open = (config: ArgsProps) => {
if (!holderRef.value) {
return;
}
const { open: originOpen, prefixCls, hashId } = holderRef.value;
const noticePrefixCls = `${prefixCls}-notice`;
const { message, description, icon, type, btn, className, ...restConfig } = config;
return originOpen({
placement: 'topRight',
...restConfig,
content: (
<PureContent
prefixCls={noticePrefixCls}
icon={icon}
type={type}
message={message}
description={description}
btn={btn}
/>
),
// @ts-ignore
class: classNames(type && `${noticePrefixCls}-${type}`, hashId, className),
});
};
// >>> destroy
const destroy = (key?: Key) => {
if (key !== undefined) {
holderRef.value?.close(key);
} else {
holderRef.value?.destroy();
}
};
const clone = {
open,
destroy,
} as NotificationInstance;
const keys = ['success', 'info', 'warning', 'error'] as const;
keys.forEach(type => {
clone[type] = config =>
open({
...config,
type,
});
});
return clone;
});
// ============================== Return ===============================
return [
wrapAPI.value,
() => <Holder key="notification-holder" {...notificationConfig} ref={holderRef} />,
] as const;
}
export default function useNotification(notificationConfig?: NotificationConfig) {
return useInternalNotification(notificationConfig);
}

View File

@ -0,0 +1,73 @@
import type { CSSProperties } from 'vue';
import type { NotificationPlacement } from './interface';
import type { CSSMotionProps } from '../_util/transition';
export function getPlacementStyle(
placement: NotificationPlacement,
top: number | string,
bottom: number | string,
) {
let style: CSSProperties;
top = typeof top === 'number' ? `${top}px` : top;
bottom = typeof bottom === 'number' ? `${bottom}px` : bottom;
switch (placement) {
case 'top':
style = {
left: '50%',
transform: 'translateX(-50%)',
right: 'auto',
top,
bottom: 'auto',
};
break;
case 'topLeft':
style = {
left: 0,
top,
bottom: 'auto',
};
break;
case 'topRight':
style = {
right: 0,
top,
bottom: 'auto',
};
break;
case 'bottom':
style = {
left: '50%',
transform: 'translateX(-50%)',
right: 'auto',
top: 'auto',
bottom,
};
break;
case 'bottomLeft':
style = {
left: 0,
top: 'auto',
bottom,
};
break;
default:
style = {
right: 0,
top: 'auto',
bottom,
};
break;
}
return style;
}
export function getMotion(prefixCls: string): CSSMotionProps {
return {
name: `${prefixCls}-fade`,
};
}

View File

@ -0,0 +1,230 @@
import type { CSSProperties } from 'vue';
import { watch, computed, defineComponent, ref, TransitionGroup } from 'vue';
import type { NoticeProps } from './Notice';
import Notice from './Notice';
import type { CSSMotionProps } from '../_util/transition';
import { getTransitionGroupProps } from '../_util/transition';
import type { Key, VueNode } from '../_util/type';
import classNames from '../_util/classNames';
import Portal from '../_util/Portal';
let seed = 0;
const now = Date.now();
export function getUuid() {
const id = seed;
seed += 1;
return `rcNotification_${now}_${id}`;
}
export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
export interface OpenConfig extends NoticeProps {
key: Key;
placement?: Placement;
content?: VueNode;
duration?: number | null;
}
export type Placements = Partial<Record<Placement, OpenConfig[]>>;
export interface NoticeContent extends Omit<NoticeProps, 'prefixCls' | 'noticeKey' | 'onClose'> {
prefixCls?: string;
key?: Key;
updateMark?: string;
content?: any;
onClose?: () => void;
style?: CSSProperties;
class?: String;
placement?: Placement;
}
export type NoticeFunc = (noticeProps: NoticeContent) => void;
export type HolderReadyCallback = (
div: HTMLDivElement,
noticeProps: NoticeProps & { key: Key },
) => void;
export interface NotificationInstance {
notice: NoticeFunc;
removeNotice: (key: Key) => void;
destroy: () => void;
add: (noticeProps: NoticeContent) => void;
component: Notification;
}
export interface HookNotificationProps {
prefixCls?: string;
transitionName?: string;
animation?: string | CSSMotionProps | ((placement?: Placement) => CSSMotionProps);
maxCount?: number;
closeIcon?: any;
hashId?: string;
// Hook Notification
remove: (key: Key) => void;
notices: NotificationState;
getStyles?: (placement?: Placement) => CSSProperties;
getClassName?: (placement?: Placement) => string;
onAllRemoved?: VoidFunction;
getContainer?: () => HTMLElement;
}
type NotificationState = {
notice: NoticeContent & {
userPassKey?: Key;
};
holderCallback?: HolderReadyCallback;
}[];
const Notification = defineComponent<HookNotificationProps>({
name: 'HookNotification',
inheritAttrs: false,
props: [
'prefixCls',
'transitionName',
'animation',
'maxCount',
'closeIcon',
'hashId',
'remove',
'notices',
'getStyles',
'getClassName',
'onAllRemoved',
'getContainer',
] as any,
setup(props, { attrs, slots }) {
const hookRefs = new Map<Key, HTMLDivElement>();
const notices = computed(() => props.notices);
const transitionProps = computed(() => {
let name = props.transitionName;
if (!name && props.animation) {
switch (typeof props.animation) {
case 'string':
name = props.animation;
break;
case 'function':
name = props.animation().name;
break;
case 'object':
name = props.animation.name;
break;
default:
name = `${props.prefixCls}-fade`;
break;
}
}
return getTransitionGroupProps(name);
});
const remove = (key: Key) => props.remove(key);
const placements = ref({} as Record<Placement, NotificationState>);
watch(notices, () => {
const nextPlacements = {} as any;
// init placements with animation
Object.keys(placements.value).forEach(placement => {
nextPlacements[placement] = [];
});
props.notices.forEach(config => {
const { placement = 'topRight' } = config.notice;
if (placement) {
nextPlacements[placement] = nextPlacements[placement] || [];
nextPlacements[placement].push(config);
}
});
placements.value = nextPlacements;
});
const placementList = computed(() => Object.keys(placements.value) as Placement[]);
return () => {
const { prefixCls, closeIcon = slots.closeIcon?.({ prefixCls }) } = props;
const noticeNodes = placementList.value.map(placement => {
const noticesForPlacement = placements.value[placement];
const classes = props.getClassName?.(placement);
const styles = props.getStyles?.(placement);
const noticeNodesForPlacement = noticesForPlacement.map(
({ notice, holderCallback }, index) => {
const updateMark = index === notices.value.length - 1 ? notice.updateMark : undefined;
const { key, userPassKey } = notice;
const { content } = notice;
const noticeProps = {
prefixCls,
closeIcon: typeof closeIcon === 'function' ? closeIcon({ prefixCls }) : closeIcon,
...(notice as any),
...notice.props,
key,
noticeKey: userPassKey || key,
updateMark,
onClose: (noticeKey: Key) => {
remove(noticeKey);
notice.onClose?.();
},
onClick: notice.onClick,
};
if (holderCallback) {
return (
<div
key={key}
class={`${prefixCls}-hook-holder`}
ref={(div: HTMLDivElement) => {
if (typeof key === 'undefined') {
return;
}
if (div) {
hookRefs.set(key, div);
holderCallback(div, noticeProps);
} else {
hookRefs.delete(key);
}
}}
/>
);
}
return (
<Notice {...noticeProps} class={classNames(noticeProps.class, props.hashId)}>
{typeof content === 'function' ? content({ prefixCls }) : content}
</Notice>
);
},
);
const className = {
[prefixCls]: 1,
[`${prefixCls}-${placement}`]: 1,
[attrs.class as string]: !!attrs.class,
[props.hashId]: true,
[classes]: !!classes,
};
function onAfterLeave() {
if (noticesForPlacement.length > 0) {
return;
}
Reflect.deleteProperty(placements.value, placement);
props.onAllRemoved?.();
}
return (
<div
key={placement}
class={className}
style={
(attrs.style as CSSProperties) ||
styles || {
top: '65px',
left: '50%',
}
}
>
<TransitionGroup tag="div" {...transitionProps.value} onAfterLeave={onAfterLeave}>
{noticeNodesForPlacement}
</TransitionGroup>
</div>
);
});
return <Portal getContainer={props.getContainer}>{noticeNodes}</Portal>;
};
},
});
export default Notification;

View File

@ -1,7 +1,8 @@
import { getTransitionGroupProps } from '../_util/transition';
import type { Key } from '../_util/type';
import type { Key, VueNode } from '../_util/type';
import type { CSSProperties } from 'vue';
import {
shallowRef,
createVNode,
computed,
defineComponent,
@ -33,6 +34,14 @@ export interface NoticeContent extends Omit<NoticeProps, 'prefixCls' | 'noticeKe
style?: CSSProperties;
class?: String;
}
export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
export interface OpenConfig extends NoticeProps {
key: Key;
placement?: Placement;
content?: VueNode;
duration?: number | null;
}
export type NoticeFunc = (noticeProps: NoticeContent) => void;
export type HolderReadyCallback = (
@ -220,7 +229,7 @@ Notification.newInstance = function newNotificationInstance(properties, callback
compatConfig: { MODE: 3 },
name: 'NotificationWrapper',
setup(_props, { attrs }) {
const notiRef = ref();
const notiRef = shallowRef();
const prefixCls = computed(() => globalConfigForApi.getPrefixCls(name, customizePrefixCls));
const [, hashId] = useStyle(prefixCls);
onMounted(() => {

View File

@ -1,4 +1,5 @@
// based on rc-notification 4.5.7
import Notification from './Notification';
import useNotification from './useNotification';
import type { NotificationAPI } from './useNotification';
export { useNotification, NotificationAPI };
export default Notification;

View File

@ -0,0 +1,184 @@
import type { CSSProperties } from 'vue';
import { shallowRef, watch, ref, computed } from 'vue';
import HookNotification, { getUuid } from './HookNotification';
import type { NotificationInstance, OpenConfig, Placement } from './Notification';
import type { CSSMotionProps } from '../_util/transition';
import type { Key, VueNode } from '../_util/type';
import type { HolderReadyCallback, NoticeContent } from './HookNotification';
const defaultGetContainer = () => document.body;
type OptionalConfig = Partial<OpenConfig>;
export interface NotificationConfig {
prefixCls?: string;
/** Customize container. It will repeat call which means you should return same container element. */
getContainer?: () => HTMLElement;
motion?: CSSMotionProps | ((placement?: Placement) => CSSMotionProps);
closeIcon?: VueNode;
closable?: boolean;
maxCount?: number;
duration?: number;
/** @private. Config for notification holder style. Safe to remove if refactor */
getClassName?: (placement?: Placement) => string;
/** @private. Config for notification holder style. Safe to remove if refactor */
getStyles?: (placement?: Placement) => CSSProperties;
/** @private Trigger when all the notification closed. */
onAllRemoved?: VoidFunction;
hashId?: string;
}
export interface NotificationAPI {
open: (config: OptionalConfig) => void;
close: (key: Key) => void;
destroy: () => void;
}
interface OpenTask {
type: 'open';
config: OpenConfig;
}
interface CloseTask {
type: 'close';
key: Key;
}
interface DestroyTask {
type: 'destroy';
}
type Task = OpenTask | CloseTask | DestroyTask;
let uniqueKey = 0;
function mergeConfig<T>(...objList: Partial<T>[]): T {
const clone: T = {} as T;
objList.forEach(obj => {
if (obj) {
Object.keys(obj).forEach(key => {
const val = obj[key];
if (val !== undefined) {
clone[key] = val;
}
});
}
});
return clone;
}
export default function useNotification(rootConfig: NotificationConfig = {}) {
const {
getContainer = defaultGetContainer,
motion,
prefixCls,
maxCount,
getClassName,
getStyles,
onAllRemoved,
...shareConfig
} = rootConfig;
const notices = ref([]);
const notificationsRef = shallowRef<NotificationInstance>();
const add = (originNotice: NoticeContent, holderCallback?: HolderReadyCallback) => {
const key = originNotice.key || getUuid();
const notice: NoticeContent & { key: Key; userPassKey?: Key } = {
...originNotice,
key,
};
const noticeIndex = notices.value.map(v => v.notice.key).indexOf(key);
const updatedNotices = notices.value.concat();
if (noticeIndex !== -1) {
updatedNotices.splice(noticeIndex, 1, { notice, holderCallback } as any);
} else {
if (maxCount && notices.value.length >= maxCount) {
notice.key = updatedNotices[0].notice.key as Key;
notice.updateMark = getUuid();
notice.userPassKey = key;
updatedNotices.shift();
}
updatedNotices.push({ notice, holderCallback } as any);
}
notices.value = updatedNotices;
};
const removeNotice = (removeKey: Key) => {
notices.value = notices.value.filter(({ notice: { key, userPassKey } }) => {
const mergedKey = userPassKey || key;
return mergedKey !== removeKey;
});
};
const destroy = () => {
notices.value = [];
};
const contextHolder = computed(() => (
<HookNotification
ref={notificationsRef}
prefixCls={prefixCls}
maxCount={maxCount}
notices={notices.value}
remove={removeNotice}
getClassName={getClassName}
getStyles={getStyles}
animation={motion}
hashId={rootConfig.hashId}
onAllRemoved={onAllRemoved}
getContainer={getContainer}
></HookNotification>
));
const taskQueue = ref([] as Task[]);
// ========================= Refs =========================
const api = computed(() => {
return {
open: (config: OpenConfig) => {
const mergedConfig = mergeConfig(shareConfig, config);
//@ts-ignore
if (mergedConfig.key === null || mergedConfig.key === undefined) {
//@ts-ignore
mergedConfig.key = `vc-notification-${uniqueKey}`;
uniqueKey += 1;
}
taskQueue.value = [...taskQueue.value, { type: 'open', config: mergedConfig as any }];
},
close: key => {
taskQueue.value = [...taskQueue.value, { type: 'close', key }];
},
destroy: () => {
taskQueue.value = [...taskQueue.value, { type: 'destroy' }];
},
};
});
// ======================== Effect ========================
watch(taskQueue, () => {
// Flush task when node ready
if (taskQueue.value.length) {
taskQueue.value.forEach(task => {
switch (task.type) {
case 'open':
// @ts-ignore
add(task.config);
break;
case 'close':
removeNotice(task.key);
break;
case 'destroy':
destroy();
break;
}
});
taskQueue.value = [];
}
});
// ======================== Return ========================
return [api.value, () => contextHolder.value] as const;
}