Making a To-do List App in Vue

In this tutorial, We’re going to be making a to-do list application with Vue. This is a follow on from my tutorial on creating your first ever vue application. Follow that tutorial if you need help getting started. Since the best way to learn is to try making something yourself, this guide should give you a good starting point to understand how Vue works.

Ultimately, our todo list app will look a little like this:

Making a Vue To-Do List Application

If you’ve already followed our other tutorial on making your first vue application, you should have a basic vue file structure. The first step on any project is thinking about what you want it to do. For our to-do application, I think the following features would be a good starting point:

  • An archive page – this will contain any to-do list items we have deleted.
  • A to-do list page – this will be our main to-do list page, where we can add and remove to-do list items.
  • Persistent lists – I want the list to exist if I leave the page, or refresh it. It shouldn’t disappear – so we’ll need storage.
  • An about page – A simple about page to display everything about us and what our mission is.

Before we start, let’s set up our file structure. If you’ve followed our other tutorial, you should have a basic idea of ​​how Vue applications are structured. For this project, set up your files to look like this:

Project File Structure

public
|- index.html     <-- this is the file where our application will exist
src
|- components     <-- a folder to put components in
|-- TodoList.vue  <-- we will only need one component today, our "TodoList" component
|- router         
|-- index.js      <-- info on our routes (another word for pages)
|- views     
|-- About.vue     <-- The about page
|-- Archive.vue   <-- The archive page
|-- Home.vue      <-- The home page
| App.vue         <-- Our main app code
| main.js         <-- Our main.js, which will contain some 

Note: if you don’t have a router folder, you can add it by running vue add router within your vue folder.

Setting up Our Router

Since we’ll have multiple pages in our Vue application, we need to configure that in our router index.js file. Open index.js in the router folder, and change it to look like this:

import(‘../views/Home.vue’)
},
{
path: ‘/archive’,
name: ‘Archive’,
component: () => import(‘../views/Archive.vue’)
},
{
path: ‘/about’,
name: ‘About’,
component: () => import(‘../views/About.vue’)
}
]

const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router” data-lang=”text/javascript”>

