Table of Contents
- Introduction
- Prerequisites
- Setting Up the Project
- Building the Todo App
- Styling the Todo App
- Testing the Application
- Deployment (Optional)
- Troubleshooting Common Issues
- Conclusion
- References
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
-
Open your terminal and run the following command to create a new Vue project:
npm create vite@latest todo-app -- --template vuetodo-appis the project name.--template vuespecifies we want a basic Vue template.
-
Navigate into the project folder:
cd todo-app -
Install dependencies:
npm install -
Start the development server:
npm run devYour 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 addTodoList.vuehere).
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.
-
In the
src/componentsfolder, create a file namedTodoList.vue. -
Open
TodoList.vueand 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> -
Update
src/App.vueto use theTodoListcomponent:
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.
-
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 variablenewTodo.@click="addTodo": Triggers theaddTodomethod when the button is clicked.@keyup.enter="addTodo": Allows submitting with the Enter key.
-
Add logic to
script setupto 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 thetodosarray.: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.
-
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> -
Add a
deleteTodomethod 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.
-
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 thecompletedproperty of the todo.:class="{ completed: todo.completed }": Applies thecompletedCSS class whentodo.completedistrue.
-
Add a
toggleCompletemethod (optional—sincev-modelalready updatestodo.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).
- Load todos from
localStoragewhen 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 intodosand saves tolocalStorage.JSON.stringify/JSON.parse: Convert thetodosarray 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:
-
Build the production-ready version:
npm run buildThis creates a
distfolder with optimized files. -
Deploy the
distfolder to a hosting platform like:- Netlify (drag-and-drop
distfolder) - Vercel (connect your GitHub repo)
- GitHub Pages (follow guides to deploy
dist).
- Netlify (drag-and-drop
Troubleshooting Common Issues
- Todos not updating? Ensure you’re using
ref/reactivefor state (Vue’s reactivity system requires this). - localStorage not saving? Check that you’re using
JSON.stringifywhen saving andJSON.parsewhen loading. - v-for errors? Always use a unique
:key(liketodo.id—avoid usingindexfor dynamic lists).
Conclusion
You’ve built a fully functional Todo App with Vue.js! You learned how to:
- Use Vue’s reactivity system with
refandreactive. - 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!