DEV Community

Cover image for Simple To-Do List Application in Vue.js 3: Step-by-Step Guide 2025
Chabba Saad
Chabba Saad

Posted on • Edited on

Simple To-Do List Application in Vue.js 3: Step-by-Step Guide 2025

This guide will walk you through building a basic To-Do List application using Vue.js 3. By the end of this tutorial, you'll understand how to handle data binding, event handling, and dynamic rendering in Vue.js.

Prerequisites

Basic knowledge of HTML, CSS, and JavaScript.
Node.js and npm installed on your computer.
Vue CLI installed. If not, you can install it by running npm install -g @vue/cli.

Step 1: Setting Up the Project

Create a New Vue.js Project: Open your terminal and run the following command to create a new Vue.js 3 project:



vue create todo-app


Enter fullscreen mode Exit fullscreen mode

Navigate into the project directory:



cd todo-app


Enter fullscreen mode Exit fullscreen mode

Run the Development Server: Start the Vue development server:



npm run serve


Enter fullscreen mode Exit fullscreen mode

This will open the default Vue.js starter template in your browser at http://localhost:8080.

Step 2: Structuring the Application

Clean Up the Default Template: Open src/App.vue and remove the default template, script, and style contents. Replace them with the following code:



<template>
  <div id="app">
    <h1>Vue.js To-Do List</h1>
    <input v-model="newTask" placeholder="Add a new task" @keyup.enter="addTask" />
    <button @click="addTask">Add Task</button>
    <ul>
      <li v-for="(task, index) in tasks" :key="index">
        <input type="checkbox" v-model="task.completed" />
        <span :class="{ 'completed-task': task.completed }">{{ task.text }}</span>
        <button @click="removeTask(index)">Delete</button>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      newTask: '',
      tasks: []
    };
  },
  methods: {
    addTask() {
      if (this.newTask.trim() !== '') {
        this.tasks.push({ text: this.newTask, completed: false });
        this.newTask = '';
      }
    },
    removeTask(index) {
      this.tasks.splice(index, 1);
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
.completed-task {
  text-decoration: line-through;
  color: grey;
}
input {
  margin-bottom: 10px;
  padding: 5px;
}
button {
  margin-left: 5px;
  padding: 5px;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 5px 0;
}
</style>



Enter fullscreen mode Exit fullscreen mode

Step 3: Understanding the Code

Template Section:

<input v-model="newTask" placeholder="Add a new task" @keyup.enter="addTask" />
: This input field is bound to the newTask data property using v-model, enabling two-way data binding. The @keyup.enter="addTask" listens for the Enter key to trigger the addTask method.

<button @click="addTask">Add Task</button>
: This button triggers the addTask method when clicked.

<ul>: This unordered list uses v-for to loop through the tasks array and render each task. The :key="index" is a unique identifier for each item, required by Vue to track list items efficiently.

<input type="checkbox" v-model="task.completed" />: A checkbox that toggles the completed status of a task.

<span :class="{ 'completed-task': task.completed }">{{ task.text }}</span> : The span element displays the task text. The :class directive conditionally applies the completed-task CSS class based on whether the task is completed.

<button @click="removeTask(index)">Delete</button> : A button to remove the task from the list

Script Section:

data() Method:
newTask: A string that holds the value of the new task to be added.
tasks: An array that holds all tasks, each represented as an object with text and completed properties.

methods Object:
addTask(): This method adds the newTask to the tasks array and then clears the newTask input field.
removeTask(index): This method removes a task from the tasks array based on its index.

Style Section:

Basic CSS to style the to-do list, with a .completed-task class to strike through completed tasks.

Persist Tasks in Local Storage: You can enhance the app by saving tasks in the browser's local storage, so they persist across page reloads.



mounted() {
  this.tasks = JSON.parse(localStorage.getItem('tasks')) || [];
},
methods: {
  addTask() {
    if (this.newTask.trim() !== '') {
      this.tasks.push({ text: this.newTask, completed: false });
      this.newTask = '';
      localStorage.setItem('tasks', JSON.stringify(this.tasks));
    }
  },
  removeTask(index) {
    this.tasks.splice(index, 1);
    localStorage.setItem('tasks', JSON.stringify(this.tasks));
  }
}


Enter fullscreen mode Exit fullscreen mode

Editing Tasks:



<template>
  <div id="app">
    <h1>Vue.js To-Do List</h1>
    <input v-model="newTask" placeholder="Add a new task" @keyup.enter="addTask" />
    <button @click="addTask">Add Task</button>
    <ul>
      <li v-for="(task, index) in tasks" :key="index">
        <input type="checkbox" v-model="task.completed" />

        <span v-if="!task.isEditing" :class="{ 'completed-task': task.completed }">{{ task.text }}</span>
        <input v-else v-model="task.text" @keyup.enter="saveTask(task)" @blur="saveTask(task)" />

        <button v-if="!task.isEditing" @click="editTask(task)">Edit</button>
        <button @click="removeTask(index)">Delete</button>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      newTask: '',
      tasks: []
    };
  },
  methods: {
    addTask() {
      if (this.newTask.trim() !== '') {
        this.tasks.push({ text: this.newTask, completed: false, isEditing: false });
        this.newTask = '';
      }
    },
    removeTask(index) {
      this.tasks.splice(index, 1);
    },
    editTask(task) {
      task.isEditing = true;
    },
    saveTask(task) {
      task.isEditing = false;
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
.completed-task {
  text-decoration: line-through;
  color: grey;
}
input {
  margin-bottom: 10px;
  padding: 5px;
}
button {
  margin-left: 5px;
  padding: 5px;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 5px 0;
}
</style>



Enter fullscreen mode Exit fullscreen mode

Install Font Awesome

First, include Font Awesome in your project. If you're using a CDN, you can add this link in your HTML file or directly in the

section of your index.html.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">

** Update the Template with Icons
**

Now, let's add some Font Awesome icons for editing, deleting, and completing tasks.



<template>
  <div id="app" class="container">
    <h1>Vue.js To-Do List</h1>
    <div class="input-container">
      <input v-model="newTask" placeholder="Add a new task" @keyup.enter="addTask" />
      <button @click="addTask" class="btn btn-add"><i class="fas fa-plus"></i></button>
    </div>
    <ul>
      <li v-for="(task, index) in tasks" :key="index" :class="{ completed: task.completed }">
        <input type="checkbox" v-model="task.completed" id="checkbox" />

        <span v-if="!task.isEditing" class="task-text">{{ task.text }}</span>
        <input v-else v-model="task.text" @keyup.enter="saveTask(task)" @blur="saveTask(task)" class="edit-input" />

        <div class="actions">
          <button v-if="!task.isEditing" @click="editTask(task)" class="btn btn-edit"><i class="fas fa-edit"></i></button>
          <button @click="removeTask(index)" class="btn btn-delete"><i class="fas fa-trash-alt"></i></button>
        </div>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      newTask: '',
      tasks: []
    };
  },
  methods: {
    addTask() {
      if (this.newTask.trim() !== '') {
        this.tasks.push({ text: this.newTask, completed: false, isEditing: false });
        this.newTask = '';
      }
    },
    removeTask(index) {
      this.tasks.splice(index, 1);
    },
    editTask(task) {
      task.isEditing = true;
    },
    saveTask(task) {
      task.isEditing = false;
    }
  }
};
</script>

