Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(components): [message] fix typings and switch to script setup #8378

Merged
merged 3 commits into from Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion internal/build/src/type-unsafe-stricter.json
Expand Up @@ -15,7 +15,6 @@
"packages/components/loading/",
"packages/components/menu/",
"packages/components/message-box/",
"packages/components/message/",
"packages/components/notification/",
"packages/components/option-group/",
"packages/components/option/",
Expand Down
Expand Up @@ -59,7 +59,7 @@ describe('Message on command', () => {
const getTopValue = (elm: Element): number =>
Number.parseInt(getStyle(elm as HTMLElement, 'top'), 10)

const topValues = []
const topValues: number[] = []
elements.forEach((e) => {
topValues.push(getTopValue(e))
})
Expand Down
6 changes: 2 additions & 4 deletions packages/components/message/__tests__/message.test.ts
Expand Up @@ -91,10 +91,8 @@ describe('Message.vue', () => {
const type = 'some-type'
const wrapper = _mount({ props: { type } })

for (const key in TypeComponentsMap) {
expect(wrapper.findComponent(TypeComponentsMap[key]).exists()).toBe(
false
)
for (const component of Object.values(TypeComponentsMap)) {
expect(wrapper.findComponent(component).exists()).toBe(false)
}
console.warn = consoleWarn
})
Expand Down
269 changes: 141 additions & 128 deletions packages/components/message/src/message-method.ts
Expand Up @@ -5,86 +5,46 @@ import {
isElement,
isFunction,
isNumber,
isObject,
isString,
isVNode,
} 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, MessageProps, MessageQueue } from './message'
import { messageDefaults, messageTypes } from './message'

import type { AppContext } from 'vue'
import type {
Message,
MessageFn,
MessageHandler,
MessageInstance,
MessageOptions,
MessageParams,
MessageParamsNormalized,
MessageQueue,
MessageQueueItem,
} from './message'

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> & { _context: AppContext | null } =
function (options = {}, context?: AppContext | null) {
if (!isClient) return { close: () => undefined }
if (isNumber(messageConfig.max) && instances.length >= messageConfig.max) {
return { close: () => undefined }
}
const normalizeOptions = (params?: MessageParams) => {
const options: MessageOptions =
!params || isString(params) || isVNode(params) || isFunction(params)
? { message: params }
: params

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 || 'info'
return {
close: () =>
((
vm.component!.proxy as ComponentPublicInstance<{
visible: boolean
}>
).visible = false),
}
}
}
const normalized = {
...messageDefaults,
...options,
}

if (isString(options) || isVNode(options)) {
options = { message: options }
}
if (isString(normalized.appendTo)) {
let appendTo = document.querySelector<HTMLElement>(normalized.appendTo)

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(),
...options,
offset: verticalOffset,
id,
onClose: () => {
close(id, userOnClose)
},
}

let appendTo: HTMLElement | null = document.body
if (isElement(options.appendTo)) {
appendTo = options.appendTo
} else if (isString(options.appendTo)) {
appendTo = document.querySelector(options.appendTo)
}
// should fallback to default value with a warning
if (!isElement(appendTo)) {
debugWarn(
Expand All @@ -94,89 +54,142 @@ const message: MessageFn & Partial<Message> & { _context: AppContext | null } =
appendTo = document.body
}

const container = document.createElement('div')
normalized.appendTo = appendTo
}

container.className = `container_${id}`
return normalized as MessageParamsNormalized
}

const messageContent = props.message
const vm = createVNode(
MessageConstructor,
props,
isFunction(messageContent)
? { default: messageContent }
: isVNode(messageContent)
? { default: () => messageContent }
: null
)
const closeMessage = (instance: MessageQueueItem) => {
const idx = instances.indexOf(instance)
if (idx === -1) return

vm.appContext = context || message._context
instances.splice(idx, 1)
const { vnode, handler } = instance
handler.close()

const removedHeight = vnode.el!.offsetHeight
// adjust other instances vertical offset
const len = instances.length
if (len < 1) return
for (let i = idx; i < len; i++) {
const pos =
Number.parseInt(instances[i].vnode.el!.style['top'], 10) -
removedHeight -
16

instances[i].vnode.component!.props.offset = pos
}
}

const createMessage = (
{ appendTo, ...options }: MessageParamsNormalized,
context?: AppContext | null
): MessageQueueItem => {
const { nextZIndex } = useZIndex()

const id = `message_${seed++}`
const userOnClose = options.onClose

let verticalOffset = options.offset
instances.forEach(({ vnode: vm }) => {
verticalOffset += (vm.el?.offsetHeight || 0) + 16
})
verticalOffset += 16

const container = document.createElement('div')

const props = {
...options,
zIndex: options.zIndex ?? nextZIndex(),
offset: verticalOffset,
id,
onClose: () => {
userOnClose?.()
closeMessage(instance)
},

// clean message element preventing mem leak
vm.props!.onDestroy = () => {
render(null, container)
onDestroy: () => {
// 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),
}
render(null, container)
},
}
const vnode = createVNode(
MessageConstructor,
props,
isFunction(props.message) || isVNode(props.message)
? { default: props.message }
: null
)
vnode.appContext = context || message._context

render(vnode, container)
// instances will remove this item when close function gets called. So we do not need to worry about it.
appendTo.appendChild(container.firstElementChild!)

const vm = vnode.component!.proxy as MessageInstance
const handler: MessageHandler = {
// 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: () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore `visible` from defineExpose
vm.visible = false
},
}

messageTypes.forEach((type) => {
message[type] = (options = {}, appContext?: AppContext | null) => {
if (isString(options) || isVNode(options)) {
options = {
message: options,
}
}
return message(
{
...options,
type,
},
appContext
)
const instance = {
vnode,
vm,
handler,
}
})

export function close(id: string, userOnClose?: (vm: VNode) => void): void {
const idx = instances.findIndex(({ vm }) => id === vm.component!.props.id)
if (idx === -1) return
return instance
}

const { vm } = instances[idx]
if (!vm) return
userOnClose?.(vm)
const message: MessageFn &
Partial<Message> & { _context: AppContext | null } = (
options = {},
context
) => {
if (!isClient) return { close: () => undefined }

const removedHeight = vm.el!.offsetHeight
instances.splice(idx, 1)
if (isNumber(messageConfig.max) && instances.length >= messageConfig.max) {
return { close: () => undefined }
}

// adjust other instances vertical offset
const len = instances.length
if (len < 1) return
for (let i = idx; i < len; i++) {
const pos =
Number.parseInt(instances[i].vm.el!.style['top'], 10) - removedHeight - 16
const normalized = normalizeOptions(options)

instances[i].vm.component!.props.offset = pos
if (normalized.grouping && instances.length) {
const instance = instances.find(
({ vnode: vm }) => vm.props?.message === normalized.message
)
if (instance) {
;(instance.vnode.component as any).props.repeatNum += 1
;(instance.vnode.component as any).props.type = normalized.type
return instance.handler
}
}

const instance = createMessage(normalized, context)

instances.push(instance)
return instance.handler
}

messageTypes.forEach((type) => {
message[type] = (options = {}, appContext) => {
const normalized = normalizeOptions(options)
return message({ ...normalized, type }, appContext)
}
})

export function closeAll(): void {
for (let i = instances.length - 1; i >= 0; i--) {
const instance = instances[i].vm.component
;(instance?.proxy as any)?.close()
for (const instance of instances) {
instance.handler.close()
}
}

Expand Down