refactor(components): refactor message (#3524)

* refactor(components): refactor message

* chore: remove unused
This commit is contained in:
三咲智子
2021-09-22 01:40:18 +08:00
committed by GitHub
parent 2431b589af
commit 9575d111c2
7 changed files with 237 additions and 258 deletions

View File

@@ -1,7 +1,7 @@
import { nextTick } from 'vue'
import { getStyle } from '@element-plus/utils/dom'
import { rAF } from '@element-plus/test-utils/tick'
import Message from '../src/message'
import Message from '../src/message-method'
jest.useFakeTimers()
const selector = '.el-message'

View File

@@ -1,9 +1,8 @@
import { h, nextTick } from 'vue'
import * as domExports from '@element-plus/utils/dom'
import makeMount from '@element-plus/test-utils/make-mount'
import { rAF } from '@element-plus/test-utils/tick'
import { EVENT_CODE } from '@element-plus/utils/aria'
import Message from '../src/index.vue'
import Message from '../src/message.vue'
import type { ComponentPublicInstance, CSSProperties } from 'vue'
@@ -76,19 +75,6 @@ describe('Message.vue', () => {
})
})
describe('lifecycle', () => {
test('should add keydown event lister on mount', () => {
jest.spyOn(domExports, 'on')
jest.spyOn(domExports, 'off')
const wrapper = _mount({
slots: { default: AXIOM },
})
expect(domExports.on).toHaveBeenCalled()
wrapper.unmount()
expect(domExports.off).toHaveBeenCalled()
})
})
describe('Message.type', () => {
test('should be able to render typed messages', () => {
for (const type of ['success', 'warning', 'info', 'error'] as const) {

View File

@@ -1,15 +1,8 @@
import Message from './src/message'
import { withInstallFunction } from '@element-plus/utils/with-install'
import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils/types'
import Message from './src/message-method'
const _Message = Message as SFCWithInstall<typeof Message>
export const ElMessage = withInstallFunction(Message, '$message')
export default ElMessage
_Message.install = (app: App) => {
app.config.globalProperties.$message = _Message
}
export default _Message
export const ElMessage = _Message
export * from './src/types'
export * from './src/message'

View File

@@ -0,0 +1,120 @@
import { createVNode, render } from 'vue'
import { isVNode } from '@element-plus/utils/util'
import PopupManager from '@element-plus/utils/popup-manager'
import isServer from '@element-plus/utils/isServer'
import MessageConstructor from './message.vue'
import { messageTypes } from './message'
import type { MessagePartial, 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: MessagePartial = function (options = {}) {
if (isServer) return { close: () => undefined }
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 id = `message_${seed++}`
const userOnClose = options.onClose
const props: Partial<MessageProps> = {
zIndex: PopupManager.nextZIndex(),
offset: verticalOffset,
...options,
id,
onClose: () => {
close(id, userOnClose)
},
}
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 })
document.body.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)) {
options = {
message: options,
}
}
return message({
...options,
type,
})
}
})
export function close(id: string, userOnClose?: (vm: VNode) => void): void {
const idx = instances.findIndex(({ vm }) => id === vm.component!.props.id)
if (idx === -1) return
const { vm } = instances[idx]
if (!vm) return
userOnClose?.(vm)
const removedHeight = vm.el!.offsetHeight
instances.splice(idx, 1)
// adjust other instances vertical offset
const len = instances.length
if (len < 1) return
for (let i = idx; i < len; i++) {
const pos =
parseInt(instances[i].vm.el!.style['top'], 10) - removedHeight - 16
instances[i].vm.component!.props.offset = pos
}
}
export function closeAll(): void {
for (let i = instances.length - 1; i >= 0; i--) {
const instance = instances[i].vm.component as any
instance.ctx.close()
}
}
message.closeAll = closeAll
export default message

View File

