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

Written by

in

In the world of web development, managing tasks and staying organized is crucial. Whether you’re a seasoned developer juggling multiple projects or a student learning the ropes, a well-designed to-do list application can be an invaluable tool. The challenge often lies in finding a balance between simplicity and functionality. Many complex to-do list apps are bloated with features, making them overwhelming for beginners. Others are so basic that they fail to meet even the most fundamental needs. This article aims to bridge that gap by guiding you through the creation of a simple, yet effective, interactive to-do list using Vue.js. This project will not only teach you the fundamentals of Vue.js but also equip you with a practical application that you can use daily.

Why Build a To-Do List with Vue.js?

Vue.js is a progressive JavaScript framework, meaning you can adopt it incrementally in your projects. It’s known for its approachable learning curve, making it an excellent choice for beginners. Here’s why building a to-do list with Vue.js is a great learning experience:

  • Component-Based Architecture: Vue.js encourages you to break down your UI into reusable components, which is a fundamental concept in modern web development.
  • Data Binding: Vue.js simplifies data binding, allowing you to easily update the UI when the data changes, and vice-versa.
  • Reactivity: Vue.js provides a reactive system that automatically updates the DOM when your data changes.
  • Simplicity: Vue.js is designed to be easy to learn and use, allowing you to focus on the core concepts without getting bogged down in complex configurations.

By building a to-do list, you’ll gain hands-on experience with these core concepts, solidifying your understanding of Vue.js and web development in general. Moreover, you’ll have a functional app that you can customize and expand upon as you learn more.

Prerequisites

Before we dive in, ensure you have the following:

  • A basic understanding of HTML, CSS, and JavaScript: You don’t need to be an expert, but familiarity with these technologies is essential.
  • Node.js and npm (or yarn) installed: These are required to manage project dependencies. You can download Node.js from the official website: https://nodejs.org/.
  • A code editor: Visual Studio Code (VS Code) is highly recommended due to its excellent Vue.js support, but you can use any editor you prefer.

Setting Up Your Vue.js Project

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

npm install -g @vue/cli

This command installs the Vue CLI globally on your system. Once installed, navigate to the directory where you want to create your project and run:

vue create todo-list-app

You will be prompted to choose a preset. Select the default preset (babel, eslint) or manually select features. For simplicity, the default is fine. Navigate into your project directory:

cd todo-list-app

Now, start the development server:

npm run serve

This will start a development server, and your app will be accessible in your web browser, usually at http://localhost:8080/. You should see the Vue.js default welcome page.

Project Structure and Component Breakdown

Before we start coding, let’s understand the basic structure of our to-do list app and break it down into smaller, manageable components. This will improve code organization and maintainability.

Our to-do list app will consist of the following components:

  • App.vue (Root Component): This is the main component and will serve as the container for our entire application.
  • TodoInput.vue: This component will handle the input field where users enter new to-do items.
  • TodoList.vue: This component will display the list of to-do items, including their completion status and delete functionality.
  • TodoItem.vue: This component will be responsible for rendering a single to-do item within the list.

This component structure promotes reusability and makes it easier to understand and modify the code. Now, let’s start building each component.

Creating the TodoInput Component

The TodoInput.vue component will contain an input field and a button to add new to-do items. Create a new file named TodoInput.vue inside the src/components directory. Add the following code:

<template>
 <div class="todo-input">
  <input type="text" v-model="newTodo" placeholder="Add a task" @keyup.enter="addTodo" />
  <button @click="addTodo">Add</button>
 </div>
</template>

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

<style scoped>
 .todo-input {
  display: flex;
  margin-bottom: 10px;
 }

 input {
  flex-grow: 1;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-right: 5px;
 }

 button {
  padding: 8px 12px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
 }

 button:hover {
  background-color: #3e8e41;
 }
</style>

