Flash Messages

Flash messages are a common UI pattern used to display a message (either success or message) to the user as a result of an operation, usually a form submission. They are only displayed in a prominent way (eg. box with border), for a short period of time, and have a dismiss option (a close button).

They require a storage mechanism that is global and reactive so when a flash message is set in one component, it can be read by another component.

Reactive Global Storage

We'll use the "reactive" part of Vue.js to achieve this, by creating a reactive GlobalStore object, independent of any component, and "providing" it to our app:

src/main.js

import { createApp, reactive } from "vue";
import App from "./App.vue";
import router from "./router";

const GlobalStore = reactive({
  flash: {
    message: null,
    type: null,
  },
});

createApp(App).use(router).provide('GlobalStore', GlobalStore).mount("#app");

Set Flash Message

Any component can access the GlobalStore object and write to it:

src/views/ProjectEdit.vue

<template>
  <div>
    <h1>Project</h1>
    ...
    <button @click="saveProject">Save</button>
  </div>
</template>

<script>
export default {
  name: "ProjectEdit",
  inject: ["GlobalStore"],
  methods: {
    async saveProject() {
      try {
        await this.$http.put("/projects/" + this.project.id, this.project);
        this.GlobalStore.flash = {
          message: "Project saved successfully",
          type: "success",
        };
      } catch (error) {
        this.GlobalStore.flash = {
          message: error.response.data.message,
          type: "error",
        };
      }
      // clear the flash messages after 2 seconds
      setTimeout(() => {
        this.GlobalStore.flash = {
          message: null,
          type: null,
        };
      }, 2000);
    }
  },
};
</script>

Display the Flash Message

Inside the app we can read the GlobalStore object and display the flash message:

src/App.vue

<template>
  <div id="app">
    <div v-if="GlobalStore.flash.message" 
      :class="['flash', GlobalStore.flash.type]">
      <span>{{ GlobalStore.flash.message }}</span>
      <button @click="clearFlash">X</button>
    </div>
    <router-view />
  </div>
</template>

<script>
export default {
  name: "App",
  inject: ["GlobalStore"],
  methods: {
    clearFlash() {
      this.GlobalStore.flash = {
        message: null,
        type: null,
      };
    },
  },
};
</script>

<style>
.flash {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  padding: 1rem;
  background-color: #eee;
  border-bottom: 1px solid #ccc;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.flash.success {
  background-color: #cfc;
  border-color: #6c6;
}
.flash.error {
  background-color: #fcc;
  border-color: #c66;
}
</style>

Flash Component

It's a good idea to encapsulate the flash message logic in a component, so we can reuse it in other parts of the app:

src/components/Flash.vue

<template>
  <div v-if="GlobalStore.flash.message" 
    :class="['flash', GlobalStore.flash.type]">
    <span>{{ GlobalStore.flash.message }}</span>
    <button @click="clearFlash">X</button>
  </div>
</template>

<script>
import { inject } from "vue";

export default {
  setup() {
    const GlobalStore = inject("GlobalStore");
    return {
      GlobalStore,
    };
  },
  methods: {
    clearFlash() {
      this.GlobalStore.flash = {
        message: null,
        type: null,
      };
    },
  },
};
</script>

<style>
...
</style>

Then we can use the Flash component in our App:

src/App.vue

<template>
  <div id="app">
    <Flash />
    <router-view />
  </div>
</template>

<script>
import Flash from "./components/Flash.vue";

export default {
  name: "App",
  components: {
    Flash,
  },
};
</script>

Using the Global Event Bus

Vue.js has a global event bus that can be used to emit and listen to events. This is a great way to communicate between components. We can use this to set and read flash messages.

Set Flash Message

Any component can emit a flash message:

src/views/ProjectEdit.vue

<template>
  <div>
    <h1>Project</h1>
    ...
    <button @click="saveProject">Save</button>
  </div>
</template>

<script>
export default {
  name: "ProjectEdit",
  methods: {
    async saveProject() {
      try {
        await this.$http.put("/projects/" + this.project.id, this.project);
        this.$root.$emit('flash', {
          message: "Project saved successfully",
          type: "success",
        });
      } catch (error) {
        this.$root.$emit('flash', {
          message: error.response.data.message,
          type: "error",
        });
      }
      // clear the flash messages after 2 seconds
      setTimeout(() => {
        this.$root.$emit('flash', {
          message: null,
          type: null,
        });
      }, 2000);
    }
  },
};
</script>

Here we are emitting the flash event on the $root instance. We are passing an object as the value of the event.

Display the Flash Message

We'll now create a component that will display the flash message. This can be used inside any other component, usually inside App.vue.

src/components/Flash.vue

<template>
  <div v-if="message" :class="['flash', messageType]">
    <span>{{ message }}</span>
    <button @click="clearFlash">X</button>
  </div>
</template>

<script>
export default {
  name: 'Flash',
  data() {
    return {
      message: null,
      messageType: null,
    };
  },
  mounted() {
    this.$root.$on('flash', (message) => {
      if(typeof message === 'object' && !!message) {
        this.message = message.message;
        this.messageType = message.type;
      }
    });
  },
  methods: {
    clearFlash() {
      this.message = null;
      this.messageType = null;
    },
  },
};
</script>

<style>
...
</style>

Here we are listening to the flash event on the $root instance. We are storing the message and type in the component's data. We are also clearing the message after 2 seconds.