@@ -1,137 +1,90 @@
import { createVNode, render } from 'vue'
import { isVNode } from '@element-plus/utils/util'
import PopupManager from '@element-plus/utils/popup-manager'
import isServer from '@element-plus/utils/isServer'
import MessageConstructor from './index.vue'
import { buildProp } from '@element-plus/utils/props'
import type { ComponentPublicInstance } from 'vue'
import type {
IMessage,
MessageQueue,
IMessageOptions,
MessageVM,
IMessageHandle,
MessageParams,
} from './types'
import type { VNode, ExtractPropTypes } from 'vue'
const instances: MessageQueue = []
let seed = 1
export const messageTypes = ['success', 'info', 'warning', 'error'] as const
// 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: IMessage = function (
opts: MessageParams = {} as MessageParams
): IMessageHandle {
if (isServer) return
export const messageProps = {
customClass: {
type: String,
default: '',
},
center: {
type: Boolean,
default: false,
},
dangerouslyUseHTMLString: {
type: Boolean,
default: false,
},
duration: {
type: Number,
default: 3000,
},
iconClass: {
type: String,
default: '',
},
id: {
type: String,
default: '',
},
message: buildProp<string | VNode>({
type: [String, Object],
default: '',
}),
onClose: buildProp<() => void>({
type: Function,
required: false,
}),
showClose: {
type: Boolean,
default: false,
},
type: buildProp({
type: String,
values: messageTypes,
default: 'info',
} as const),
offset: {
type: Number,
default: 20,
},
zIndex: {
type: Number,
default: 0,
},
} as const
export type MessageProps = ExtractPropTypes<typeof messageProps>
if (typeof opts === 'string') {
opts = {
message: opts,
}
}
export const messageEmits = {
destroy: () => true,
}
export type MessageEmits = typeof messageEmits
let options: IMessageOptions = <IMessageOptions>opts
export type MessageOptions = Omit<MessageProps, 'id'>
export type MessageOptionsTyped = Omit<MessageOptions, 'type'>
let verticalOffset = opts.offset || 20
instances.forEach(({ vm }) => {
verticalOffset += (vm.el.offsetHeight || 0) + 16
})
verticalOffset += 16
const id = `message_${seed++}`
const userOnClose = options.onClose
options = {
...options,
onClose: () => {
close(id, userOnClose)
},
offset: verticalOffset,
id,
zIndex: PopupManager.nextZIndex(),
}
const container = document.createElement('div')
container.className = `container_${id}`
const message = options.message
const vm = createVNode(
MessageConstructor,
options,
isVNode(options.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 })
document.body.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),
}
} as any
export function close(id: string, userOnClose?: (vm: MessageVM) => void): void {
const idx = instances.findIndex(({ vm }) => {
const { id: _id } = vm.component.props
return id === _id
})
if (idx === -1) {
return
}
const { vm } = instances[idx]
if (!vm) return
userOnClose?.(vm)
const removedHeight = vm.el.offsetHeight
instances.splice(idx, 1)
// adjust other instances vertical offset
const len = instances.length
if (len < 1) return
for (let i = idx; i < len; i++) {
const pos =
parseInt(instances[i].vm.el.style['top'], 10) - removedHeight - 16
instances[i].vm.component.props.offset = pos
}
export interface MessageHandle {
close: () => void
}
export function closeAll(): void {
for (let i = instances.length - 1; i >= 0; i--) {
const instance = instances[i].vm.component as any
instance.ctx.close()
}
export type MessageParams = Partial<MessageOptions> | string | VNode
export type MessageParamsTyped = Partial<MessageOptionsTyped> | string | VNode
export interface MessagePartial {
(options?: MessageParams): MessageHandle
closeAll(): void
success?: (options?: MessageParamsTyped) => MessageHandle
warning?: (options?: MessageParamsTyped) => MessageHandle
info?: (options?: MessageParamsTyped) => MessageHandle
error?: (options?: MessageParamsTyped) => MessageHandle
}
export type Message = Required<MessagePartial>
type MessageQueueItem = {
vm: VNode
}
;(['success', 'warning', 'info', 'error'] as const).forEach((type) => {
Message[type] = (options) => {
if (typeof options === 'string') {
options = {
message: options,
type,
}
} else {
options.type = type
}
return Message(options)
}
})
Message.closeAll = closeAll
export default Message
export type MessageQueue = MessageQueueItem[]

View File

@@ -28,7 +28,6 @@
{{ message }}
</p>
<!-- Caution here, message could've been compromised, never use user's input as message -->
<!-- eslint-disable-next-line -->
<p v-else class="el-message__content" v-html="message"></p>
</slot>
<div
@@ -40,71 +39,49 @@
</transition>
</template>
<script lang="ts">
import { defineComponent, computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { defineComponent, computed, ref, onMounted } from 'vue'
import { useEventListener, useTimeoutFn } from '@vueuse/core'
import { EVENT_CODE } from '@element-plus/utils/aria'
import { on, off } from '@element-plus/utils/dom'
import { messageEmits, messageProps } from './message'
// MessageVM is an alias of vue.VNode
import type { PropType } from 'vue'
import type { Indexable } from '@element-plus/utils/types'
import type { MessageVM } from './types'
const TypeMap: Indexable<string> = {
success: 'success',
info: 'info',
warning: 'warning',
error: 'error',
import type { CSSProperties } from 'vue'
import type { MessageProps } from './message'
const typeMap: Record<MessageProps['type'], string> = {
success: 'el-icon-success',
info: 'el-icon-info',
warning: 'el-icon-warning',
error: 'el-icon-error',
}
export default defineComponent({
name: 'ElMessage',
props: {
customClass: { type: String, default: '' },
center: { type: Boolean, default: false },
dangerouslyUseHTMLString: { type: Boolean, default: false },
duration: { type: Number, default: 3000 },
iconClass: { type: String, default: '' },
id: { type: String, default: '' },
message: {
type: [String, Object] as PropType<string | MessageVM>,
default: '',
},
onClose: {
type: Function as PropType<() => void>,
required: true,
},
showClose: { type: Boolean, default: false },
type: { type: String, default: 'info' },
offset: { type: Number, default: 20 },
zIndex: { type: Number, default: 0 },
},
emits: ['destroy'],
setup(props) {
const typeClass = computed(() => {
const type = !props.iconClass && props.type
return type && TypeMap[type] ? `el-icon-${TypeMap[type]}` : ''
})
const customStyle = computed(() => {
return {
top: `${props.offset}px`,
zIndex: props.zIndex,
}
})
props: messageProps,
emits: messageEmits,
setup(props) {
const visible = ref(false)
let timer = null
let timer: (() => void) | undefined = undefined
const typeClass = computed(() =>
props.iconClass ? props.iconClass : typeMap[props.type] ?? ''
)
const customStyle = computed<CSSProperties>(() => ({
top: `${props.offset}px`,
zIndex: props.zIndex,
}))
function startTimer() {
if (props.duration > 0) {
timer = setTimeout(() => {
if (visible.value) {
close()
}
}, props.duration)
;({ stop: timer } = useTimeoutFn(() => {
if (visible.value) close()
}, props.duration))
}
}
function clearTimer() {
clearTimeout(timer)
timer = null
timer?.()
}
function close() {
@@ -125,12 +102,9 @@ export default defineComponent({
onMounted(() => {
startTimer()
visible.value = true
on(document, 'keydown', keydown)
})
onBeforeUnmount(() => {
off(document, 'keydown', keydown)
})
useEventListener(document, 'keydown', keydown)
return {
typeClass,

View File

@@ -1,47 +0,0 @@
import type { VNode } from 'vue'
export interface IMessageHandle {
close: () => void
}
export type IMessageOptions = {
customClass?: string
center?: boolean
dangerouslyUseHTMLString?: boolean // default false
duration?: number // default 3000
iconClass?: string
id?: string
message?: string | VNode
offset?: number // defaults 20
onClose?: () => void
showClose?: boolean // default false
type?: 'success' | 'warning' | 'info' | 'error' | ''
zIndex?: number
}
export type MessageType = 'success' | 'warning' | 'info' | 'error' | ''
export type IMessageDispatcher = (
options?: IMessageOptions | string
) => IMessageHandle
export type MessageParams = IMessageOptions | string
export type TypedMessageParams<T extends MessageType> =
| ({ type: T } & Omit<IMessageOptions, 'type'>)
| string
export interface IMessage {
(options?: MessageParams): IMessageHandle
success: (options?: TypedMessageParams<'success'>) => IMessageHandle
warning: (options?: TypedMessageParams<'warning'>) => IMessageHandle
info: (options?: TypedMessageParams<'info'>) => IMessageHandle
error: (options?: TypedMessageParams<'error'>) => IMessageHandle
closeAll(): void
}
export type MessageVM = VNode
type MessageQueueItem = {
vm: MessageVM
}
export type MessageQueue = Array<MessageQueueItem>