Let’s break down this code:

  • Template: The template defines the structure of the input component, including an input field and a button.
  • v-model=”newTodo”: This directive creates two-way data binding. It binds the input field’s value to the newTodo data property, so when the user types in the input field, the newTodo value is automatically updated, and vice-versa.
  • @keyup.enter=”addTodo”: This is an event listener that triggers the addTodo method when the user presses the Enter key in the input field.
  • @click=”addTodo”: This is an event listener that triggers the addTodo method when the user clicks the “Add” button.
  • Data: The data() function defines the component’s data. In this case, it contains the newTodo property, which stores the value entered in the input field.
  • Methods: The methods object contains the addTodo method, which is called when the user clicks the “Add” button or presses Enter.
  • this.$emit(‘add-todo’, this.newTodo.trim()): This line emits a custom event named add-todo to the parent component (App.vue), passing the trimmed value of newTodo as the payload.
  • this.newTodo = ”: Resets the input field after adding a task.
  • Scoped Styles: The <style scoped> tag contains the CSS styles specific to this component. The scoped attribute ensures that these styles only apply to this component and do not affect other components.

Creating the TodoList Component

The TodoList.vue component will display the list of to-do items. Create a new file named TodoList.vue inside the src/components directory. Add the following code:

<template>
 <ul class="todo-list">
  <todo-item
   v-for="(todo, index) in todos"
   :key="index"
   :todo="todo"
   @delete-todo="deleteTodo(index)"
   @toggle-complete="toggleComplete(index)"
  ></todo-item
  >
 </ul>
</template>

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

 export default {
  components: {
   TodoItem
  },
  props: {
   todos: {
    type: Array,
    required: true
   }
  },
  methods: {
   deleteTodo(index) {
    this.$emit('delete-todo', index);
   },
   toggleComplete(index) {
    this.$emit('toggle-complete', index);
   }
  }
 }
</script>

<style scoped>
 .todo-list {
  list-style: none;
  padding: 0;
 }
</style>

Here’s a breakdown of the code:

  • Template: The template uses a <ul> element to display the list of to-do items.
  • v-for=”(todo, index) in todos”: This directive iterates over the todos array (passed as a prop), rendering a TodoItem component for each to-do item.
  • :key=”index”: The :key attribute is crucial for Vue.js to efficiently update the list. It provides a unique identifier for each item, which helps Vue.js track and re-render only the necessary elements when the data changes.
  • :todo=”todo”: This passes the current to-do item (todo) as a prop to the TodoItem component.
  • @delete-todo=”deleteTodo(index)”: This listens for the delete-todo event emitted by the TodoItem component and calls the deleteTodo method, passing the index of the to-do item to be deleted.
  • @toggle-complete=”toggleComplete(index)”: This listens for the toggle-complete event emitted by the TodoItem component and calls the toggleComplete method, passing the index of the to-do item to toggle its completion status.
  • components: This registers the TodoItem component, making it available for use within this component.
  • props: The props object defines the properties that the component expects to receive from its parent component. In this case, it expects an array named todos.
  • methods: The methods object contains the deleteTodo and toggleComplete methods, which emit custom events to the parent component.

Creating the TodoItem Component

The TodoItem.vue component is responsible for rendering a single to-do item within the list. Create a new file named TodoItem.vue inside the src/components directory. Add the following code:

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

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

<style scoped
 .todo-item {
  display: flex;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid #eee;
 }

 input[type="checkbox"] {
  margin-right: 10px;
 }

 .completed-text {
  text-decoration: line-through;
  color: #888;
  flex-grow: 1;
 }

 .completed {
  opacity: 0.6;
 }

 button {
  padding: 5px 10px;
  background-color: #f44336;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-left: 10px;
 }

 button:hover {
  background-color: #da190b;
 }
</style>

Let’s break down this code:

  • Template: The template defines the structure of a single to-do item, including a checkbox, the text of the to-do item, and a delete button.
  • :class=”{ ‘completed’: todo.completed }”: This dynamically adds the completed class to the <li> element if the todo.completed property is true. This is used to visually indicate whether the to-do item is completed.
  • :checked=”todo.completed”: This binds the checked state of the checkbox to the todo.completed property.
  • @change=”toggleComplete”: This listens for the change event on the checkbox and calls the toggleComplete method when the checkbox’s state changes.
  • :class=”{ ‘completed-text’: todo.completed }”: This dynamically adds the completed-text class to the <span> element if the todo.completed property is true. This is used to visually strike through the text of the completed to-do item.
  • {{ todo.text }}: This displays the text of the to-do item.
  • @click=”deleteTodo”: This listens for the click event on the delete button and calls the deleteTodo method when the button is clicked.
  • props: The props object defines the properties that the component expects to receive from its parent component. In this case, it expects an object named todo.
  • methods: The methods object contains the toggleComplete and deleteTodo methods, which emit custom events to the parent component.

