Build a message component in Vue3
Published: 2022-03-24 12:37:17

## 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](https://element.eleme.io/#/en-US/component/message)

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](https://dev.to/zhangjt9317/vue3-script-setup-writing-16gd)

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

```javascript 
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: 

```bash
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: 

```markdown
- 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](https://sass-lang.com/) 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:

```vue
<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: 

```javascript 
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`:

```javascript 
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: 

```javascript 
export { default as COMessage } from './message';
```

and an `index.js` file that registers all components: 

```javascript 
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:**
```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 

```vue
<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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2udgo22cldo7qssxnaps.png)

a basic preview via [Loom](https://www.loom.com/share/cef318468e934899845676cb61712ce4)

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