Awwwards
How to Use Vuex to Build a Feature's Hero Image

How to Use Vuex to Build a Feature

Over the last few weeks, we've explored a lot of different Vuex concepts. We went over how to use Vuex with Laravel Spark, how to set up modules, and how to make API calls. Throughout these walkthroughs, we've relied on brief code snippets to illustrate the concepts we were covering. But, we haven't really seen how to use everything together to build an actual feature. So today, let's do just that. We'll build an events messaging system that you'll be able to use inside of your Vue components as well as your Vuex.

Project Set Up

We've uploaded the code for this walkthrough on GitHub so you can follow along as we explore all of the concepts.

Unlike in our previous posts where we used Laravel Spark as our application framework, today we're going to use the vue-cli tool to get us up and running quickly. To set up this project, hop into the command line and navigate to where you want to store it. If you haven't already, install the vue-cli tool with npm install -g vue-cli. Now you can init the project with one of the vue scaffolding templates. For this project, I just used webpack-simple so the command was vue init webpack-simple <project-name>.

Now that we have our project up and running, we need to make a few quick tweaks to make sure we can use Vuex. First, let's add Vuex and some other packages to the project by running the following command from our terminal.

npm install --save-dev vuex babel-plugin-transform-runtime babel-preset-stage-2

With those installed let's update our .babelrc in the root of our project. It should look something like this:

{
"presets": [
    ["es2015", { "modules": false }]
  ]
} 

We're going to update it to look like this so babel can correctly parse everything.

{
  "presets": ["es2015", "stage-2"],
  "plugins": ["transform-runtime"]
}

With that taken care of we can now start to create our event messaging system.

Event Messaging

Before we dive into the code, let's examine what an event messaging system should do so we have an idea of what to build. Basically, we're trying to create a system that lets a user know that something has happened in the application that they should know about. So it's essentially a notification system for our application.

With this in mind, what exactly would we have to tell our user? We'll probably need different types of messages so the user understands the context of the message. We'll need a way to actually display some text so the user knows what is happening. And while most messages should just appear and then disappear after a few seconds, we should also have a way to let the user dismiss messages that persist in the interface. Now that we know what we need, let's get to work.

Setting Up Our Messages Vuex

Inside of our src directory, we can see that we have an App.vue and a main.js file. Inside of main.js, we need to initialize vuex so we can use it in the project. To do this we need to import vuex, tell vue to use it, and then add our store to our vue instance. So our main.js file should look like this:

// src/main.js

import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex'

Vue.use(Vuex);

import store from './vuex/store'

new Vue({
  el: '#app',
  render: h => h(App),
  store
})

Now our import store from './vuex/store' won't work because we haven't created it yet. So let's hop back into our src directory and create a directory called vuex. Inside of vuex we can add two sub-directories called modules and utils as well as our store.js file. Here we can set up our application's global vuex store like so:

// src/vuex/store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

//Separate Modules
import messages from './modules/messages/store'

export default new Vuex.Store({
    modules: {
        messages: messages
    }
})

As you can see, we're going to be using modules like we did in my previous blog post. So let's set them up now. In our utils directory, create two files for namespace.js and types.js.

// src/vuex/utils/namespace.js

function mapValues (obj, f) {
    const res = {};
    Object.keys(obj).forEach(key => {
        res[key] = f(obj[key], key)
    });
    return res
}

export default (module, types) => {
    let newObj = {};

    mapValues(types, (names, type) => {
        newObj[type] = {};
        types[type].forEach(name=> {
            var newKey = module + '/' + name;
            newObj[type][name] = newKey;
        });
    });
    return newObj;
}
// src/vuex/utils/types.js

import namespace from './namespace'

//Messages Module
export const messages = namespace('messages', {
    getters: [
        'messages'
    ],
    actions: [
        'customMessage',
        'genericMessage',
        'genericDismissMessage',

        'successMessage',
        'successDismissMessage',

        'errorMessage',
        'errorDismissMessage',

        'trashMessage',
        'trashDismissMessage',

        'warningMessage',
        'warningDismissMessage',

        'infoMessage',
        'infoDismissMessage',

        'removeMessage'
    ],
    mutations: [
        'ADD_MESSAGE',
        'REMOVE_MESSAGE'
    ]
});

