DEV Community

Cover image for Build a message component in Vue3
Jingtian Zhang
Jingtian Zhang

Posted on • Edited on

Build a message component in Vue3

Intro

in this case lets build a message component in Vue3, lets take a look how some famous UI frameworks, like how Element UI does it

from the demo we know what we need to do:

  1. using transition to show and close the message
  2. control the time of interval
  3. take user input and display in the message container
  4. the message should occur in the middle of screen
  5. messages will not overlap, knowing position of each message is necessary

above are some rough guess when I first time did it, lets walk through and examine

Basic Setup

first, if you need help with how Vue3 work, especially for understanding some customs for <script setup>, check this article of mine

in our case, lets build a message component that can satisfy our need:

message.success("this is a big success")
message.warning("take a look at this warning")
message.error("what a tragedy")
Enter fullscreen mode Exit fullscreen mode

lets start a Vue3 project using Vue CLI:

npm init vue@latest

✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formating? … No / Yes

Scaffolding project in ./<your-project-name>...
Done.
Enter fullscreen mode Exit fullscreen mode

lets construct our structure directory like the following:

- src
-- components // this components directory is for basic UI components, not for detailed components
---- packages
------ message 
-------- co-message.scss // style file
-------- co-message.vue  // template file 
-------- index.js       // component entry file
-------- instance.js    // instance file 
------ components.js // import each component and export as them in specific names
------ index.js // register component globally across te entire project 
-- views // views!!
---- Sample
------ index.vue // where we demo the message component
Enter fullscreen mode Exit fullscreen mode

in this case we are going to use Sass as css extension for better flexibility

you can read more about how to config scss for Vue3 in its doc

we will first build the template Vue file:

<template>
    <transition name="slide-fade">
        <div class="message-container" v-show="visible">
            <!-- content -->
            <div class="message-content">
                <!-- message icon -->
                <div class="message-icon" v-if="config.icon">
                    <i :class="config.icon"></i>
                </div>
                <span v-text="config.content" class="message-text"></span>
                <div class="option" v-if="!config.close">
                    <!-- manually close the message by clicking close icon -->
                    <i class="meta-iconfont meta-Close" @click="onClose"></i>
                </div>
            </div>
        </div>
    </transition>
</template>
<script>
import { reactive, toRefs } from 'vue';
import './co-message.scss'; // import style here

export default {
    props: {
        config: { type: Object, default: () => {} }, // configuration
        remove: { type: Function, default: () => {} } // unmount callback
    },
    setup(props) {
        const state = reactive({
            visible: false
        });
        // open message
        const onOpen = config => {
            setTimeout(() => {
                state.visible = true;
            }, 10);
            // remove message after duration 
            if (config.duration !== 0) {
                setTimeout(() => {
                    onClose();
                }, config.duration);
            }
        };
        onOpen(props.config);
        // onClose event
        const onClose = () => {
            state.visible = false;
            setTimeout(() => {
                props.remove();
            }, 200);
        };
        return {
            ...toRefs(state),
            onOpen,
            onClose
        };
    }
};
</script>
Enter fullscreen mode Exit fullscreen mode

after you setup the template file (can ignore about style file for now), you can setup the instance.js first, it is mainly instructions about how to create an Vue instance and append your component to Body in HTML

instance.js:

import { createApp } from 'vue';
import COMessage from './co-message.vue';

/**
 * Message instance operation
 * @param {*} cfg configuration
 */
const createInstance = cfg => {
    const config = cfg || {};

        // create a container and set its class
    let messageNode = document.createElement('div');
    let attr = document.createAttribute('class');
    attr.value = 'CO-message';
    messageNode.setAttributeNode(attr);

        // set a counter, when the next message happens, it will have a distance from the previous one
    const height = 70; // height, play around 
    const messageList = document.getElementsByClassName('CO-message');
    messageNode.style.top = `${messageList.length * height}px`;


        // reset each message's distance (Top value) to the top 
    const resetMsgTop = () => {
        for (let i = 0; i < messageList.length; i++) {
            messageList[i].style.top = `${i * height}px`;
        }
    };

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


        // create a Vue instance and append to Body
    const app = createApp(COMessage, {
        config,

                 // remove the element, close message and unmount and remove from DOM
        remove() {
            handleRemove();
        }
    });


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

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

export default createInstance;
Enter fullscreen mode Exit fullscreen mode

then you need to use this instance instruction in the entry file as index.js:

import createInstance from './instance.js';

/**
 * read, config and render Message
 * @param {Object} typeCfg 
 * @param {Object/String} cfg
 */
function renderMsg(typeCfg = {}, cfg = '') {
    // allow passing params, need to tell the type 
    const isContent = typeof cfg === 'string';

    // piece together config and merge them
    cfg = isContent
        ? {
                content: cfg
          }
        : cfg;

    const config = Object.assign({}, typeCfg, cfg); // merge configuration
    const {
        type = 'text', // type of message
        icon = '', // your icon
        content = '', // content
        immersive = false, // show immersive? 
        duration = 3000, // set the duration 
        close = false // showClose? 
    } = config;

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

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

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

        return renderMsg(successCfg, cfg);
    },
    // warning here
    warning(cfg = '') {
        const warningCfg = {
            type: 'warning',
            icon: 'icon-warning warning'
        };

        return renderMsg(warningCfg, cfg);
    },
    // error here
    error(cfg = '') {
        const errorCfg = {
            type: 'error',
            icon: 'icon-error error'
        };

        return renderMsg(errorCfg, cfg);
    }

};
Enter fullscreen mode Exit fullscreen mode

