From 6aa69126b8fa491ad98006c60fa540b8843a7fd2 Mon Sep 17 00:00:00 2001 From: JeremyWuuuuu <15975785+JeremyWuuuuu@users.noreply.github.com> Date: Thu, 24 Feb 2022 11:24:34 +0800 Subject: [PATCH] feat(components): [message] add vue context for message component (#6259) --- docs/en-US/component/message.md | 18 ++ ...nager.spec.ts => message-manager.spec.tsx} | 18 ++ .../components/message/src/message-method.ts | 214 +++++++++--------- packages/components/message/src/message.ts | 12 +- packages/utils/vue/install.ts | 9 +- packages/utils/vue/typescript.ts | 6 +- 6 files changed, 169 insertions(+), 108 deletions(-) rename packages/components/message/__tests__/{message-manager.spec.ts => message-manager.spec.tsx} (86%) 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 +}