As you know from the previous post, these files prevent our vuex modules from running into namespacing conflicts. Within types.js you can see that we've already defined a variety of Getters, Actions, and Mutations for our messages component, which correspond directly with the feature requirements we touched on earlier.

On a side note, your type.js file can serve as a form of documentation for your vuex modules. Since you're defining that happens in your vuex modules here, you get a good overview of your entire vuex at a glance. In bigger projects, I like to add comments here to group everything into Data and State sections, and then add additional comments explaining what each of them do.

Creating the Messages Module

Back to our project, let's hop into our src/vuex/modules directory and create another directory for messages. In here, let's create a store.js and an actions.js. Our store.js will look like this:

import { messages } from '../../utils/types.js';
import actions from './actions.js';

const state = {
    messages: []
};

const getters = {
    [messages.getters.messages]: state => {
        return state.messages;
    }
};

const mutations = {
    [messages.mutations.ADD_MESSAGE]: (state, message) => {
        state.messages.push(message);
    },
    [messages.mutations.REMOVE_MESSAGE]: (state, id) => {
        state.messages.some(function(message, index) {
            if(message.id == id) {
                state.messages.splice(index, 1);
            }
        });
    }
};

const module = {
    state,
    getters,
    mutations,
    actions
};

export default module;

As you can see, it's pretty straight-forward. At the top, we're importing our namespacing and our actions. Then we define our state, which is just an array of messages. Next, there's our getters where we define a simple one that let's us access our messages. In our mutations, we've got two functions. The first simply pushes a message into our messages array. The second is a little trickier, but all we're doing is receiving an id and then looping through our messages until we find a message that matches the id. Then we splice that message from the messages to remove it. Lastly, we're bundling everything together into our module and declaring our export default so we can import it into src/vuex/store.js.

Now let's hop into our actions.js file.

import { messages } from '../../utils/types.js';

var lut = []; for (var i=0; i<256; i++) { lut[i] = (i<16?'0':'')+(i).toString(16); }

function generateUuid() {
    var dvals = new Uint32Array(4);
    window.crypto.getRandomValues(dvals);
    var d0 = dvals[0];
    var d1 = dvals[1];
    var d2 = dvals[2];
    var d3 = dvals[3];
    return lut[d0&0xff]+lut[d0>>8&0xff]+lut[d0>>16&0xff]+lut[d0>>24&0xff]+'-'+
        lut[d1&0xff]+lut[d1>>8&0xff]+'-'+lut[d1>>16&0x0f|0x40]+lut[d1>>24&0xff]+'-'+
        lut[d2&0x3f|0x80]+lut[d2>>8&0xff]+'-'+lut[d2>>16&0xff]+lut[d2>>24&0xff]+    lut[d3&0xff]+lut[d3>>8&0xff]+lut[d3>>16&0xff]+lut[d3>>24&0xff];
}

class Message {
    constructor(text) {
        this.message = {
            'id': generateUuid(),
            'text': text,
            'type': '',
            'dismissible': false,
            'forceDismiss': false,
            'timeout': 6
        }
    }

    text(message) {
        this.message.text = message;
        return this;
    }

    success() {
        this.message.type = 'success';
        return this;
    }

    error() {
        this.message.type = 'error';
        return this;
    }

    trash() {
        this.message.type = 'trash';
        return this;
    }

    warning() {
        this.message.type = 'warning';
        return this;
    }

    info() {
        this.message.type = 'info';
        return this;
    }

    dismissible() {
        this.message.dismissible = true;
        return this;
    }

    forceDismiss() {
        this.message.forceDismiss = true;
        return this;
    }

    short() {
        this.message.timeout = 4;
        return this;
    }

    long() {
        this.message.timeout = 10;
        return this;
    }

    save(store) {
        return store.commit(messages.mutations.ADD_MESSAGE, this.message);
    }
}