Putting it All Together in App.vue

Now, let’s bring all these components together in the App.vue component, which is the main component of our application. Open src/App.vue and replace its content with the following code:

<template>
 <div id="app">
  <h1>To-Do List</h1>
  <todo-input @add-todo="addTodo"></todo-input>
  <todo-list
   :todos="todos"
   @delete-todo="deleteTodo"
   @toggle-complete="toggleComplete"
  ></todo-list
  >
 </div>
</template>

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

 export default {
  components: {
   TodoInput, 
   TodoList
  },
  data() {
   return {
    todos: []
   }
  },
  mounted() {
   // Load todos from localStorage on component mount
   const savedTodos = localStorage.getItem('todos');
   if (savedTodos) {
    this.todos = JSON.parse(savedTodos);
   }
  },
  watch: {
   todos: {
    handler(newTodos) {
     // Save todos to localStorage whenever they change
     localStorage.setItem('todos', JSON.stringify(newTodos));
    },
    deep: true
   }
  },
  methods: {
   addTodo(newTodoText) {
    this.todos.push({
     text: newTodoText,
     completed: false
    });
   },
   deleteTodo(index) {
    this.todos.splice(index, 1);
   },
   toggleComplete(index) {
    this.todos[index].completed = !this.todos[index].completed;
   }
  }
 }
</script>

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

Let’s break down this code:

  • Template: The template defines the structure of the main application, including a heading, the TodoInput component, and the TodoList component.
  • @add-todo=”addTodo”: This listens for the add-todo event emitted by the TodoInput component and calls the addTodo method, passing the new to-do item’s text.
  • :todos=”todos”: This passes the todos array (from the data) as a prop to the TodoList component.
  • @delete-todo=”deleteTodo”: This listens for the delete-todo event emitted by the TodoList component and calls the deleteTodo method, passing the index of the to-do item to be deleted.
  • @toggle-complete=”toggleComplete”: This listens for the toggle-complete event emitted by the TodoList component and calls the toggleComplete method, passing the index of the to-do item to toggle its completion status.
  • components: This registers the TodoInput and TodoList components, making them available for use within this component.
  • data: The data() function defines the component’s data. In this case, it contains the todos array, which stores the list of to-do items.
  • mounted(): This lifecycle hook is called after the component has been mounted. It attempts to load any saved to-dos from localStorage when the app loads.
  • watch: The watch option allows us to watch for changes to the todos array. The handler function is called whenever the todos array changes, and it saves the updated to-do list to localStorage. The deep: true option ensures that the watcher also detects changes within the objects in the todos array.
  • methods: The methods object contains the addTodo, deleteTodo, and toggleComplete methods, which handle adding, deleting, and toggling the completion status of to-do items, respectively.

Adding Styling

While the application functions, it needs some styling to make it visually appealing. Add the following CSS to the <style> block within the App.vue component:

#app {
 font-family: Avenir, Helvetica, Arial, sans-serif;
 -webkit-font-smoothing: antialiased;
 -moz-osx-font-smoothing: grayscale;
 text-align: center;
 color: #2c3e50;
 margin-top: 60px;
}

Also, add the CSS in the respective <style scoped> blocks of each component (TodoInput.vue, TodoList.vue, and TodoItem.vue) as provided in the component code snippets above. This includes the basic styles for the input field, the button, the list, and the to-do items themselves.

Testing and Running Your Application

Now that you’ve built all the components and integrated them into App.vue, it’s time to test your application. Run the following command in your terminal if you haven’t already:

npm run serve

