Building Message Component in Vue3
Tags: vue, vue3, message, component
Published: 2024-06-01 06:32:56

In most web products, global message components are widely used. They often appear in scenarios for user feedback, information prompts, and dialogues with the system. If you use the traditional component writing method, you need to import the component and register it in components, then call it in the template in the form of a tag, pass in custom props attributes, and trigger events through emit.

What we want the component is easy to use, just call a method.

```js
Message.text("This is default message");
Message.error({
    content: "error message",
    duration: 3000,
    close: true
});
```

## Component API Design

```
// Message type:
["text", "success", "error"]

// Message option
[String]: message content
[Object]: message config


text [String] "" message content
duration [Number] 0 milliseconds to close, 0 means it will not close automatically.
close [Boolean] false show close icon or not

// call api
Message[type](option);
```

## Component File Structure

```
|--- message
	|--- src
	|	|--- style
	|	|	|--- index.less
	|	|--- message-item.vue // component template of one message
	|	|--- message.js // render the component instance
	|	|--- Instance.js // component instance
	|---index.js // export component

```

### Template

The template is simple. The outer layer is wrapped by an animation component, and the message display and closing are controlled by v-show. The content part includes an icon, message text, and a configurable manual close button.

```vue
<template>
  <!-- message list -->
  <Transition name="slide-fade">
    <div class="message-container" v-show="visible">
      <!-- content -->
      <div class="message-content">
        <!-- Icon is determined by the message type. No icon is configured for text type -->
        <div class="message-icon" v-if="config.icon">
          <i :class="config.icon"></i>
        </div>

        <!-- message text -->
        <span v-text="config.content"></span>

        <!-- click to close -->
        <div class="option" v-if="config.close">
          <i class="ri-close-fill" @click="onClose"></i>
        </div>
      </div>
    </div>
  </Transition>
</template>
```

### message icon

