feat(components): [message] add vue context for message component (#6259)

This commit is contained in:
JeremyWuuuuu 2022-02-24 11:24:34 +08:00 committed by GitHub
parent cd0f01c034
commit 6aa69126b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 169 additions and 108 deletions

View File

@ -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 |

View File

@ -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
})
})
})

View File

@ -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

View File

@ -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

View File

@ -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) => {

View File

@ -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
}