Open your web browser and navigate to http://localhost:8080/. You should see your to-do list application. Try the following:

  • Enter a new task in the input field and click the “Add” button (or press Enter).
  • The new task should appear in the to-do list.
  • Click the checkbox next to a task to mark it as complete (or incomplete).
  • Click the “Delete” button next to a task to remove it from the list.
  • Refresh the page. Your to-do items should still be there, thanks to the local storage implementation.

If everything works as expected, congratulations! You’ve successfully built a simple, interactive to-do list application with Vue.js.

Common Mistakes and How to Fix Them

While building this application, you might encounter some common mistakes. Here’s a list of potential issues and how to resolve them:

  • Incorrect Component Import: Make sure you import components correctly in your App.vue file. For example, use import TodoInput from './components/TodoInput.vue';.
  • Data Binding Issues: Double-check that you’re using v-model correctly to bind input fields to data properties. Ensure the data properties are defined in the data() function.
  • Event Handling Problems: Verify that you’re correctly using event listeners (e.g., @click, @keyup.enter) and that your methods are correctly defined and called.
  • Prop Issues: If you’re passing props to child components, make sure you’ve defined the props in the child component’s props option and that you’re passing the props correctly from the parent component.
  • Key Attributes for v-for: Always include the :key attribute when using v-for to improve performance and avoid unexpected behavior.
  • Scope of Styles: If your styles aren’t applying correctly, check if you’ve added the scoped attribute to the <style> tag in your components. If you want global styles, remove the scoped attribute.
  • Local Storage Issues: Ensure you are using JSON.stringify() when saving to local storage and JSON.parse() when retrieving from local storage. Also, check for any errors in the browser’s console.

Key Takeaways and Next Steps

You’ve successfully built a functional to-do list application using Vue.js. Here are the key takeaways from this project:

  • Component-Based Development: You’ve learned how to break down a complex UI into smaller, reusable components.
  • Data Binding and Reactivity: You’ve gained hands-on experience with Vue.js’s data binding and reactivity features.
  • Event Handling: You’ve learned how to handle user interactions using event listeners.
  • Props and Events: You’ve understood how to pass data between components using props and events.
  • Local Storage: You’ve learned how to persist data using local storage.

Now that you’ve completed this project, you can extend your to-do list app with additional features:

  • Adding Editing Functionality: Allow users to edit existing to-do items.
  • Implementing Due Dates: Add due dates to your tasks.
  • Adding Categories/Tags: Categorize tasks for better organization.
  • Implementing Filtering and Sorting: Allow users to filter and sort their tasks (e.g., by due date, priority, or completion status).
  • Using a Backend: Connect your app to a backend database to store the data more permanently.
  • Adding User Authentication: Allow multiple users to use the app.

FAQ

Q: Why is it important to use a key attribute with v-for?

A: The key attribute is crucial for Vue.js to efficiently update the list. It provides a unique identifier for each item. This helps Vue.js track and re-render only the necessary elements when the data changes, improving performance and avoiding unexpected behavior.

Q: How can I debug my Vue.js application?

A: You can use your browser’s developer tools (usually accessed by pressing F12). The Vue.js devtools browser extension is also incredibly useful for debugging Vue.js applications. It allows you to inspect component data, props, and events directly in your browser.

Q: Can I use this to-do list app on my phone?

A: Yes, this app is responsive and should work on mobile devices. You can also deploy it to a platform like Netlify or Vercel for easy access.

Q: What is the difference between data, props, and computed properties in Vue.js?

A: data holds the component’s internal state, props are used to pass data from parent components to child components, and computed properties are used to derive values based on the component’s data.

Conclusion

Building a to-do list application with Vue.js is a fantastic way to learn the fundamentals of this powerful framework. By breaking down the project into manageable components and understanding the core concepts of Vue.js, you’ve taken your first steps into the world of front-end development. Remember, the best way to learn is by doing. Continue to experiment, build new features, and explore the vast possibilities that Vue.js offers. With each project, you’ll gain a deeper understanding of the framework and become more proficient in building interactive and engaging web applications. The skills you’ve acquired here are transferable and will serve as a solid foundation for your future web development endeavors. Keep coding, keep learning, and keep building!