The icon is determined by the type in the API call. The icon type is determined when creating the instance. Here, the open-source icon library Remix Icon is referenced. You can visit: [remixicon.cn](https://remixicon.cn/)

### Style

Create styles and animations.

```css
@radius: 4px;
@normalHeight: 34px;

.message {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  text-align: center;
  box-sizing: border-box;
  z-index: 9999;
  transform: translateZ(9999px);
  padding-top: 28px;
  transition: top 0.4s ease;

  .message-container {
    margin-bottom: 14px;

    .message-content {
      display: inline-block;
      padding: 0 18px;
      height: @normalHeight;
      text-align: left;
      line-height: @normalHeight;
      font-size: 14px;
      font-weight: 400;
      border-radius: @radius;
      color: #fff;
      background: #000;
      .option {
        display: inline-block;
        pointer-events: all;
        margin-left: 18px;

        i {
          font-size: 18px;
          font-weight: 400;
          margin-top: -3px;
          display: inline-block;
          box-sizing: border-box;
          vertical-align: middle;
          cursor: pointer;
          color: #d9d9d9;
          transition: color 0.2s ease;

          &:hover {
            color: #ff7c75;
            transition: color 0.2s ease;
          }
        }
      }
    }

    .message-icon {
      display: inline-block;

      i {
        font-size: 18px;
        font-weight: 400;
        margin-top: -3px;
        margin-right: 6px;
        display: inline-block;
        box-sizing: border-box;
        vertical-align: middle;
      }

      .ri-checkbox-circle-fill {
        color: #58c05b;
      }

      .ri-close-circle-fill {
        color: #fd4f4d;
        width: 20px;
        height: 20px;
        background-color: red;
        -webkit-mask-image: url('close-circle-fill.svg');
        mask-image: url('close-circle-fill.svg');
      }
    }
  }
}
.slide-fade-enter-active {
  transition: all 0.2s ease-out;
}

.slide-fade-leave-active {
  transition: all 0.2s ease;
}

.slide-fade-enter-from,
.slide-fade-leave-to {
  transform: translateY(-50px);
  opacity: 0;
}
```

### template script

In the component, rendering and unmounting are implemented by obtaining the passed-in config configuration and remove. The onOpen and onClose methods are used to control message opening and manual closing. The specific code is as follows.

```js
<script setup>
import { ref } from 'vue';
import './style/index.less';

const props = defineProps({
  config: { type: Object, default: () => undefined }, // message config
  remove: { type: Function, default: () => undefined } // callback of remove message
});

const visible = ref(false);

const onClose = () => {
  visible.value = false;
  setTimeout(() => {
    props.remove();
  }, 200);
};

const onOpen = (config) => {
  setTimeout(() => {
    visible.value = true;
  }, 10);

  // remove message in duration
  if (config.duration !== 0) {
    setTimeout(() => {
      onClose();
    }, config.duration);
  }
};

onOpen(props.config);
</script>
```

### Create Component Instance

Next, in Instance.js, we will write APIs for creating, mounting, and destroying components when the component is called. At the top, import the method for creating a Vue instance and the component template we just wrote:

```js
import { createApp } from 'vue';
import MessageItem from './message-item.vue';
```

Declare instance operation methods, accepting a message configuration parameter, cfg.

```js
/**
 * Message 
 * @param {Object} cfg
 */
const createInstance = cfg => {
	const config = cfg || {}
        // 1、Create a wrapper container, and set the outer Class attribute and message count.

	// 2、Create an instance and mount it to the body.
	
	// 3、unmount method, and recount after unmounting.
}
export default createInstance
```

#### 1. Create a wrapper container and set the outer Class attribute

Create a DIV as an outer container to wrap the component, and set the corresponding class attribute.

```js
let messageNode = document.createElement('div')
let attr = document.createAttribute("class")
attr.value = "message"
messageNode.setAttributeNode(attr)
```

Message count, we define the height of a message pop-up as 54 px. When multiple messages are queued to open, we make each component staggered by setting the top value.

```js
const height = 54 // Height of a single message box

const messageList = document.getElementsByClassName('message')
messageNode.style.top = `${messageList.length * height}px`
```

#### 2. Create an instance and mount it to the body.

```js
const app = createApp(MessageItem, {
  config,
  remove() {
    handleRemove()// Remove the element. After the message is closed, unmount and remove it from the DOM.
  }
})

// Mount the instance and append it to the end of the body
app.vm = app.mount(messageNode)
document.body.appendChild(messageNode)

app.close = () => {
  handleRemove()
}

return app

```

#### 3. Define methods for unmounting and resetting the top value.

```js
const handleRemove = ()=>{
  app.unmount(messageNode)
  document.body.removeChild(messageNode)
  resetMsgTop()
 }

const resetMsgTop = () => {
  for (let i = 0; i < messageList.length; i++) {
    messageList[i].style.top = `${i * height}px`
  }
}
```

### API for rendering instances

Read the configuration and render in message.js.

```js
import createInstance from './instance.js';

/**
 * Read configuration and render Message
 * @param {Object} typeCfg message type
 * @param {Object/String} cfg config
 */
function renderMsg(typeCfg = {}, cfg = '') {
  // Allow direct input of message content,
  // therefore, it is necessary to determine the type of the passed-in cfg
  const isContent = typeof cfg === 'string';

  // Integrate custom configurations
  cfg = isContent
    ? {
        content: cfg
      }
    : cfg;

  const config = Object.assign({}, typeCfg, cfg); // 合并配置

  const {
    type = 'text', // message type
    content = '', // message content
    icon = '', // message icon
    duration = 3000, // Automatic closing delay time
    close = false // Whether to display the close button
  } = config;

  // create instance
  return createInstance({
    type,
    content,
    duration,
    icon,
    close
  });
}
```

Expose APIs such as text, success, error, etc.

```js
export default {
  // text type
  text(cfg = '') {
    const textCfg = {
      type: 'text',
      icon: ''
    };

    return renderMsg(textCfg, cfg);
  },
  // success type
  success(cfg = '') {
    const successCfg = {
      type: 'success',
      icon: 'ri-checkbox-circle-fill'
    };

    return renderMsg(successCfg, cfg);
  },
  // error type
  error(cfg = '') {
    const errorCfg = {
      type: 'error',
      icon: 'ri-close-circle-fill'
    };
    return renderMsg(errorCfg, cfg);
  }
};
```

Finally, expose this component in the index.js.

```js
import Message from './src/Message.js';

export default Message;
```

Checkout out full code: https://github.com/markliu2013/StellarNovaUI/tree/main/packages/components/src/message

After understanding the code, You can style and change icon base on your project.