<style>
body {
  background-color: #f4f7f6;
  font-family: 'Roboto', sans-serif;
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

.container {
  background-color: #ffffff;
  border-radius: 10px;
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
  padding: 20px;
  max-width: 400px;
  width: 100%;
}

h1 {
  color: #333;
  margin-bottom: 20px;
}

.input-container {
  display: flex;
  margin-bottom: 20px;
}

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

.btn {
  background-color: #4caf50;
  color: white;
  border: none;
  padding: 10px;
  border-radius: 5px;
  cursor: pointer;
  margin-left: 5px;
  transition: background-color 0.3s;
}

.btn:hover {
  background-color: #45a049;
}

.btn-add {
  background-color: #28a745;
}

.btn-edit {
  background-color: #ffc107;
}

.btn-delete {
  background-color: #dc3545;
}

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

li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
  padding: 10px;
  border-radius: 5px;
  background-color: #f8f9fa;
}

li.completed .task-text {
  text-decoration: line-through;
  color: #888;
}

li:hover {
  background-color: #e9ecef;
}

.actions {
  display: flex;
}

.actions .btn {
  margin-left: 5px;
}

.edit-input {
  flex: 1;
  margin-right: 10px;
}
</style>



Enter fullscreen mode Exit fullscreen mode

Top comments (0)