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

MessageOfficial 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

    1. Find the closed component
    1. Delete it
    1. 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