const actions = {
    [messages.actions.customMessage]: (store, message) => {
        message.id = generateUuid();
        return store.commit(messages.mutations.ADD_MESSAGE, message);
    },

    [messages.actions.genericMessage]: (store, text) => {
        return new Message(text).save(store);
    },
    [messages.actions.genericDismissMessage]: (store, text) => {
        return new Message(text).dismissible().forceDismiss().save(store);
    },

    [messages.actions.successMessage]: (store, text) => {
        return new Message(text).success().save(store);
    },
    [messages.actions.successDismissMessage]: (store, text) => {
        return new Message(text).success().dismissible().forceDismiss().save(store);
    },

    [messages.actions.errorMessage]: (store, text) => {
        return new Message(text).error().save(store);
    },
    [messages.actions.errorDismissMessage]: (store, text) => {
        return new Message(text).error().dismissible().forceDismiss().save(store);
    },

    [messages.actions.trashMessage]: (store, text) => {
        return new Message(text).trash().save(store);
    },
    [messages.actions.trashDismissMessage]: (store, text) => {
        return new Message(text).trash().dismissible().forceDismiss().save(store);
    },

    [messages.actions.warningMessage]: (store, text) => {
        return new Message(text).warning().save(store);
    },
    [messages.actions.warningDismissMessage]: (store, text) => {
        return new Message(text).warning().dismissible().forceDismiss().save(store);
    },

    [messages.actions.infoMessage]: (store, text) => {
        return new Message(text).info().save(store);
    },
    [messages.actions.infoDismissMessage]: (store, text) => {
        return new Message(text).info().dismissible().forceDismiss().save(store);
    },

    [messages.actions.removeMessage]: (store, id) => {
        return store.commit(messages.mutations.REMOVE_MESSAGE, id);
    }
};

export default actions

As you can see, this is a much larger file. But we'll break it down slowly and make sure we understand everything.

First, we import namespacing like usual. Then we have this generateUuid() function, which I found in this StackOverflow question (thank you Jeff Ward!). We need each of our messages to have a unique id so we can identify and remove them from our state.messages array as we demonstrated in our REMOVE_MESSAGE mutation.

Right below generateUuid(), we've defined a class for Messages. This is not a required step, but it does help us keep our Actions smaller and more readable. In our class, we have a constructor that defines our base message and then a variety of other functions that we can use to manipulate our message. To see this in action, let's finally look at our actual Actions now.

In our const for actions, we've defined everything that we set up in src/vuex/utils/types.js. Each of these Actions can be triggered from our vue components, or other vuex modules, to add a message. Inside of our successMessage action, you can see that we create a new Messages class and then chain on our class functions until we've constructed the message we need. So our action doesn't have to look like this:

[messages.actions.successMessage]: (store, text) => {
	var message = {
		'id': generateUuid(),
	    'text': text,
	    'type': 'success',
	    'dismissible': false,
	    'forceDismiss': false,
	    'timeout': 6
	}
   return store.commit(messages.mutations.ADD_MESSAGE, message);
},

As you can tell, this gets tedious and is kind of hard to read - especially given the number of Actions we have. Instead we have a nice chainable syntax that takes care of a default state; ensures we're using the same message structure, and is easy to understand.

Finally, we declare our export default so we can import our Actions into our src/vuex/modules/messages/store.js. And there you have it! Our vuex is ready to be used in our vue components.

Building Our Vue Components

Now that we have our vuex set up, let's hop back into our src directory. Here we have an App.vue file that the vue-cli added for us. Let's go in there and change up the boilerplate code so we can demo our messages.

// src/App.vue

<template>
  <div id="app">
    <h1>Messages</h1>
    <div class="inputs">
      <div class="input-group">
        <label>Message Text</label>
        <input type="text" v-model="text">
      </div>
      <div class="input-group">
        <label>Message Type</label>
        <select v-model="type">
          <option value="">Default</option>
          <option value="success">Success</option>
          <option value="info">Info</option>
          <option value="warning">Warning</option>
          <option value="error">Error</option>
        </select>
      </div>
      <div class="input-group">
        <label>Dismissible?</label>
        <span>
          <input type="radio" :value="true" v-model="dismiss"> Yes
          <input type="radio" :value="false" v-model="dismiss"> No
        </span>
      </div>
      <button @click="sendMsg">Send Message</button>
    </div>
    <messages></messages>
  </div>
</template>

