How I implemented toast notifications in the Vue application using Vuex

Toast notifications, among other things like for example loaders, are a basic functionality that usually is implemented before first release. This is because of their meaning in application. If user edits data in application and send it afterwards to a server, he must know whether these data are successfully stored in database. Or if a user started a long task in application but in the meantime can use other parts, he must be notified when the task is finished. Toast notifications often appear in top right corner of an application, overshadowing actual content. They may be persistent, requiring user to close them or they may disappear by themselves after specific time. It might be possible that more than one notification appears. If each of them vanishes after given time, we need to manage their state and manage display on a screen by for example moving up all of them if the one being at the top disappears. I would like to show you how I implemented this management system in my Vue application using Vuex.

Setup

The setup for this application looks like this:

  • Vue 2.6.10
  • Vuex 3.1.2
  • TypeScript 3.5.3

I will present individual files together with an explanation of their content.

Vuex

Before I start with showing application code, I want to explain what Vuex is. Vuex is a store management library and pattern based on Redux for applications written in Vue. In Vuex there are few concepts:

• a store – a container for a state and methods related to it (getters, mutations) as well as actions which play communication role between components and the store,

• a state – an object being a single source of truth because it contains all data used in the application,

• getters – methods behaving like Vue computed properties which help to convert stored data,

• actions – asynchronous methods which call server for data, dispatch other actions and commit mutations,

• mutations – synchronous methods which take the state as an argument and change it. Only mutations should affect stored data,

• modules – independent pieces of the store. They reflect different functionalities of the application.

Application components can:

• get stored data using getters,

• dispatch actions which can call BE to fetch state with data or trigger state changes. A component itself shouldn’t change state or call mutation directly. More about Vuex you can read from official website.

App.vue component

// App.vue

<template>
  <div>
    <ToastNotifications />
    <ToastNotificationsManager />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import ToastNotificationsManager from './components/ToastNotificationsManager.vue';
import ToastNotifications from './components/ToastNotifications.vue';

@Component({
  components: {
    ToastNotificationsManager,
    ToastNotifications
  },
})
export default class App extends Vue {}
</script>

The main component of the project. This is where I include the toast notifications component and a component for toast notifications display management. Nothing magical here.

ToastNotifications component

// ToastNotifications.vue

<template>
  <div>
    <div  v-for="(notification, index) of notifications"
          class="toast-overflow"
          :style="{ top: getNotificationPosition(index) }"
          :key="notification.id">
      <ToastNotification :notification="notification"/>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
import { mapState } from 'vuex';
import ToastNotification from './ToastNotification.vue';

@Component({
  components: {
    ToastNotification
  },
  computed: {
    ...mapState('toastModule', ['notifications'])
  }
})
export default class ToastNotifications extends Vue {
  private readonly firstNotificationTopPosition = 60;
  private readonly notificationVerticalSpace = 60;
  
  getNotificationPosition(notificationIndex: number): string {
    return `${notificationIndex * this.notificationVerticalSpace + this.firstNotificationTopPosition}px`;
  }
}
</script>

<style lang="scss" scoped>
  .toast-overflow {
    position: fixed;
    right: 30px;
    z-index: 9999;
    transition: all 1s ease-in-out;
  }
</style>

This component is responsible for displaying toast notifications in right order and place. Using v-for iterates over notifications list taken from vuex store module toastModule. ToastNotifications know which notification should remove from DOM due to dynamic key binding with value set to notification id. This component also adds proper class to toast notification element wrapper and sets its top position in the application.

ToastNotification component

// ToastNotification.vue

<template>
  <div class="notification-wrapper">
    <transition name="fade" appear>
      <div class="notification" :style="{ 'background-color': notification.backgroundColor }">
        {{ notification.text ? notification.text : 'Default text' }}
      </div>
    </transition>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
import { mapState } from 'vuex';
import { ToastNotification } from '../models';

@Component({
})
export default class ToastElement extends Vue {
  @Prop() notification!: ToastNotification;
}
</script>

<style lang="scss">
  .notification-wrapper {
    position: relative;
    width: 170px;
    margin: 10px;
  }
  .notification {
    height: 50px;
    padding: 10px 15px;
    border-radius: 7px;
    color: #FFF;
    background-color: #000;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
  }
  .fade-enter, .fade-leave-to {
    opacity: 0;
  }
</style>

This component is responsible for displaying one notification. ToastNotification sets notification text via text interpolation and background-color using dynamic binding. Both properties comes from Prop which is provided from parent component. The notification’s basic styling and proper animation is set here as well.

ToastNotificationsManager component

// ToastNotificationsManager.vue