import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: "https://dzone.com/",
    name: 'Home',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/archive',
    name: 'Archive',
    component: () => import('../views/Archive.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

We’ve covered this in our previous tutorial, but essentially this is going to create 3 different pages – /archive, / and /about – and enable the history API for them. We use import() to import the pages we created in our file structure from before – those being Archive.vue, Home.vue and About.vue.

Storing Data in Vue With Vuex

Now that we have the “structure” of our application, let’s discuss how we’ll store data in our application. Vue has a very useful plugin called Vuex, which is a state management tool. All that means is we can take all of our data from Vue, store it within a Vuex store, and we’ll be able to easily manage all of our data. To install vuex, simply run the following command in your vue folder:

Adding Vuex to Our Application

Since we’ve installed Vuex, we can start to configure it in our application. Let’s focus on how we’ll manipulate and store our data. We’ll add our Vuex Store straight to our main.js file, within the src folder. Change that file to the following, so that we can initiate a store:

import { createApp } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'
import router from './router'

const app = createApp(App);

// Create a store for our to do list items
const store = createStore({
    state() {

    }, 
    getters: {

    },
    mutations: {

    }
});

app.use(router).use(store).mount('#app')

Vuex allows us to create a store for our data. We’ll store our entire to-do list within a Vuex store. Within Vuex, there are 3 main pieces of functionality we’ll be leveraging:

  • state() – this is where we will store our data. All of our to-do list data will go in here.
  • getters – this does exactly what you think – it lets us get the data from our store.
  • mutations – these are functions we’ll use to update our state data – so these functions will update our to-do list – for example, marking an item as done.

State and Getters in Vuex

The two easiest pieces of functionality we’ll look at in our store will be our state() and getters. Let’s think about how we’ll store our todo list items in state(). Our to-do list items have a few different attributes – they will have a name, and probably a unique id. We’ll need to label which page they are on (home page, or archive), and we’ll need an option to set them to complete or not.

For getters, when we want to get our to-do list, we really only need one method – get all of our to-do list items. Below, I’ve configured one default to-do list item, and a getter that simply gets all of our to-do lists:

const store = createStore({
    state () {
        return {
            todos: [
                // I've added one default todo below which will show when you first access the page.
                // You can remove this if you want!
                // id<string> can be any unique ID
                // name<string> is the name of our item
                // completed<boolean> is set to true when done, false when not
                // location<['home', 'archive']> is set to home or archive depending on which page we want to show it on
                { id: 'first-element', name: 'My First To Do Item', completed: false, location: 'home' }
            ]
        }
    },
    getters: {
        todos (state) {
            // Returns every todo list (state stores our data, 
            // so state.todos refers to our entire todo list)
            return state.todos;
        }
    }
    mutations: {

    }
}

In our code, we will later be able to call getters.todo to retrieve all of our to-do list items. Now we have a store to keep our data, and a way to get our data. Next up let’s look at how we’ll mutate our data.

Mutating Our Data With Vuex

Now let’s think about how our data might change. There are a few ways our data will change:

  1. We could mark a to-do list item as done.
  2. We could add a new to-do list item.
  3. We could delete a to-do list item.
  4. We could archive a to-do list item. As such, we’ll make 4 mutation functions. Let’s start with the first – updateTodo.
mutations: {
    updateTodo (state, todoItem) {
        // the state argument holds all of our data
        // the todoItem argument holds the data about a particular todo list item
        // Let's get all the data from the todoItem
        let id = todoItem.id;
        let completed = todoItem.completed;
        let name = todoItem.name;
        // Let's find the item in our state we are trying to change, by checking for its ID
        let findEl = state.todos.find((x) => x.id == id);
        if(findEl !== null) {
            // If we find it, then we'll update complete or name if those properties exist
            if(completed !== undefined) {
                findEl.completed = completed;
            }
            if(name !== undefined) {
                findEl.name = name;
            }
        }
        else {
            // Otherwise lets console log that the item can't be found for some reason
            console.log(`To Do List Item ${id} couldn't be found`);
        }
    }
}

In the above code, the state will hold our to-do list data, while todoItems will hold the item that is changing. You might be wondering, how do we know which item is changed? When we create our Home.vuepage, we’ll be able to pass data to our mutation to let the function know which item is changing. While designing this, we can think about what data we might need to mutate our state, and then pass that data to the store when we build our frontend.

The other 3 mutation functions we will need are shown below, but they all follow the same principles as updateTodo. Add these to your mutation:{} list.

addTodo (state, todoItem) {
    // Check we have all the right properties to make an element
    if(todoItem.id !== undefined && typeof todoItem.name == 'string' && typeof todoItem.completed == 'boolean') {
        // Push our new element to our store!
        state.todos.push({
            id: todoItem.id,
            name: todoItem.name,
            completed: todoItem.completed,
            location: 'home'
        })
    }
},
deleteTodo (state, todoItem) {
    // Check for the id of the element we want to delete
    let id = todoItem.id;
    let removedEl = state.todos.findIndex((x) => x.id == id);
    if(removedEl !== null) {
        // If it exists, delete it!
        state.todos.splice(removedEl, 1);
    }
},
moveTodoItem (state, todoItem) {
    // Check for the id and location information
    let id = todoItem.id;
    let location = todoItem.location;
    let findEl = state.todos.find((x) => x.id == id);
    // If the item exists, update its location
    if(findEl !== null) {
        findEl.location = location;
    }
    else {
        // Otherwise console log a message
        console.log(`To Do List Item ${id} couldn't be found`);
    }
}

How To Save Vuex Data To Local Storage

Now we have our entire data store set up. We can manipulate and change our store as we need to. The final piece of the puzzle is we need a way to save the changes. Vuex does not persist. If you refresh the page, the data will disappear, which is not what we want. As such, we need to add one more function, which fires any time a mutation occurs. This method is called subscribe. Add it to the bottom of your main.js, just before app.use(router).use(store).mount('#app'):

store.subscribe((mutation, state) => {
    // The code inside the curly brackets fires any time a mutation occurs.
    // When a mutation occurs, we'll stringify our entire state object - which
    // contains our todo list. We'll put it in the users localStorage, so that
    // their data will persist even if they refresh the page.
    localStorage.setItem('store', JSON.stringify(state));
})

Now, it’s one thing to save something in localStorage – it’s another to show it to the user. As such, we need to update our entire Vuex state whenever the page loads. The first thing to do is to make a new mutation which we’ll call loadStore. All this will do is open localStorage, retrieve our data, and set the state of the data store to the value found.

mutations: {
    loadStore() {
        if(localStorage.getItem('store')) {
            try {
                this.replaceState(JSON.parse(localStorage.getItem('store')));
            }
            catch(e) {
                console.log('Could not initialize store', e);
            }
        }
    }
    // ... other mutations
}

We want to run this whenever the app loads, so we can sync our local storage to our Vuex store – so we’ll need to add that to our App.vue file. Change your script to import our store (useStore()), and then we can run our loadStore mutation with commit(). This is the final step to link everything up.

<script>
    import { useStore } from 'vuex'
    export default {
        beforeCreate() {
            // Get our store
            const store = useStore()
            // use store.commit to run any mutation. Below we are running the loadStore mutation
            store.commit('loadStore');
        }
    }
</script>

That’s everything we need for our data. Let’s recap what we’ve done here:

  1. We created a new Vuex store. This is so we can store our to-do list data.
  2. We created a getter method to load any to-do list data from our Vuex store.
  3. We created a number of mutations to manipulate our Vuex store data.
  4. We created a function to put our Vuex store into local storage. We then put this in our App.vue file as well, to ensure our local storage and Vuex store remained in sync. Implementing our to-do list frontend

The hard bit is over, and we can finally start creating our front end. We’ll be making one component for our todo list application – TodoList.vuewhich we’ll put in the src/components folder. Our component will have one property – location, which will let us differentiate between whether we’re on the archive page, or the home page.

Let’s start with the basic Javascript for our component. To begin, let’s import our Vuex store, and put it all within our component’s data() function. Let’s also import UUID, to let us give IDs to our to-do list items. You can install UUID by running the following code:

I’m also going to include a data element called newTodoItem, which we’ll use when we’re adding new to-do list items. Now, our Javascript will look like this:

<script>
    import { useStore } from 'vuex'
    import { v4 as uuidv4 } from 'uuid'

    export default {
        name: "TodoList",
        data() {
            return {
                // Used for adding new todo list items.
                newTodoItem: ''
            }
        },
        props: {
            location: String
        },
        setup() {
            // Open our Vuex store
            const store = useStore()
            // And use our getter to get the data.
            // When we use return {} here, it will
            // pass our todos list data straight to
            // our data() function above.
            return {
                todos: store.getters.todos
            }
        }
    }
</script>

Now all of our stored to-do list data will be within our data() function. You may recall that our todo list items looked a bit like this:

[{ id: 'first-element', name: 'My First To Do Item', completed: false, location: 'home' }]

Given we know the structure of our to-do list items, we can start to display them in our application. Add the following template to your TodoList.vueabove your script tag:

<template>
    <div id="todo-list">
        <div class="list-item" v-for="n in todos" :key="n.id">
            <div class="list-item-holder" v-if="n.location == location" :data-status="n.completed">
                <input type="checkbox" :data-id="n.id" :id="n.id" @click="updateTodo" :checked="n.completed"> <label :data-id="n.id" :for="n.id">{{ n.name }}</label>
                <div class="delete-item" @click="deleteItem" :data-id="n.id">Delete</div>
                <div class="archive-item" v-if="n.location !== 'archive'" @click="archiveItem" :data-id="n.id">Archive</div>
            </div>
        </div>
        <div id="new-todo-list-item">
            <input type="text" id="new-todo-list-item-input" @keyup="updateItemText">
            <input type="submit" id="new-todo-list-item-submit" @click="newItem" value="Add To Do List Item">
        </div>
    </div>
</template>

This is all just normal HTML. At the bottom, we have a few inputs that we’ll use to add new to-do list items. At the top, we’re using the v-for functionality that Vue comes with. With v-for, we can iterate through our array of todo items, and display them all reactively. We’ll use our todo list ID as the key for each, and this is shown by the following line:

<div class="list-item-holder" v-if="n.location == location" :data-status="n.completed">

Remember we said that our component will have a property called location? Well, we only want to show to-do list items where the to-do list item location matches the property. If we’re on the home page, we’d only want to show “home” to-do list items. So the next line does just that, using v-if. If the todo list location, n.location is the same as the property location, then it will show. If it isn’t, it won’t.

<div class="list-item-holder" v-if="n.location == location" :data-status="n.completed">

The next few lines simply pull in the name and ID information from the to-do list item to show it in our application. We’ve also got two more buttons, one to delete, and one to archive our to-do list item. You’ll notice events in Vue shown as @click or @keyup. These fire whenever the user clicks or keys up on that element. The text within is a function we’ll call, but we haven’t defined them yet. As such, let’s start defining our functions so that we can send data back to our Vuex store.

To-Do List Frontend Methods

As we’ve said, we have a number of “events” that will fire whenever the user clicks or marks a to-do list item as done. For example, when they click the checkbox, we run updateTodo. We need to define these functions, though, so let’s do that now. All of our functions (also known as methods) will be stored within our export default {} Javascript, within methods: {}.

Since we’ve initialized our data store, we can access it via this.$store. Remember we defined a bunch of mutation events in our store? We’ll now target those and fire information across to update our store in real-time. Let’s look at one example, updateTodo. Here, we want to change the status of the todo to either done or not done. So we’ll get a new status first, and send it to our Vuex store.

To fire a mutation on the Vuex store, we use store.commit. The first argument will be the mutation we want to fire, and the second is the data we want to send. As such, our method looks like this for updateTodo:

methods: {
    updateTodo: function(e) {
        // Get the new status of our todo list item
        let newStatus = e.currentTarget.parentElement.getAttribute('data-status') == "true" ? false : true;
        // Send this to our store, and fire the mutation on our
        // Vuex store called "updateTodo". Take the ID from the 
        // todo list, and send it along with the current status
        this.$store.commit('updateTodo', {
            id: e.currentTarget.getAttribute('data-id'),
            completed: newStatus
        })
    }
}

The rest of our methods follow the same pattern. Get the ID of the to-do list – and send this along with new data to our store. Our mutation events on our store then update the Vuex store, and since we implemented the subscribe method, it all updates automatically in our local storage. Here are all of our methods, including the methods to add new items:

methods: {
    // As a user types in the input in our template
    // We will update this.newTodoItem. This will then
    // have the full name of the todo item for us to use
    updateItemText: function(e) {
        this.newTodoItem = e.currentTarget.value;
        if(e.keyCode === 13) {
            this.newItem();
        }
        return false;

    },
    updateTodo: function(e) {
        // Get the new status of our todo list item
        let newStatus = e.currentTarget.parentElement.getAttribute('data-status') == "true" ? false : true;
        // Send this to our store, and fire the mutation on our
        // Vuex store called "updateTodo". Take the ID from the 
        // todo list, and send it along with the current status
        this.$store.commit('updateTodo', {
            id: e.currentTarget.getAttribute('data-id'),
            completed: newStatus
        })
    },
    deleteItem: function(e) {
        // This will fire our "deleteTodo" mutation, and delete
        // this todo item according to their ID
        this.$store.commit('deleteTodo', {
            id: e.currentTarget.getAttribute('data-id')
        })
    },
    newItem: function() {
        // If this.newTodoItem has been typed into
        // We will create a new todo item using our
        // "addTodo" mutation
        if(this.newTodoItem !== '') {
            this.$store.commit('addTodo', {
                id: uuidv4(),
                name: this.newTodoItem,
                completed: false
            })
        }
    },
    archiveItem: function(e) {
        // Finally, we can change or archive an item
        // using our "moveTodoItem" mutation
        this.$store.commit('moveTodoItem', {
            id: e.currentTarget.getAttribute('data-id'),
            location: 'archive'
        })
    }
}

Finally, I’ve added some basic styling to cross out items that are marked as complete. Add this just after your final tag:

<style scoped>
    .list-item-holder {
        display: flex;
    }

    [data-status="true"] label {
        text-decoration: line-through;
    }
</style>

Pulling It All Together

We now have a reliable Vuex storeand a TodoList.vue component. The final step is to integrate it into our Home.vue page – and that bit is easy. Simply import the component, and then add it to your Home.vue template:

.

Leave a Comment