<script>
import { mapActions } from 'vuex'
import messages from './components/messages/messages'
export default {
  name: 'app',
  data () {
    return {
      text: '',
      type: '',
      dismiss: false
    }
  },
  methods: {
    ...mapActions({
      'genericMessage': 'messages/genericMessage',
      'genericDismissMessage': 'messages/genericDismissMessage',
      'infoMessage': 'messages/infoMessage',
      'infoDismissMessage': 'messages/infoDismissMessage',
      'successMessage': 'messages/successMessage',
      'successDismissMessage': 'messages/successDismissMessage',
      'errorMessage': 'messages/errorMessage',
      'errorDismissMessage': 'messages/errorDismissMessage',
      'warningMessage': 'messages/warningMessage',
      'warningDismissMessage': 'messages/warningDismissMessage'
    }),
    sendMsg: function () {
      var t = this;

      if(t.dismiss == false) {
        if(t.type == 'success') {
          t.successMessage(t.text)
        } else if(t.type == 'error') {
          t.errorMessage(t.text)
        } else if(t.type == 'info') {
          t.infoMessage(t.text)
        } else if(t.type == 'warning') {
          t.warningMessage(t.text)
        } else {
          t.genericMessage(t.text);
        }
      } else {
        if(t.type == 'success') {
          t.successDismissMessage(t.text)
        } else if(t.type == 'error') {
          t.errorDismissMessage(t.text)
        } else if(t.type == 'info') {
          t.infoDismissMessage(t.text)
        } else if(t.type == 'warning') {
          t.warningDismissMessage(t.text)
        } else {
          t.genericDismissMessage(t.text);
        }
      }
    }
  },
  components: {
    messages
  }
}
</script>

<style>
  #app {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    height: 100vh;
  }
  .inputs {
    display: flex;
    flex-direction: column;
    margin: 1em 0;
  }
  .input-group {
    display: flex;
    flex-direction: column;
    width: 190px;
    margin: 1.25em 0;
  }
  label {
    padding-bottom: .25em;
  }
  button {
    cursor: pointer;
    transition: all 0.2s ease-out;
    margin: 1em auto 0;
    padding: 1em;
    background-color: white;
    color: steelblue;
    box-shadow: none;
    border: 1px solid steelblue;
    border-radius: 2px;
  }
  button:hover {
    color: white;
    background-color: steelblue !important;
  }
</style>

In here, we've got a few different things going on. Our template is a pretty simple form that can trigger different messages for us so we can see all of the different permutations. Our script holds the meat and potatoes of our component. At the top, we've imported our vuex actions as well as the messages component. Next, we have our data, which is used for the v-models. Then we have our methods where we map our Actions from our vuex, and we also have the sendMsg function that does some logic to determine which vuex action to trigger based on our form selection. And lastly, we have our components where we've registered our messages component. Finally, our style has, you guessed it, some styles defined for us.

Now this won't work just yet because we haven't created our messages component. So in our src directory, let's create a components directory and inside of that we can add a messages directory. Inside of messages let's create a messages.js, a messages.vue, and a message.vue. Our messages.js file is pretty standard:

// src/components/messages/messages.vue

var Vue = require('vue');

import messages from './messages.vue';

export default Vue.component('messages', messages);

Next up we have messages.vue:

// src/components/messages/messages.vue

<template>
    <transition-group name="messages" tag="div" class="messages">
        <message v-for="message in messages" :key="message.id" :message="message"></message>
    </transition-group>
</template>

<script>
    import message from './message.vue'
    import { mapGetters } from 'vuex'
    export default{
        computed: {
            ...mapGetters( {
                'messages': 'messages/messages'
            })
        },


        components:{
            message
        }
    }
</script>

<style>
    .messages {
        transition: all 0.75s ease-in;
        display: flex;
        flex-direction: column;
        justify-content: center;
        position: fixed;
        bottom: 15px;
        left: 20px;
        z-index: 1000;
    }
</style>

Here we're taking advantage of some vue 2.0 improvements to improve our messages UX. In our <template> we have a <transition-group> defined so our messages can fade in and out of the interface nicely. Then we have a simple v-for on our <message> component so we can display all of the messages. In <script>, we're importing the message component as well as our Getters from the vuex. In our computed property we're mapping the messages Getter, and then in components we're registering message. And again we have some styles defined as well in our <style>.

