I’ve been looking into the source code of the Element-Plus Message component and have had some good results. Finally, I’ll show you how to build your own Message component
1. Introduction
Considering the main focus on the logical part, so some HTML source CODE I will simplify and do not involve CSS operations, more convenient for everyone to read
The Vue developer library Vue-demi, which allows you to write code compatible with 2 and 3, is recommended and will be used in this article
Before reading, I need you to have the following foundations
Basic syntax for Vue3. <script setup> syntax syntax for Vue3. TypeScript basics 4Copy the code
Message
Official website examples of components
Specific usage:
ElMessage({
duration: 2000.message: 'Test'.type: 'info',})Copy the code
2. Online Cases:mdvui.github.io/MDVUI/
-
info
-
error
-
success
3. Components
html
<template>
<transition
name="message-fade"
@before-leave="onClose"
@after-leave="destroy"
>
<div
v-show="render"
ref="rootRef"
class="message"
:class="[ info ? 'color-blue': '', error ? 'color-red': '', success ? 'color-green': '', ]"
:style="Style"
>
<i class="icon" v-html="error || info ? 'info': 'done'" />
<div class="mv-alert-tip-slot">
{{ message }}
</div>
</div>
</transition>
</template>
Copy the code
Notice that we use transition, a built-in Vue component, and the core of it is @before-leave and @after-leave, with two methods bound to each
This is strange, why bind two destruction methods? Let’s keep it in suspense
Now come to the core
<div
v-show="render"
ref="rootRef"
class="message"
:class="[ info ? 'color-blue': '', error ? 'color-red': '', success ? 'color-green': '', ]"
:style="Style"
>
<i class="icon" v-html="error || info ? 'info': 'done'" />
<div class="message-slot">
{{ message }}
</div>
</div>
Copy the code
Since we want to make the component disappear or appear, we need to use a V-show or v-if command. Both of these commands can be implemented, but the official website uses the V-show example. We also use v-show here
This triggers the @before-leave and @after-leave events of the Transition component
As for the
:class="[
info ? 'color-blue': '',
error ? 'color-red': '',
success ? 'color-green': '',
]"
Copy the code
Determines the current message-type that is passed in, and finally displays the color
If the user does not pass in type, we can display info by default. Some readers might wonder if we could use TypeScript to make type in props mandatory.
That’s fine, but you have to consider that JS users are not type-bound, and you’re not wearing an attribute in props. Vue Complier will only throw a Warning, so the final solution is to default to INFO, okay
while
<i class="icon" v-html="error || info ? 'info': 'done'" />
Copy the code
Is the left icon that shows Message
The last
<div class="message-slot">
{{ message }}
</div>
Copy the code
It’s time to insert what you want to display in Message
4. The logical part of the component
I’m only going to analyze the core here
import type { VNode } from 'vue-demi'
import { computed, ref } from 'vue-demi'
import { onMounted } from 'vue'
export type MessageType = 'success' |'error'| 'info'
exportinterface IMessageProps { id? : number type? : MessageType duration? : number zIndex? : number message? : string | VNode offset? : number onDestroy? :() = > voidonClose? :() = > void
}
const props = withDefaults(defineProps<IMessageProps>(), {
type: 'info'.duration: 3000.message: ' '.offset: 20.onDestroy: () = > {},
onClose: () = >{},})const Style = computed(() = > ({
top: `${props.offset}px`.zIndex: props.zIndex,
}))
const error = computed(() = > props.type === 'error')
const info = computed(() = > props.type === 'info'|| (props.type ! = ='success'&& props.type ! = ='error'))
const success = computed(() = > props.type === 'success')
const render = ref()
onMounted(() = > {
startTimer()
render.value = true
})
function startTimer() {
setTimeout(() = > {
close()
}, props.duration)
}
function destroy() {
props.onDestroy()
}
function close() {
render.value = false
}
Copy the code
Core 1
const Style = computed(() = > ({
top: `${props.offset}px`.zIndex: props.zIndex,
}))
Copy the code
The code here controls the height and zIndex of each Message to ensure that each Message component is displayed correctly
The core 2
onDestroy? :() = > void, onClose? :() = > void.Copy the code
This code exists in props, and onClose is used for @before-leave in Transition
function destroy() {
props.onDestroy()
}
Copy the code
To trigger the onDestory function, onDestroy() is passed in externally, as we’ll see later
5. ElMessage core
import type { VNode } from 'vue-demi'
import { PopupManager } from '@mdvui/utils/popup-manager'
import { createVNode, isVNode, render } from 'vue-demi'
import MessageConstructor from './Message.vue'
import type { IMessageProps } from './Message.vue'
interface MessageOptions extendsIMessageProps { appendTo? : HTMLElement | string }let instances: VNode[] = []
let seed = 0
const message = (options: MessageOptions | string) = > {
if (typeof options === 'string') {
options = { message: options }
}
let appendTo: HTMLElement | null = document.body
if (typeof options.appendTo === 'string') {
appendTo = document.querySelector(options.appendTo)
}
if(! (appendToinstanceof HTMLElement)) {
appendTo = document.body
}
const props = {
zIndex: PopupManager.nextZIndex(),
id: seed++,
onClose: () = > {
close(seed - 1)},... options, }let verticalOffset = options.offset || 20
instances.forEach((vInstance) = >{ verticalOffset += (vInstance.el? .offsetHeight ||0) + 16
})
props.offset = verticalOffset
const container = document.createElement('div')
container.className = 'message-container'
const vm = createVNode(
MessageConstructor,
props,
isVNode(props.message) ? { default: () = > props.message } : null, ) vm.props! .onDestroy =() = > {
render(null, container)
}
instances.push(vm)
render(vm, container)
appendTo.appendChild(container)
return {
close: () = >close(vm.props! .idas number),
}
}
export const close = (vmId: number) = > {
const idx = instances.findIndex(vm= >vm.props! .id = vmId)if (idx === -1) {
return
}
const vm = instances[idx]
constremovedHeight = vm.el! .offsetHeight instances.splice(idx,1)
const len = instances.length
if (len === 0) {
return
}
for (let i = 0; i < len; i++) {
// TODO Why when using `offsetHeight` will cause bug? And use `style.top` it will be ok?
const pos = parseInt(instances[i].el! .style.top,10) - removedHeight - 16instances[i].component! .props.offset = pos } }export default message
Copy the code
Following up, onDestory() is used to free up memory after the component animation ends, thus avoiding a memory leak
vm.props! .onDestroy =() = > {
render(null, container)
}
Copy the code
So here’s the whole point. How do we render Message to the user? What’s the render function for, of course?
Let’s analyze the function of the render function
export declare const render: RootRenderFunction<Element | ShadowRoot>;
export declare type RootRenderFunction<HostElement = RendererElement> = (vnode: VNode | null, container: HostElement, isSVG? : boolean) = > void;
Copy the code
Render is bound to a RootRenderFunction type. RootRenderFunction is bound to a RendererElement. RendererElement is also an HTMLElement
Now to clear up our thinking, what we need to do is to render the Virtual Node we just wrote, the Vue Component, into a div that acts as a container
We can do that
const container = document.createElement('div')
container.className = 'container'
const vm = createVNode(
MessageConstructor,
props,
isVNode(props.message) ? { default: () = > props.message } : null, ) vm.props! .onDestroy =() = > {
render(null, container)
}
Copy the code
This creates a Virtual Node and an HTMLDIVElement, and now the render function
render(vm, container)
appendTo.appendChild(container)
Copy the code
Render renders the Virtual Node as an HTMLELement and then mounts it into the container. Finally we attach the container to appendTo, which renders it on the page
let appendTo: HTMLElement | null = document.body
if (typeof options.appendTo === 'string') {
appendTo = document.querySelector(options.appendTo)
}
if(! (appendToinstanceof HTMLElement)) {
appendTo = document.body
}
Copy the code
This code ensures that your appendTo is either document.body or the DOM you passed in, and your Message component will mount to the page, but don’t get too excited because we haven’t calculated the height of each Message yet
let verticalOffset = options.offset || 20
instances.forEach((vInstance) = >{ verticalOffset += (vInstance.el? .offsetHeight ||0) + 16
})
props.offset = verticalOffset
Copy the code
Each component has a height 16px higher than the previous one and passes the height to props. Offset so that the component automatically updates the height
Once the initial height problem is solved, there is another problem: when components are closed, we want the height of each component (except the first component) to return to the height of the previous component. How do we solve this problem?
let instances: VNode[] = []
let seed = 0
export const close = (vmId: number) = > {
const idx = instances.findIndex(vm= >vm.props! .id = vmId)if (idx === -1) {
return
}
const vm = instances[idx]
constremovedHeight = vm.el! .offsetHeight instances.splice(idx,1)
const len = instances.length
if (len === 0) {
return
}
for (let i = 0; i < len; i++) {
// TODO Why when using `offsetHeight` will cause bug? And use `style.top` it will be ok?
const pos = parseInt(instances[i].el! .style.top,10) - removedHeight - 16instances[i].component! .props.offset = pos } }Copy the code
It’s very simple
-
- Find the closed component
-
- Delete it
-
- Then set the height of each component to the height of the previous component
Instances do this when we use the Render function
instances.push(vm)
render(vm, container)
Copy the code
And so, at the end of that, remember we have a sign before-leave=”onClose” in the transition component?
Perform the following operations
const props = { zIndex: PopupManager.nextZIndex(), id: seed++, onClose: () => { close(seed - 1) }, ... options, } const vm = createVNode( MessageConstructor, props, isVNode(props.message) ? { default: () => props.message } : null, )Copy the code
This will automatically close the component when it is destroyed, and we already threw props in the createVNode
This is the end of your Message component building
Thank you for reading, and I hope you can point out your shortcomings and suggestions in the comments section, as well as give a small thumbs-up