Building a Simple Vue.js Interactive To-Do List with Local Storage: A Beginner’s Guide

Written by

in

Are you looking to dive into the world of web development with Vue.js? One of the best ways to learn is by building practical projects. A to-do list is a classic project, and it’s perfect for beginners because it introduces core concepts like data binding, event handling, and component composition. Furthermore, by incorporating local storage, you can learn how to persist data, making your to-do list even more functional and useful. This guide will walk you through, step-by-step, how to create an interactive to-do list app with Vue.js, complete with the ability to add, delete, and mark tasks as complete, all while saving your data in the browser’s local storage. Let’s get started!

Why Build a To-Do List?

A to-do list might seem simple, but it’s an excellent project for learning the fundamentals of any JavaScript framework, including Vue.js. Building a to-do list allows you to:

  • Understand Data Binding: See how data updates in the UI when the underlying data changes.
  • Master Event Handling: Learn how to respond to user interactions, such as clicking buttons or submitting forms.
  • Work with Components: Break down your application into reusable and manageable components.
  • Explore Local Storage: Discover how to save and retrieve data in the user’s browser, making your app persistent.

Moreover, the project is self-contained. You don’t need a backend server to store your data, which simplifies the learning process and allows you to focus on the front-end aspects of Vue.js.

Prerequisites

Before you begin, make sure you have the following:

  • Basic HTML, CSS, and JavaScript knowledge: You don’t need to be an expert, but familiarity with these technologies is essential.
  • Node.js and npm (or yarn) installed: You’ll need these to set up your Vue.js project. You can download Node.js from nodejs.org.
  • A code editor: Visual Studio Code, Sublime Text, or any other editor of your choice.

Setting Up Your Vue.js Project

Let’s start by creating a new Vue.js project using the Vue CLI (Command Line Interface). Open your terminal or command prompt and run the following commands:

npm install -g @vue/cli
vue create todo-list-app

During the project creation process, you’ll be prompted to choose a preset. Select the default preset (babel, eslint). Navigate into your project directory:

cd todo-list-app

Now, start the development server:

npm run serve

This will start a development server, and you can access your app in your browser at http://localhost:8080/ (or a similar address). You should see the default Vue.js welcome screen.

Project Structure

Before we start writing code, let’s briefly understand the project structure created by the Vue CLI:

  • src/: This directory contains your source code.
  • src/components/: This directory will hold your reusable Vue components.
  • src/App.vue: This is the root component of your application.
  • src/main.js: This is the entry point of your application.
  • public/: This directory contains static assets like your index.html file.

Building the To-Do List Components

Let’s create the components for our to-do list. We’ll need at least two components: a component to display each individual to-do item and a component to handle the form for adding new tasks.

1. Creating the TodoItem Component

Create a new file named TodoItem.vue inside the src/components directory. Add the following code:

<template>
  <li :class="{ completed: todo.completed }">
    <input type="checkbox" :checked="todo.completed" @change="toggleComplete">
    <span>{{ todo.text }}</span>
    <button @click="deleteTodo">Delete</button>
  </li>
</template>

<script>
export default {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },
  methods: {
    toggleComplete() {
      this.$emit('toggle', this.todo.id);
    },
    deleteTodo() {
      this.$emit('delete', this.todo.id);
    }
  }
}
</script>

<style scoped>
.completed {
  text-decoration: line-through;
}
</style>

Let’s break down this component:

  • Template: Defines the structure of each to-do item. It includes a checkbox for marking the task as complete, the task text, and a delete button. The :class="{ completed: todo.completed }" part dynamically adds the ‘completed’ class to the <li> element if the todo.completed property is true.
  • Script: Defines the component’s logic.
    • props: This section declares the properties that the component will receive from its parent. In this case, it expects a todo object.
    • methods: This section contains the methods for handling events.
      • toggleComplete(): Emits a custom event called ‘toggle’ with the ID of the to-do item when the checkbox is clicked. This is how the parent component will know to update the todo item’s completion status.
      • deleteTodo(): Emits a custom event called ‘delete’ with the ID of the to-do item when the delete button is clicked. This will trigger the deletion of the item from the parent’s data.
  • Style: Defines the styles for the component. The scoped attribute ensures that these styles only apply to this component.

2. Creating the TodoForm Component

Create a new file named TodoForm.vue inside the src/components directory. Add the following code:

<template>
  <form @submit.prevent="addTodo">
    <input type="text" v-model="newTodoText" placeholder="Add a task">
    <button type="submit">Add</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      newTodoText: ''
    }
  },
  methods: {
    addTodo() {
      if (this.newTodoText.trim() !== '') {
        this.$emit('add', this.newTodoText.trim());
        this.newTodoText = '';
      }
    }
  }
}
</script>

Explanation:

  • Template: Defines the form with an input field and an add button. The v-model directive binds the input field’s value to the newTodoText data property. The @submit.prevent="addTodo" prevents the default form submission behavior (page refresh) and calls the addTodo method.
  • Script: Defines the component’s logic.
    • data(): Initializes the newTodoText data property, which holds the text entered in the input field.
    • addTodo(): Emits a custom event called ‘add’ with the trimmed text from the input field when the form is submitted. It also clears the input field after adding the task.

3. Modifying the App.vue Component

Now, let’s modify the App.vue component to use these components and manage the to-do list data. Replace the contents of src/App.vue with the following code:

<template>
  <div id="app">
    <h1>To-Do List</h1>
    <TodoForm @add="addTodo"></TodoForm>
    <ul>
      <TodoItem
        v-for="todo in todos"
        :key="todo.id"
        :todo="todo"
        @toggle="toggleComplete"
        @delete="deleteTodo"
      ></TodoItem>
    </ul>
  </div>
</template>

<script>
import TodoItem from './components/TodoItem.vue';
import TodoForm from './components/TodoForm.vue';

export default {
  components: {
    TodoItem, TodoForm
  },
  data() {
    return {
      todos: []
    }
  },
  mounted() {
    this.loadTodos();
  },
  methods: {
    addTodo(text) {
      const newTodo = {
        id: Date.now(),
        text: text,
        completed: false
      };
      this.todos.push(newTodo);
      this.saveTodos();
    },
    toggleComplete(id) {
      const todo = this.todos.find(todo => todo.id === id);
      if (todo) {
        todo.completed = !todo.completed;
        this.saveTodos();
      }
    },
    deleteTodo(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
      this.saveTodos();
    },
    saveTodos() {
      localStorage.setItem('todos', JSON.stringify(this.todos));
    },
    loadTodos() {
      const storedTodos = localStorage.getItem('todos');
      if (storedTodos) {
        this.todos = JSON.parse(storedTodos);
      }
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.completed {
  text-decoration: line-through;
  color: #999;
}
</style>

Here’s what’s happening in App.vue:

  • Importing Components: Imports the TodoItem and TodoForm components.
  • Registering Components: Registers the imported components in the components object.
  • Data: Initializes the todos array, which will hold our to-do list items.
  • mounted(): The mounted lifecycle hook is called after the component is mounted (added to the DOM). It calls the loadTodos method to retrieve any saved to-do items from local storage.
  • Methods:
    • addTodo(text): This method is triggered when the TodoForm component emits the ‘add’ event. It creates a new to-do object, adds it to the todos array, and then calls saveTodos.
    • toggleComplete(id): This method is triggered when the TodoItem component emits the ‘toggle’ event. It finds the to-do item with the matching ID and toggles its completed property. It then calls saveTodos.
    • deleteTodo(id): This method is triggered when the TodoItem component emits the ‘delete’ event. It filters the todos array to remove the to-do item with the matching ID. It then calls saveTodos.
    • saveTodos(): This method saves the todos array to local storage. It uses JSON.stringify() to convert the array into a JSON string before storing it.
    • loadTodos(): This method loads the to-do items from local storage. It uses JSON.parse() to convert the JSON string back into a JavaScript array.
  • Template: The template displays the to-do list.
    • It includes the TodoForm component and listens for the ‘add’ event, calling the addTodo method when the event is emitted.
    • It uses v-for to iterate over the todos array and render a TodoItem component for each to-do item.
    • It passes the todo object as a prop to the TodoItem component.
    • It listens for the ‘toggle’ and ‘delete’ events from the TodoItem component, calling the toggleComplete and deleteTodo methods, respectively.

Adding Functionality

Now, let’s test our application. Start the development server (npm run serve) if it’s not already running. Open your browser and go to http://localhost:8080/. You should see:

  • An input field and an “Add” button.
  • An empty list.

Adding Tasks: Type a task into the input field and click the “Add” button. The task should appear in the list. If you refresh the page, the task will still be there, thanks to local storage.

Marking Tasks as Complete: Click the checkbox next to a task. The task text should be crossed out.

Deleting Tasks: Click the “Delete” button next to a task. The task should be removed from the list.

Common Mistakes and How to Fix Them

Here are some common mistakes beginners make when building a to-do list app and how to avoid them:

  • Not Using v-model Correctly: Make sure you’re using v-model correctly to bind the input field’s value to a data property. This is crucial for two-way data binding. If the input field doesn’t update when you type, or the data doesn’t update when you change the input field, double-check your v-model bindings.
  • Incorrect Event Handling: Ensure that your event handlers are correctly defined and that you’re passing the correct data to them. For example, when emitting custom events from child components, make sure you’re emitting the correct data (like the to-do item’s ID).
  • Forgetting to Save to Local Storage: If your to-do list doesn’t persist after a page refresh, you likely forgot to save the todos array to local storage whenever you add, update, or delete a task. Double-check your saveTodos method.
  • Incorrect Data Types in Local Storage: Local storage only stores strings. You must convert JavaScript objects and arrays to JSON strings using JSON.stringify() before saving them to local storage, and convert them back to JavaScript objects using JSON.parse() when retrieving them.
  • Not Using Scoped Styles: If your styles are affecting other parts of your application, you might have forgotten to add the scoped attribute to your <style> tags in your components. This ensures that the styles are only applied to the component’s elements.

Key Takeaways and Next Steps

You’ve successfully built a basic, yet functional, to-do list app with Vue.js! You’ve learned about components, data binding, event handling, and local storage. Here’s a summary of the key concepts:

  • Components: Vue.js apps are built from reusable components.
  • Data Binding: Use v-model to bind input fields to data properties.
  • Event Handling: Use @event to listen for events and trigger methods.
  • Props: Pass data from parent to child components using props.
  • Custom Events: Emit custom events from child components to communicate with parent components.
  • Local Storage: Use localStorage to persist data in the browser.

Now that you’ve completed this project, consider these next steps to further enhance your skills:

  • Add Edit Functionality: Allow users to edit existing tasks.
  • Implement Filtering and Sorting: Add features to filter tasks by status (e.g., “Active,” “Completed”) and sort them by date or priority.
  • Use a UI Library: Explore UI component libraries like Vuetify or Element UI to create a more polished user interface.
  • Integrate with a Backend: Learn how to fetch and save your to-do list data from a backend server (e.g., using Node.js and Express).
  • Explore Vuex or Pinia: For larger applications, consider using a state management library like Vuex or Pinia to manage your application’s state more effectively.

As you build more projects, you’ll become more comfortable with Vue.js and web development in general. Keep practicing, experimenting, and building! The more you build, the better you’ll become.