With messages taken care of we just need one last component: message.

// src/components/messages/message.vue
<template>
    <div class="message">
        <div>
            <i v-if="iconClass != ''" class="fa fa-fw" :class="iconClass"></i>
            <h5>{{message.text}}</h5>
        </div>
        <i v-if="message.dismissible == true " class="fa fa-fw fa-times" @click="removeMessage(message.id)"></i>
    </div>
</template>

<script>
    import { mapActions } from 'vuex'
    export default{
        props: ['message'],
        computed: {
            iconClass: function () {
                var t = this;
                if (t.message.type == 'success') {
                    return 'fa-check'
                } else if (t.message.type == 'error') {
                    return 'fa-ban'
                } else if (t.message.type == 'trash') {
                    return 'fa-trash'
                } else if (t.message.type == 'warning') {
                    return 'fa-exclamation'
                } else if (t.message.type == 'info') {
                    return 'fa-info'
                } else {
                    return ''
                }
            },
            timer: function () {
                var t = this;
                return t.message.timeout * 1000;
            }
        },
        mounted() {
            var t = this;
            if(t.message.forceDismiss == false) {
                setTimeout(function() {
                    t.removeMessage(t.message.id);
                }, t.timer);
            }
        },
        methods: {
            ...mapActions({
                'removeMessage': 'messages/removeMessage'
            })
        }
    }
</script>

<style>
    .message {
        transition: all 0.75s ease-in;
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        padding: 1em;
        max-width: 225px;
        border-radius: 3px;
        margin-bottom: 1em;
        background-color: #fff;
        box-shadow: 0px 4px 6px 1px rgba(61, 63, 65, 0.4);
    }
    .message > div {
        display: flex;
    }
    .message i, .message h5 {
        font-size: 14px;
    }
    .message i {
        height: 100%;
        margin-right: 0.75em;
    }
    .message h5 {
        line-height: 1.2;
        margin: 0;
    }
    .message .fa-times {
        cursor: pointer;
        margin: 0 0 0 0.75em;
        color: #ccc;
    }
    .message .fa-times:hover {
        color: #a6a6a6;
    }
    .message .fa-check {
        color: green;
    }
    .message .fa-ban, .message .fa-trash {
        color: red;
    }
    .message .fa-info {
        color: steelblue;
    }
    .message .fa-exclamation {
        color: goldenrod;
    }

    .messages-enter, .messages-leave-active {
        opacity: 0;
        transform: translateY(-30px);
    }
</style>

In our <template> we have a simple message that leverages some dynamic classes to display the correct Font Awesome Icon based on its type. We also have a dismissible toggle that a user can use to dismiss a message. Moving to our <script>, we have imported our Actions from vuex as usual. Then we've defined the message prop that is being passed during the v-for in the messages component. Next we have the computed properties where we have that dynamic class I mentioned earlier as well as the timer, which converts our human-readable seconds into milliseconds. Then in mounted we put the timer to good use and dismiss the message if it's dismissible after the set amount of time. Our methods are where we mapped the actions from the vuex. In <style> we once again have some styles defined.

Final Touches

And there you have it! Our components and vuex are all ready to go. The last thing we need to do is update our index.html to include Font Awesome and some other styles for us.

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>vuex-messages-demo</title>
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
    <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
    <style>
      body {
        margin: 0;
        font-family: 'Open Sans', sans-serif;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <script src="/dist/build.js"></script>
  </body>
</html>

Now if you run npm run dev from your project directory, you should see our final result at localhost:8080/ in your browser.

Conclusion

Phew! That was a lot of code to get through! We saw how to use vuex and vue to implement a real, legitimate feature that any application could use. And while we covered a lot of ground, there's a lot more that we can do with our messages. For instance, we could let our users know that their latest save went through by dispatch a message when our API succeeds. The sky is really the limit here, and I'd love to see how you use messages in your application. So feel free to Tweet your implmentations, and any questions, to @metricloop. Until next time, happy coding!


Nick Basile's Profile Picture

Nick Basile


Lead UI/UX Engineer

Nick is the Lead UI/UX Engineer at Metric Loop. He lives in Austin, TX and spends his free time taking long strolls on South Lamar. If you see him, be sure to honk.