Making a To-do list in Vue

Johnny Simpson - Feb 23 '22 - - Dev Community

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:

How our Vue to-do list will look

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 setup 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, setup 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 
Enter fullscreen mode Exit fullscreen mode

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 { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    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
Enter fullscreen mode Exit fullscreen mode

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:

npm i vuex
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

Vuex allows us to create a store for our data. We'll store our entire todo 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 todo 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 todo 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 todo 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 todo list, we really only need one method - get all of our todo list items. Below, I've configured one default todo list item, and a getter which simply gets all of our todo 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: {

    }
}
Enter fullscreen mode Exit fullscreen mode

In our code, we will later be able to call getters.todo to retrieve all of our todo 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 todo list item as done.
  2. We could add a new todo list item.
  3. We could delete a todo list item.
  4. We could archive a todo 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`);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, state will hold of our todo list data, while todoItems will hold the item that is changing. You might be wondering, how do we know which item is change? 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 within you 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`);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 mutations 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));
})
Enter fullscreen mode Exit fullscreen mode

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 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
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 todo list data.
  2. We created a getter method to load any todo 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.vue, which 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 todo list items. You can install uuid by running the following code:

npm i uuid
Enter fullscreen mode Exit fullscreen mode

I'm also going to include a data element called newTodoItem, which we'll use when we're adding new todo 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>
Enter fullscreen mode Exit fullscreen mode

Now all of our stored todo 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' }]
Enter fullscreen mode Exit fullscreen mode

Given we know the structure of our todo list items, we can start to display them in our application. Add the following template to your TodoList.vue, above 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>
Enter fullscreen mode Exit fullscreen mode

This is all just normal HTML. At the bottom, we have a few inputs which 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 shown by the following line:

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

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">
Enter fullscreen mode Exit fullscreen mode

The next few lines simply pull in the name and ID information from the todo list item to show it in our application. We've also got two more buttons, one to delete, and one to archive our todo 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.

Todo list frontend methods

As we've said, we have a number of "events" which will fire whenever the user clicks or marks a todo 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 the new status first, and send it to our Vuex store.

To fire a mutation on 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
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

The rest of our methods follow the same pattern. Get the ID of the todo 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'
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Pulling it all together

We now have a reliable Vuex store, and 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 into your Home.vue template:

<template>
    <h1>To do List:</h1>
    <TodoList location="home" />
</template>

<script>
import TodoList from '../components/TodoList.vue';

export default { 
    name: "HomePage",
    components: {
        TodoList
    }
}
</script>
Enter fullscreen mode Exit fullscreen mode

And on our archive page, we'll have the same, only our TodoList location will be set to "archive".

<template>
    <TodoList location="archive" />
</template>
Enter fullscreen mode Exit fullscreen mode

Styling our to do application

Now we're done, we can test out our todo list by running the following command, which will let us view it at http://localhost:8080:

npm run serve
Enter fullscreen mode Exit fullscreen mode

We should have a todo list that looks something like this:
How our Vue to-do list will look

I will leave the overall design of the page to you, but I have updated it a little bit to look slightly more modern. All of the styles below will be available in the final code repo. After a bit of work, I landed on this design:
How our vue to-do list will look after styling

Demo

I have set up a demo of how the final application looks on Github Pages. You can find the demo here. Check it out if you want to get a feel for what we'll build.

Conclusion

I hope you've enjoyed this guide on making your to-do list application. As you start to learn more about Vue, it's important to try your own application ideas out, in order to learn more about how it actually works. By working through this example, we've covered a lot of new ideas:

  1. Configuring your router within Vue.
  2. Data stores using Vuex - and how they work.
  3. Interacting with data stores, and making Vuex data stores persist in local storage.
  4. Creating components which interact with Vuex data stores using store.commit.
  5. Implementing those components with custom props into home pages

As always, you can find some useful links below:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .