mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-02 03:08:21 +08:00
feat(components): [message] add vue context for message component (#6259)
This commit is contained in:
parent
cd0f01c034
commit
6aa69126b8
@ -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 <el-tag>> 2.0.2</el-tag>
|
||||
|
||||
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 |
|
||||
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
@ -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<Message> = function (options = {}) {
|
||||
if (!isClient) return { close: () => undefined }
|
||||
if (isNumber(messageConfig.max) && instances.length >= messageConfig.max) {
|
||||
return { close: () => undefined }
|
||||
}
|
||||
const message: MessageFn & Partial<Message> & { _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<MessageProps> = {
|
||||
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<MessageProps> = {
|
||||
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
|
||||
|
@ -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<MessageOptions> | string | VNode
|
||||
export type MessageParamsTyped = Partial<MessageOptionsTyped> | 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
|
||||
|
@ -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 = <T, E extends Record<string, any>>(
|
||||
main: T,
|
||||
@ -20,11 +22,12 @@ export const withInstall = <T, E extends Record<string, any>>(
|
||||
}
|
||||
|
||||
export const withInstallFunction = <T>(fn: T, name: string) => {
|
||||
;(fn as SFCWithInstall<T>).install = (app) => {
|
||||
;(fn as SFCWithInstall<T>).install = (app: App) => {
|
||||
;(fn as SFCInstallWithContext<T>)._context = app._context
|
||||
app.config.globalProperties[name] = fn
|
||||
}
|
||||
|
||||
return fn as SFCWithInstall<T>
|
||||
return fn as SFCInstallWithContext<T>
|
||||
}
|
||||
|
||||
export const withNoopInstall = <T>(component: T) => {
|
||||
|
@ -1,3 +1,7 @@
|
||||
import type { Plugin } from 'vue'
|
||||
import type { AppContext, Plugin } from 'vue'
|
||||
|
||||
export type SFCWithInstall<T> = T & Plugin
|
||||
|
||||
export type SFCInstallWithContext<T> = SFCWithInstall<T> & {
|
||||
_context: AppContext | null
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user