diff --git a/docs/en-US/component/message.md b/docs/en-US/component/message.md
index 06f398ee8c..33b86a325a 100644
--- a/docs/en-US/component/message.md
+++ b/docs/en-US/component/message.md
@@ -85,6 +85,24 @@ import { ElMessage } from 'element-plus'
In this case you should call `ElMessage(options)`. We have also registered methods for different types, e.g. `ElMessage.success(options)`. You can call `ElMessage.closeAll()` to manually close all the instances.
+## App context inheritance > 2.0.2
+
+Now message accepts a `context` as second parameter of the message constructor which allows you to inject current app's context to message which allows you to inherit all the properties of the app.
+
+You can use it like this:
+
+:::tip
+
+If you globally registered ElMessage component, it will automatically inherit your app context.
+
+:::
+
+```ts
+import { ElMessage } from 'element-plus'
+
+ElMessage({}, app._context)
+```
+
## Options
| Attribute | Description | Type | Accepted Values | Default |
diff --git a/packages/components/message/__tests__/message-manager.spec.ts b/packages/components/message/__tests__/message-manager.spec.tsx
similarity index 86%
rename from packages/components/message/__tests__/message-manager.spec.ts
rename to packages/components/message/__tests__/message-manager.spec.tsx
index e93ff6d5f4..f03ddf2fd4 100644
--- a/packages/components/message/__tests__/message-manager.spec.ts
+++ b/packages/components/message/__tests__/message-manager.spec.tsx
@@ -1,6 +1,7 @@
import { nextTick } from 'vue'
import { getStyle } from '@element-plus/utils'
import { rAF } from '@element-plus/test-utils/tick'
+import { ElMessage } from '..'
import Message from '../src/message-method'
jest.useFakeTimers()
@@ -115,4 +116,21 @@ describe('Message on command', () => {
await nextTick()
expect(htmlElement.querySelector(selector)).toBeFalsy()
})
+
+ describe('context inheritance', () => {
+ it('should globally inherit context correctly', () => {
+ expect(ElMessage._context).toBe(null)
+ const testContext = {
+ config: {
+ globalProperties: {},
+ },
+ _context: {},
+ }
+ ElMessage.install?.(testContext as any)
+ expect(ElMessage._context).not.toBe(null)
+ expect(ElMessage._context).toBe(testContext._context)
+ // clean up
+ ElMessage._context = null
+ })
+ })
})
diff --git a/packages/components/message/src/message-method.ts b/packages/components/message/src/message-method.ts
index fe6a7836fc..5176e49c52 100644
--- a/packages/components/message/src/message-method.ts
+++ b/packages/components/message/src/message-method.ts
@@ -1,125 +1,136 @@
import { createVNode, render } from 'vue'
import { isClient } from '@vueuse/core'
-import { isVNode, isNumber, debugWarn } from '@element-plus/utils'
+import {
+ isVNode,
+ isNumber,
+ isObject,
+ isString,
+ debugWarn,
+} from '@element-plus/utils'
import { useZIndex } from '@element-plus/hooks'
import { messageConfig } from '@element-plus/components/config-provider/src/config-provider'
import MessageConstructor from './message.vue'
import { messageTypes } from './message'
+import type { AppContext, ComponentPublicInstance, VNode } from 'vue'
import type { Message, MessageFn, MessageQueue, MessageProps } from './message'
-import type { ComponentPublicInstance, VNode } from 'vue'
const instances: MessageQueue = []
let seed = 1
// TODO: Since Notify.ts is basically the same like this file. So we could do some encapsulation against them to reduce code duplication.
-const message: MessageFn & Partial = function (options = {}) {
- if (!isClient) return { close: () => undefined }
- if (isNumber(messageConfig.max) && instances.length >= messageConfig.max) {
- return { close: () => undefined }
- }
+const message: MessageFn & Partial & { _context: AppContext | null } =
+ function (options = {}, context?: AppContext | null) {
+ if (!isClient) return { close: () => undefined }
+ if (isNumber(messageConfig.max) && instances.length >= messageConfig.max) {
+ return { close: () => undefined }
+ }
- if (
- !isVNode(options) &&
- typeof options === 'object' &&
- options.grouping &&
- !isVNode(options.message) &&
- instances.length
- ) {
- const tempVm: any = instances.find(
- (item) =>
- `${item.vm.props?.message ?? ''}` ===
- `${(options as any).message ?? ''}`
- )
- if (tempVm) {
- tempVm.vm.component!.props.repeatNum += 1
- tempVm.vm.component!.props.type = options?.type
- return {
- close: () =>
- ((
- vm.component!.proxy as ComponentPublicInstance<{ visible: boolean }>
- ).visible = false),
+ if (
+ !isVNode(options) &&
+ isObject(options) &&
+ options.grouping &&
+ !isVNode(options.message) &&
+ instances.length
+ ) {
+ const tempVm: any = instances.find(
+ (item) =>
+ `${item.vm.props?.message ?? ''}` ===
+ `${(options as any).message ?? ''}`
+ )
+ if (tempVm) {
+ tempVm.vm.component!.props.repeatNum += 1
+ tempVm.vm.component!.props.type = options?.type
+ return {
+ close: () =>
+ ((
+ vm.component!.proxy as ComponentPublicInstance<{
+ visible: boolean
+ }>
+ ).visible = false),
+ }
}
}
+
+ if (isString(options) || isVNode(options)) {
+ options = { message: options }
+ }
+
+ let verticalOffset = options.offset || 20
+ instances.forEach(({ vm }) => {
+ verticalOffset += (vm.el?.offsetHeight || 0) + 16
+ })
+ verticalOffset += 16
+
+ const { nextZIndex } = useZIndex()
+
+ const id = `message_${seed++}`
+ const userOnClose = options.onClose
+ const props: Partial = {
+ zIndex: nextZIndex(),
+ offset: verticalOffset,
+ ...options,
+ id,
+ onClose: () => {
+ close(id, userOnClose)
+ },
+ }
+
+ let appendTo: HTMLElement | null = document.body
+ if (options.appendTo instanceof HTMLElement) {
+ appendTo = options.appendTo
+ } else if (isString(options.appendTo)) {
+ appendTo = document.querySelector(options.appendTo)
+ }
+ // should fallback to default value with a warning
+ if (!(appendTo instanceof HTMLElement)) {
+ debugWarn(
+ 'ElMessage',
+ 'the appendTo option is not an HTMLElement. Falling back to document.body.'
+ )
+ appendTo = document.body
+ }
+
+ const container = document.createElement('div')
+
+ container.className = `container_${id}`
+
+ const messageContent = props.message
+ const vm = createVNode(
+ MessageConstructor,
+ props,
+ isVNode(messageContent) ? { default: () => messageContent } : null
+ )
+
+ vm.appContext = context || message._context
+
+ // clean message element preventing mem leak
+ vm.props!.onDestroy = () => {
+ render(null, container)
+ // since the element is destroy, then the VNode should be collected by GC as well
+ // we do not want cause any mem leak because we have returned vm as a reference to users
+ // so that we manually set it to false.
+ }
+
+ render(vm, container)
+ // instances will remove this item when close function gets called. So we do not need to worry about it.
+ instances.push({ vm })
+ appendTo.appendChild(container.firstElementChild!)
+
+ return {
+ // instead of calling the onClose function directly, setting this value so that we can have the full lifecycle
+ // for out component, so that all closing steps will not be skipped.
+ close: () =>
+ ((
+ vm.component!.proxy as ComponentPublicInstance<{ visible: boolean }>
+ ).visible = false),
+ }
}
- if (typeof options === 'string' || isVNode(options)) {
- options = { message: options }
- }
-
- let verticalOffset = options.offset || 20
- instances.forEach(({ vm }) => {
- verticalOffset += (vm.el?.offsetHeight || 0) + 16
- })
- verticalOffset += 16
-
- const { nextZIndex } = useZIndex()
-
- const id = `message_${seed++}`
- const userOnClose = options.onClose
- const props: Partial = {
- zIndex: nextZIndex(),
- offset: verticalOffset,
- ...options,
- id,
- onClose: () => {
- close(id, userOnClose)
- },
- }
-
- let appendTo: HTMLElement | null = document.body
- if (options.appendTo instanceof HTMLElement) {
- appendTo = options.appendTo
- } else if (typeof options.appendTo === 'string') {
- appendTo = document.querySelector(options.appendTo)
- }
- // should fallback to default value with a warning
- if (!(appendTo instanceof HTMLElement)) {
- debugWarn(
- 'ElMessage',
- 'the appendTo option is not an HTMLElement. Falling back to document.body.'
- )
- appendTo = document.body
- }
-
- const container = document.createElement('div')
-
- container.className = `container_${id}`
-
- const message = props.message
- const vm = createVNode(
- MessageConstructor,
- props,
- isVNode(props.message) ? { default: () => message } : null
- )
-
- // clean message element preventing mem leak
- vm.props!.onDestroy = () => {
- render(null, container)
- // since the element is destroy, then the VNode should be collected by GC as well
- // we do not want cause any mem leak because we have returned vm as a reference to users
- // so that we manually set it to false.
- }
-
- render(vm, container)
- // instances will remove this item when close function gets called. So we do not need to worry about it.
- instances.push({ vm })
- appendTo.appendChild(container.firstElementChild!)
-
- return {
- // instead of calling the onClose function directly, setting this value so that we can have the full lifecycle
- // for out component, so that all closing steps will not be skipped.
- close: () =>
- ((
- vm.component!.proxy as ComponentPublicInstance<{ visible: boolean }>
- ).visible = false),
- }
-}
-
messageTypes.forEach((type) => {
message[type] = (options = {}) => {
- if (typeof options === 'string' || isVNode(options)) {
+ if (isString(options) || isVNode(options)) {
options = {
message: options,
}
@@ -161,5 +172,6 @@ export function closeAll(): void {
}
message.closeAll = closeAll
+message._context = null
export default message as Message
diff --git a/packages/components/message/src/message.ts b/packages/components/message/src/message.ts
index e26136e706..50eb2af7cc 100644
--- a/packages/components/message/src/message.ts
+++ b/packages/components/message/src/message.ts
@@ -1,5 +1,5 @@
import { buildProps, definePropType, iconPropType } from '@element-plus/utils'
-import type { VNode, ExtractPropTypes } from 'vue'
+import type { VNode, ExtractPropTypes, AppContext } from 'vue'
export const messageTypes = ['success', 'info', 'warning', 'error'] as const
@@ -85,10 +85,16 @@ export interface MessageHandle {
export type MessageParams = Partial | string | VNode
export type MessageParamsTyped = Partial | string | VNode
-export type MessageFn = ((options?: MessageParams) => MessageHandle) & {
+export type MessageFn = ((
+ options?: MessageParams,
+ appContext?: null | AppContext
+) => MessageHandle) & {
closeAll(): void
}
-export type MessageTypedFn = (options?: MessageParamsTyped) => MessageHandle
+export type MessageTypedFn = (
+ options?: MessageParamsTyped,
+ appContext?: null | AppContext
+) => MessageHandle
export interface Message extends MessageFn {
success: MessageTypedFn
diff --git a/packages/utils/vue/install.ts b/packages/utils/vue/install.ts
index d16feec144..3405635d66 100644
--- a/packages/utils/vue/install.ts
+++ b/packages/utils/vue/install.ts
@@ -1,5 +1,7 @@
import { NOOP } from '@vue/shared'
-import type { SFCWithInstall } from './typescript'
+
+import type { App } from 'vue'
+import type { SFCWithInstall, SFCInstallWithContext } from './typescript'
export const withInstall = >(
main: T,
@@ -20,11 +22,12 @@ export const withInstall = >(
}
export const withInstallFunction = (fn: T, name: string) => {
- ;(fn as SFCWithInstall).install = (app) => {
+ ;(fn as SFCWithInstall).install = (app: App) => {
+ ;(fn as SFCInstallWithContext)._context = app._context
app.config.globalProperties[name] = fn
}
- return fn as SFCWithInstall
+ return fn as SFCInstallWithContext
}
export const withNoopInstall = (component: T) => {
diff --git a/packages/utils/vue/typescript.ts b/packages/utils/vue/typescript.ts
index 0b178dcfff..1ea4353d00 100644
--- a/packages/utils/vue/typescript.ts
+++ b/packages/utils/vue/typescript.ts
@@ -1,3 +1,7 @@
-import type { Plugin } from 'vue'
+import type { AppContext, Plugin } from 'vue'
export type SFCWithInstall = T & Plugin
+
+export type SFCInstallWithContext = SFCWithInstall & {
+ _context: AppContext | null
+}