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:
- using transition to show and close the message
- control the time of interval
- take user input and display in the message container
- the message should occur in the middle of screen
- 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")
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.
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
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>
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;
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);
}
};
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';
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];
}
});
}
};
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;
}
}
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>
and it should work here
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)