Making a Vue To-do List App
📣 Sponsor
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 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 core Javascript
Routers
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
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
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 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: {
}
}
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:
- We could mark a todo list item as done.
- We could add a new todo list item.
- We could delete a todo list item.
- 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`);
}
}
}
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.vue
page, 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`);
}
}
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));
})
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
}
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:
- We created a new Vuex store. This is so we can store our todo list data.
- We created a getter method to load any todo list data from our Vuex store.
- We created a number of mutations to manipulate our Vuex store data.
- 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
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>
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' }]
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:
<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"></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>
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">
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 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
})
}
}
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'
})
}
}
Finally, I've added some basic styling to cross out items that are marked as complete. Add this just after your final </script>
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 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>
And on our archive page, we'll have the same, only our TodoList
location will be set to "archive".
<template>
<TodoList location="archive" />
</template>
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
We should have a todo list that looks something like this:
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:
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:
- Configuring your router within Vue.
- Data stores using Vuex - and how they work.
- Interacting with data stores, and making Vuex data stores persist in local storage.
- Creating components which interact with Vuex data stores using
store.commit
. - Implementing those components with custom props into home pages
As always, you can find some useful links below:
- The full code available on Github
- A guide to making your first Vue application
- More Vue Content
- A demo of the application
More Tips and Tricks for Vue
- How to use Teleport in Vue to Move Parts of Templates
- How Vue Components Work
- How v-if and v-else work in Vue
- A Guide on How to use Emit in Vue
- Creating a Reusable Tab Component in Vue
- v-show vs v-if: Conditional Rendering in Vue
- How to give Props Default Values in Vue
- Using .env Environment Variables in Vue
- How to use v-for in Vue
- Making a Vue To-do List App