feel free to replace icon-success icon-warning and icon-error with other icons such as remix icons, Flaticon, etc.

well, then we need to register it globally and call it directly inside your js code, lets do that first:

there is a components.js file in the same level to message folder:

export { default as COMessage } from './message';
Enter fullscreen mode Exit fullscreen mode

and an index.js file that registers all components:

import * as components from './components';

export default {
    install: Vue => {
        Object.keys(components).forEach(key => {
            Vue.component(key, components[key]);
            if (key === 'COMessage') {
                Vue.config.globalProperties.$message = components[key];
            }
        });
    }
};
Enter fullscreen mode Exit fullscreen mode

after you have done the above job, time to beautify the component using scss:

message.scss:

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

    .message-container {
        .message-text {
            font-family: Roboto, sans-serif;
            font-style: normal;
            font-weight: 500;
            font-size: 14px;
            line-height: 20px;
        }

        .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;
            }

            .success {
                color: #32b732;
            }

            .warning {
                color: #f2b847;
            }

            .error {
                color: #fb4e4b;
            }

            // 加载图标
            .ri-loader-5-fill {
                display: inline-block;
                animation: rotating 1s ease-in-out infinite;
                -webkit-animation: rotating 1s ease-in-out infinite;
                color: #449efb;
            }

            @keyframes rotating {
                0% {
                    -webkit-transform: rotate(0deg);
                    transform: rotate(0deg);
                }

                100% {
                    -webkit-transform: rotate(359deg);
                    transform: rotate(359deg);
                }
            }
        }

        .message-content {
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 16px;
            height: 52px;
            margin-left: 16px;
            margin-right: 16px;
            text-align: left;
            line-height: 45px;
            font-size: 14px;
            font-weight: 400;
            border-radius: 4px;
            color: #595959;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            background: #ffffff;

            span {
                pointer-events: none;
                -moz-user-select: none;
                -o-user-select: none;
                -khtml-user-select: none;
                -webkit-user-select: none;
                -ms-user-select: none;
                user-select: none;
            }

            .option {
                display: inline-block;
                pointer-events: all;
                margin-left: auto;

                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: #fb4e4b;
                        transition: color 0.2s ease;
                    }
                }
            }
        }

        .message-text-immersive {
            color: #2f7cd4;
            border: 1px solid #96d5ff;
            background: #f0faff;
        }

        .message-success-immersive {
            color: #209124;
            border: 1px solid #81d17b;
            background: #ebf7e9;
        }

        .message-warning-immersive {
            color: #cc9331;
            border: 1px solid #ffe49c;
            background: #fffcf0;
        }

        .message-error-immersive {
            color: #d43538;
            border: 1px solid #ffa69e;
            background: #fff3f0;
        }
    }

    .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(-20px);
        opacity: 0;
    }
}

Enter fullscreen mode Exit fullscreen mode

in this case you have successfully done your job, lets use it in a demo single file component

<template>
<button @click="showSuccessMessage">click me to show success</button>
<button @click="showWarningMessage">click me to show warning</button>
<button @click="showErrorMessage">click me to show error</button>
</template>
<script setup>
import {ref, computed, getCurrentInstance} from "vue";

const { proxy } = getCurrentInstance(); 
const showErrorMessage = () => {
    proxy.$message.error('this is an error message');
};
const showSuccessMessage = () => {
    proxy.$message.success('this is a success message');
};
const showWarningMessage = () => {
    proxy.$message.warning('this is a warning message');
};
</script>
Enter fullscreen mode Exit fullscreen mode

and it should work here

Image description

a basic preview via Loom

I have a border Collie, so I call my own demo component library CollieUI, thats why you saw prefix as CO- once it is done will share the Github repo, thx for reading

Top comments (0)