From npm init to a Single-Page Application in Vue.js, Part 2

In the last part we set up our webpack with babel and our boilerplate. In this part, we’ll use that webpack setup to start building our Single-Page application with vue-router and vuex. To start putting our routing together we’ll need to define a few components to separate out our application logic. Create a folder in your source directory called components and an admin folder inside of that. Now create a Home.vue component in the components root and the admin folder. Inside of these vue files is where we’ll build our Todo app with admin controls. Here’s some boilerplate to start with:

<template>
<div id="app">
    <h1>{{ message }}</h1>
</div>
</template>
<script>
export default {
    name: 'home',
    data () {
        return {
            message: "Welcome to vue.js!"
        }
    }
}
</script>
<style></style>
<template>
<div id="app">
    <h1>{{ message }}</h1>
</div>
</template>
<script>
export default {
    name: 'adminHome',
    data () {
        return {
            message: "Welcome to the admin page!"
        }
    }
}
</script>
<style></style>

So now that we have some components to route to we can set up the router in our index.js file. To setup the router we have to:

  1. Import the Vue router.
  2. Create an instance of the Vue router object.
  3. Add some routes and map them to components.
  4. Register the router with our root Vue instance.
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
import Home from './components/Home.vue'
import Admin from './components/admin/Home.vue'

Vue.use(VueRouter);

const router = new VueRouter({
    routes: [
        { path: '', component: Home },
        { path: '/admin', component: Admin }
    ]
});


new Vue({
    el: "#app",
    router,
    render: h => h(App)
});

Finally, we have to use a router view element in our App.vue file:

<template>
<div id="app">
    <router-view></router-view> 
</div>
</template>
<script>
export default {
    name: 'app',
    data () {
        return {}
    }
}
</script>
<style></style>

Putting the router-view in the root App.vue file renders our entire app structure to the browser and allows us to send users to different components by appending a /$route to our url. Awesome! Now we can start building some basic Todo functionality. To do this we’ll set up a data property in our home component, add a few html elements, and add an addition method to add new Todos.

<template>
<div id="app">
    <h1>{{ message }}</h1>
    <ul>
        <li v-for="(item, index) in this.items">
            <template>
                <div class="input-group">
                    <input type="checkbox" :id="index" v-model="item.checked">
                    <label :for="index">{{ item.description }}</label>                    
                </div>
            </template>
        </li>
    </ul>
    <input v-model="tempMessage">
    <button @click="addTodo">Submit</button>
</div>
</template>
<script>
export default {
    name: 'home',
    data () {
        return {
            message: "Todo App",
            items: [
                { description: "Get milk", checked: false },
                { description: "Send emails", checked: true }
            ],
            tempMessage: ''
        }
    },
    methods: {
        addTodo() {
            this.items.push({description: this.tempMessage, checked: false});
            this.tempMessage = '';
        }
    }
}
</script>
<style></style>

The result

Now let’s add an admin panel with options to style the todos as we add them! We’ll add a toggle for strikethrough, zebra striping, and a “salty developer” mode to overwrite our todos completely with a random message.

<template>
<div id="app">
    <h1>{{ message }}</h1>
    <ul class="switch-padding">
        <li v-for="(option, index) in this.options">
            <template>
                <div class="input-group">
                    <input type="checkbox" :id="index" v-model="option.checked" tabindex="0">
                    <label :for="index" class="switch">{{ option.description }}</label>                    
                </div>
            </template>
        </li>
    </ul>
</div>
</template>
<script>
export default {
    name: 'adminHome',
    data () {
        return {
            message: "Welcome to the admin page!",
            options: [
                {description: "Zebra stripes", checked: false},
                {description: "Strikethrough items", checked: false},
                {description: "Salty Developer Mode", checked: false}
            ]
        }
    }
}
</script>
<style>
.switch-padding {
    padding-left: 50px;
}
</style>

The final result looks like this:

This sets up a toggle switch object that we can use to control the main todo component. Normally we could pass global state to the router as a property, but our root instance doesn’t have the data we need. This also wouldn’t solve the problem of our todos being reset each time we navigate to a different route. So, to accomplish this we’ll add Vuex, some router-links, and a global store for our instances to store data. To do this we’ll create an object called store inside of a new file called store which we will store in a folder called store. The final store file looks like this:

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

Vue.use(Vuex);

