Toast system using Typescript, Vue 3 and bootstrap 5
Hi, I wanted to create a toast error system for my web application where whenever an error is thrown a toast is created with the error body and appended to a div element. the final result looks like this :
What's in the stack
- Vue 3/ Composition API
- Bootstrap 5.1
- Typescript 4.6
Deep dive
Firstly we create a Vue component called Toast.vue that encapsulates the UI and the Algorithm that processes toast show up.
The setup script block is as follows :
<script setup lang="ts">
import { onUpdated } from "vue";
import { Toast } from "bootstrap";
const props = defineProps({
errors: { type: Array, default: () => [] },
});
onUpdated(() => {
const hiddenToasts = props.errors.filter((obj) => {
return obj.show != true;
});
hiddenToasts.forEach(function (error) {
var errorToast = document.getElementById(error.id);
var toast = new Toast(errorToast);
toast.show();
error.show = true;
errorToast.addEventListener("hidden.bs.toast", function () {
const indexOfObject = props.errors.findIndex((item) => {
return item.id === error.id;
});
if (indexOfObject !== -1) {
props.errors.splice(indexOfObject, 1);
}
});
});
});
</script>
errors array will host toast's states, it can be anything. In my case, it's an object with an id, msg, and a boolean show state
onUpdated() is a Vue lifecycle hooks triggered whenever the component has updated its DOM tree due to a reactive state change, in our case the reactive state is the errors array.
1. Firstly we filter the errors that are not shown yet, then for each error we trigger toast.show() and we add a listener on hidden.bs.toast
2. Whatever an event hidden.bs.toast is triggered we remove the error object from the array so that we have a nice and clean DOM
The script block is as follows :
<script lang="ts">
const TOASTS_MAX = 5;
export function push(array: Array, data): Array {
if (array.length == TOASTS_MAX) {
array.shift();
array.push(data);
} else {
array.push(data);
}
}
</script>
- TOASTS_MAX defines the max number of toasts to show up at the same time
- the push method is a simple way to implement a circular array. Whenever the array contains TOASTS_MAX elements, adding a new one will delete the element at the position 0 and push the new element at the end, in this way we are sure to have a maximum of TOASTS_MAX elements
- the push method needs to be exported so that other components can use it
The template block is as follows :
<template>
<div ref="container" class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
<div v-for="item in errors" v-bind:id="item.id" class="toast fade opacity-75 bg-danger" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="15000">
<div class="toast-header bg-danger">
<strong class="me-auto text-white">Error</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body text-white error-body">{{ item.msg }}</div>
</div>
</div>
</template>
- <div ref="container"...> fixed in the bottom right host the toasts
- v-for directive loop on errors array and render it whenever a change happens.
- v-bind:id directive let us define an id for each toast, the id needs to be unique!
- data-bs-delay let us define the timeout before auto-hiding the toast
How to use it?
<script setup lang="ts">
import { ref, reactive } from 'vue';
import toast from './Toast.vue';
import { push } from './Toast.vue';
const state = reactive({ errors: [], count : 0 });
function pushError(id: int) {
push(state.errors, { id: id, msg: 'Error message ' + id });
}
</script>
<template>
<toast :errors="state.errors"></toast>
<button type="button" @click="pushError('toast' + state.count++)">
Error trigger: {{ state.count }}
</button>
</template>
- state hold the reactive errors array and the count that forms the error's id
- Whenever the button is clicked an error object is pushed to the errors array and because the errors array is attached to the Toast component using :errors="state.errors" whenever we change the source array the target array is updated
Here is the Demo!