javascriptroom guide

How to Create a Todo App with Vue.js

In today’s fast-paced world, staying organized is key—and what better way to learn a new framework than by building a practical tool? A **Todo App** is a classic project that teaches core programming concepts like state management, user input handling, and data persistence. In this tutorial, we’ll build a fully functional Todo App using **Vue.js**—a progressive JavaScript framework known for its simplicity, reactivity, and ease of integration. By the end, you’ll have a Todo App that lets users: - Add new todos - Mark todos as completed - Delete todos - Persist todos across page refreshes (using `localStorage`) We’ll use Vue 3 (the latest version) with the Composition API and `<script setup>` syntax for a modern, concise development experience. Let’s dive in!

Table of Contents

Prerequisites

Before starting, ensure you have the following tools installed:

  • Node.js (v14.18+ or later): Vue projects rely on Node.js for package management. Download it from nodejs.org.
  • npm or Yarn: These are package managers included with Node.js (npm) or can be installed separately (Yarn).
  • Code Editor: We recommend VS Code with the Volar extension for Vue syntax highlighting and IntelliSense.

Setting Up the Project

We’ll use Vite (a fast build tool) to scaffold our Vue project. Vite is the official recommendation for Vue 3 and offers faster development times than the older Vue CLI.

Using Vite to Create a Vue Project

  1. Open your terminal and run the following command to create a new Vue project:

    npm create vite@latest todo-app -- --template vue  
    • todo-app is the project name.
    • --template vue specifies we want a basic Vue template.
  2. Navigate into the project folder:

    cd todo-app  
  3. Install dependencies:

    npm install  
  4. Start the development server:

    npm run dev  

    Your browser should open to http://localhost:5173, showing a default Vue welcome page.

Project Structure Overview

Let’s familiarize ourselves with the key files in the todo-app folder:

  • src/main.js: The entry point of the app, where Vue is initialized.
  • src/App.vue: The root component, which will render our TodoList component.
  • src/components/: Folder for reusable components (we’ll add TodoList.vue here).

Building the Todo App

We’ll build the app step-by-step, starting with a basic component and adding features like adding, deleting, and persisting todos.

Step 1: Creating the TodoList Component

First, create a new component to hold our todo logic.

  1. In the src/components folder, create a file named TodoList.vue.

  2. Open TodoList.vue and add the basic component structure:

    <template>  
      <div class="todo-app">  
        <h1>Todo App</h1>  
        <!-- We'll add input, buttons, and todo list here -->  
      </div>  
    </template>  
    
    <script setup>  
    // Logic will go here  
    </script>  
    
    <style scoped>  
    /* Styling will go here */  
    </style>  
  3. Update src/App.vue to use the TodoList component:
    Replace the default content with:

    <template>  
      <TodoList />  
    </template>  
    
    <script setup>  
    import TodoList from './components/TodoList.vue'  
    </script>  
    
    <style>  
    /* Global styles (optional) */  
    body {  
      font-family: Arial, sans-serif;  
      max-width: 600px;  
      margin: 0 auto;  
      padding: 20px;  
    }  
    </style>  

Step 2: Adding Todo Functionality

Let’s add an input field to type todos and a button to submit them.

  1. In TodoList.vue’s template, add an input and button:

    <template>  
      <div class="todo-app">  
        <h1>Todo App</h1>  
        <div class="todo-input">  
          <input  
            v-model="newTodo"  
            placeholder="Add a new todo..."  
            @keyup.enter="addTodo"  
          />  
          <button @click="addTodo">Add</button>  
        </div>  
      </div>  
    </template>  
    • v-model="newTodo": Binds the input value to a reactive variable newTodo.
    • @click="addTodo": Triggers the addTodo method when the button is clicked.
    • @keyup.enter="addTodo": Allows submitting with the Enter key.
  2. Add logic to script setup to manage the todo state:

    <script setup>  
    import { ref } from 'vue'  
    
    // Reactive variable for the input field  
    const newTodo = ref('')  
    
    // Reactive array to store todos (each todo is an object)  
    const todos = ref([  
      // Example initial todo (optional)  
      // { id: 1, text: 'Learn Vue.js', completed: false }  
    ])  
    
    // Method to add a new todo  
    const addTodo = () => {  
      if (newTodo.value.trim()) { // Avoid empty todos  
        todos.value.push({  
          id: Date.now(), // Unique ID using timestamp  
          text: newTodo.value.trim(),  
          completed: false // Track completion status  
        })  
        newTodo.value = '' // Clear input after adding  
      }  
    }  
    </script>  

Step 3: Displaying Todos

Now, let’s render the list of todos.

In the template, add a <ul> below the input to loop through todos with v-for:

<template>  
  <div class="todo-app">  
    <h1>Todo App</h1>  
    <div class="todo-input">  
      <!-- Input and button (from Step 2) -->  
    </div>  
    <ul class="todo-list">  
      <li v-for="todo in todos" :key="todo.id" class="todo-item">  
        {{ todo.text }}  
      </li>  
    </ul>  
  </div>  
</template>  
  • v-for="todo in todos": Loops through the todos array.
  • :key="todo.id": Required for Vue to track list items efficiently (use a unique ID).

Step 4: Deleting Todos