export const store = new Vuex.Store({
    state: {
        todoItems: [
            { description: "Get milk", checked: false },
            { description: "Send emails", checked: true }
        ],
        adminOptions: [
            { description: "Zebra stripes", checked: false },
            { description: "Strikethrough items", checked: false },
            { description: "Salty Developer Mode", checked: false }
        ],
        saltyPhrases: [
            'Consider ASP.NET career',
            'Vomit cause I considered an ASP.NET career (or was it the sushi?)',
            'Remove \'I dreamed a dream\' (new version) from coding playlist',
            'Cry because I don\'t actually know how to build anything useful',
            'Pretend that ruby on rails performance is good enough for production',
            'Learn erlang + elixir (later)',
            'Get those TPS to boss',
            'Resent getting TPS to boss',
            'Play with my whitespace configuration (again)',
            'Create another project repo that I\'m never going to keep updated',
            'Add \'Sound of Silence\' to coding playlist'
        ]
    },
    getters: {
        getTodos(state) {
            return state.todoItems;
        },
        getAdminOptions(state) {
            return state.adminOptions;
        },
        getSaltyPhrases(state) {
            return state.saltyPhrases;
        },
        isZebraStripes(state) {
            return state.adminOptions[0].checked;
        },
        isStrikeThrough(state) {
            return state.adminOptions[1].checked;
        },
        isSaltyDeveloperMode(state) {
            return state.adminOptions[2].checked;
        }
    },
    mutations: {
        addTodo(state, payload) {
            state.todoItems.push(payload);
        }
    }
});

After we’ve defined the state we can import it into our index.js file.

import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
import Home from './components/Home.vue'
import Admin from './components/admin/Home.vue'
import { store } from './store/store.js'

Vue.use(VueRouter);

const router = new VueRouter({
    routes: [
        { path: '', component: Home },
        { path: '/admin', component: Admin }
    ],
    mode: 'history'
});

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

I also changed the router mode to history so we don’t have that hashtag hanging around in the address bar.

The Vuex instance is sort of unique to the rest of our Vue instances, but it shares similar concepts. There are still reserved properties, but instead of data, computed, methods, etc. we have state, getters, and mutations. The state is the global state store for your single page application. This is usually where results from REST API requests will be stored in a traditional SPA. The mutations section is where rest requests get made and stored in the state property, and all changes to state should happen through a mutation. This ensures consistency of operation on state across your application. Finally, we have the getters property which allows us to retrieve state in a consistent manner across the application.

Now that we have the state extracted we just need to wire up the new state to the old components:

<template>
    <div id="app">
        <h1>{{ message }}</h1>
        <router-link to="/admin">Go to the admin page!</router-link>
        <ul>
            <li v-for="(item, index) in this.$store.getters.getTodos">
                <template>
                    <div class="input-group">
                        <input type="checkbox" :id="index" v-model="item.checked" tabindex="0">
                        <label :for="index" :class="{strikethrough: (isStrikeThrough && item.checked), zebra: (isZebraStripes && index % 2 === 0)}">{{ item.description }}</label>
                    </div>
                </template>
            </li>
        </ul>
        <input v-model="tempMessage">
        <button @click="addTodo">Submit</button>
    </div>
</template>
<script>
export default {
    name: 'home',
    data() {
        return {
            message: "Todo App",
            tempMessage: ''
        }
    },
    computed: {
        isStrikeThrough() {
            return this.$store.getters.isStrikeThrough;
        },
        isZebraStripes() {
            return this.$store.getters.isZebraStripes;
        }
    },
    methods: {
        addTodo() {
            var tempObject = { description: '', checked: false };
            if (this.$store.getters.isSaltyDeveloperMode) {
                var saltyPhrases = this.$store.getters.getSaltyPhrases;
                var saltyIndex = Math.floor(Math.random() * saltyPhrases.length);
                tempObject.description = saltyPhrases[saltyIndex];
            }
            else {
                tempObject.description = this.tempMessage;
            }
            this.$store.commit('addTodo', tempObject);
            this.tempMessage = '';
        }
    }
}
</script>
<style>
.strikethrough {
    text-decoration: line-through;
}

.zebra {
    color: white;
    background-color: black;
}
</style>
<template>
    <div id="app">
        <h1>{{ message }}</h1>
        <router-link to="/">Go to the home page!</router-link>
        <ul class="switch-padding">
            <li v-for="(option, index) in this.$store.getters.getAdminOptions">
                <template>
                    <div class="input-group">
                        <input type="checkbox" :id="index" v-model="option.checked" tabindex="0">
                        <label :for="index" class="switch">{{ option.description }}</label>
                    </div>
                </template>
            </li>
        </ul>
    </div>
</template>
<script>
export default {
    name: 'adminHome',
    data() {
        return {
            message: "Welcome to the admin page!"
        }
    }
}
</script>
<style>
.switch-padding {
    padding-left: 50px;
}
</style>

And here you can see what it looks like when you turn on all three modes:

The result of turning all the options on and typing in four more todos.

Next up we’ll add child routes to allow for editing of todos. Stay tuned!