<template>
  <div class="container-fluid">
    <h1>Example application showing toast notifications using Vuex</h1>
    <div class="form">
        <div class="form-group">
        <label for="notificationText">Notification text</label>
        <input type="text" v-model="textInput" class="form-control" id="notificationText">
      </div>
      <div>
        <div>Notification color</div>
        <div class="form-check">
          <input class="form-check-input" type="radio" id="success" value="#43A047" v-model="notificationColor" name="notificationColor">
          <label class="form-check-label" for="success">
            Success
          </label>
        </div>
        <div class="form-check">
          <input class="form-check-input" type="radio" id="error" value="#E53935" v-model="notificationColor" name="notificationColor">
          <label class="form-check-label" for="error">
            Error
          </label>
        </div>
        <div class="form-check">
          <input class="form-check-input" type="radio" id="warning" value="#FFD700" v-model="notificationColor" name="notificationColor">
          <label class="form-check-label" for="warning">
            Warning
          </label>
        </div>
      </div>
      <button @click="onClick()" class="btn btn-primary mt-3">Click to show notification</button>
    </div>
  </div>
</template>

<script>
import { Vue } from 'vue-property-decorator';
import { NotificationColor } from '../models';
export default class ToastNotificationsManager extends Vue {
    textInput = '';
    notificationColor = '';
    onClick() {
      this.$store.dispatch('toastModule/displayNotification', {
        text: this.textInput,
        backgroundColor: this.notificationColor
      });
  }
}
</script>

<style scoped>
  .form {
    display: flex;
    flex-direction: column;
    width: 300px;
    padding: 10px;
  }
</style>
 

ToastNotificationsManager component is used here only for presentational purpose. Here you can type your custom notification message in the text input field and choose what type of notification: success, warning or error it is. Then by clicking on the button, you can add new notification to the list which results in displaying newly-made one along with others.

Store

// index.ts in store

import Vue from 'vue';
import Vuex, { StoreOptions } from 'vuex';

import { toastModule } from './toast';
import { RootState } from '../models';

Vue.use(Vuex);

const store: StoreOptions<RootState> = {
    modules: {
        toastModule,
    },
};

export default new Vuex.Store<RootState>(store);

This is our main store object which contains only one module: toastModule which is responsible for toast notifications management. A RootState is just an empty object. I decided to extract all code related to notifications to one module because it reflects that is is one independent functionality.

 

// Toast.ts

import { Commit, Dispatch, Module } from 'vuex/types';

import { ToastNotification, ToastState, RootState } from '../models';


export const toastModule: Module<ToastState, RootState> = {
    namespaced: true,
    state: {
        notifications: [],
    },
    mutations: {
        displayNotification(state: ToastState, payload: ToastNotification) {
            state.notifications = [...state.notifications, {...payload, id: Symbol()}];
        },
        removeNotification(state: ToastState) {
            const notifications = [...state.notifications];
            notifications.shift();
            state.notifications = [...notifications];
        },
    },
    actions: {
        async displayNotification({dispatch, commit}: {dispatch: Dispatch, commit: Commit}, payload: ToastNotification) {
            commit('displayNotification', payload);
            await new Promise(
                (resolve, reject) => setTimeout(() => resolve(), 4000));
            dispatch('removeNotification');
        },
        removeNotification({commit}: {commit: Commit}) {
            commit('removeNotification');
        },
    },
};

The most important part of the project. In this module we set actions, mutations and state. The state is pretty simple – just a list of notifications. Each notification has a structure

interface ToastNotification {
    id: symbol;
    text: string;
    backgroundColor: string;
}

 

To provide unique ID for each notification, I use ES6 feature – symbol. By calling Symbol() function I always get new, unique value. This function cannot be used as a constructor. More information about this interesting primitive data type you can find here.

There are two actions – one for adding notification to the list and second one for removing. Only the first one is called in ToastNotificationsManager component. displayNotification action takes as a payload an object with text and backgroundColor fields, calls mutation with the same name. Subsequently, awaits for a promise to be resolved after 4 seconds and calls the second action – removeNotification action. The latter just calls mutation with the same name. Let’s go to mutations. displayNotification mutation adds new notification to the list. removeNotification mutation removes the first element from the list of notifications. The oldest elements are at the beginning.

Summary

Implementing multiple toast notifications in Vue with Vuex store is not hard, is it? By adding small module to vuex store and few components you will highly improve user experience in your application. Starting from now you can notify your users about some finished background tasks or other important information. So let’s make your users’ experience better!

 

Tags: , ,

Michał Wajer

Software Developer at Aspire Systems Poland. He began his adventure with Angular in the summer of 2018. In his free time, he learns the secrets of Vue, RxJS and TypeScript. Enthusiast of sport (most of all running) and speedway.