Add a “Delete” button next to each todo to remove it from the list.

  1. Update the <li> in the template to include a delete button:

    <li v-for="todo in todos" :key="todo.id" class="todo-item">  
      {{ todo.text }}  
      <button @click="deleteTodo(todo.id)" class="delete-btn">×</button>  
    </li>  
  2. Add a deleteTodo method to the script:

    <script setup>  
    // ... (previous code: ref, todos, addTodo)  
    
    const deleteTodo = (id) => {  
      // Filter out the todo with the matching ID  
      todos.value = todos.value.filter(todo => todo.id !== id)  
    }  
    </script>  

Step 5: Toggling Todo Completion

Let’s add checkboxes to mark todos as “completed” and style them differently.

  1. Update the <li> to include a checkbox:

    <li v-for="todo in todos" :key="todo.id" class="todo-item">  
      <input  
        type="checkbox"  
        v-model="todo.completed"  
        @change="toggleComplete(todo.id)"  
      />  
      <span :class="{ completed: todo.completed }">{{ todo.text }}</span>  
      <button @click="deleteTodo(todo.id)" class="delete-btn">×</button>  
    </li>  
    • v-model="todo.completed": Binds the checkbox to the completed property of the todo.
    • :class="{ completed: todo.completed }": Applies the completed CSS class when todo.completed is true.
  2. Add a toggleComplete method (optional—since v-model already updates todo.completed, but explicit methods help with debugging):

    <script setup>  
    // ... (previous code)  
    
    const toggleComplete = (id) => {  
      const todo = todos.value.find(todo => todo.id === id)  
      if (todo) todo.completed = !todo.completed  
    }  
    </script>  

Step 6: Persisting Todos with LocalStorage

To keep todos from disappearing when the page reloads, we’ll use localStorage (a browser API for storing data locally).

  1. Load todos from localStorage when the component mounts:
    <script setup>  
    import { ref, onMounted, watch } from 'vue'  
    
    // ... (previous code: newTodo, todos, addTodo, deleteTodo, toggleComplete)  
    
    // Load todos from localStorage on component mount  
    onMounted(() => {  
      const savedTodos = localStorage.getItem('todos')  
      if (savedTodos) todos.value = JSON.parse(savedTodos)  
    })  
    
    // Save todos to localStorage whenever they change  
    watch(todos, (newTodos) => {  
      localStorage.setItem('todos', JSON.stringify(newTodos))  
    }, { deep: true }) // Watch for nested changes (e.g., todo.completed)  
    </script>  
    • onMounted: Vue lifecycle hook that runs after the component is mounted.
    • watch: Reacts to changes in todos and saves to localStorage.
    • JSON.stringify/JSON.parse: Convert the todos array to/from a string (localStorage only stores strings).

Styling the Todo App

Add CSS to TodoList.vue’s <style scoped> section to make the app look clean:

<style scoped>  
.todo-app {  
  max-width: 500px;  
  margin: 0 auto;  
  padding: 20px;  
}  

h1 {  
  text-align: center;  
  color: #333;  
}  

.todo-input {  
  display: flex;  
  gap: 10px;  
  margin-bottom: 20px;  
}  

input {  
  flex: 1;  
  padding: 10px;  
  font-size: 16px;  
  border: 1px solid #ddd;  
  border-radius: 4px;  
}  

button {  
  padding: 10px 20px;  
  background: #42b983; /* Vue's brand color */  
  color: white;  
  border: none;  
  border-radius: 4px;  
  cursor: pointer;  
}  

button:hover {  
  background: #359469;  
}  

.todo-list {  
  list-style: none;  
  padding: 0;  
}  

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

.todo-item input[type="checkbox"] {  
  width: 18px;  
  height: 18px;  
}  

.completed {  
  text-decoration: line-through;  
  color: #666;  
}  

.delete-btn {  
  margin-left: auto;  
  background: #ff4444;  
  padding: 5px 10px;  
  font-size: 14px;  
}  

.delete-btn:hover {  
  background: #cc0000;  
}  
</style>  

Testing the Application

Run the development server again (npm run dev) and test the following:

  • Add a todo: Type text and click “Add” or press Enter.
  • Mark as completed: Check the checkbox next to a todo (text should strikethrough).
  • Delete a todo: Click the ”×” button.
  • Persist data: Refresh the page—todos should still appear!

Deployment (Optional)

To share your app with others:

  1. Build the production-ready version:

    npm run build  

    This creates a dist folder with optimized files.

  2. Deploy the dist folder to a hosting platform like:

Troubleshooting Common Issues

  • Todos not updating? Ensure you’re using ref/reactive for state (Vue’s reactivity system requires this).
  • localStorage not saving? Check that you’re using JSON.stringify when saving and JSON.parse when loading.
  • v-for errors? Always use a unique :key (like todo.id—avoid using index for dynamic lists).

Conclusion

You’ve built a fully functional Todo App with Vue.js! You learned how to:

  • Use Vue’s reactivity system with ref and reactive.
  • Create components and manage state.
  • Handle user input and events.
  • Persist data with localStorage.

To expand the app, try adding:

  • Due dates or categories for todos.
  • Filtering (All/Active/Completed).
  • Editing existing todos.

Vue’s simplicity and reactivity make it easy to extend apps—explore the Vue docs to learn more!

References