Completing Todos
Marking a Todo as Completed
When a todo is clicked, we want to toggle its completed status. We will add an event listener to the todo list and create a function to handle this behavior. First, we will update the renderTodos function to add an id attribute to the todo text element. This will help us identify the clicked todo later.
// Function to render the todos
function renderTodos() {
// code not shown for brevity
// Loop through the filtered todos and add them to the DOM
for (let i = 0; i < todos.length; i++) {
// code not shown for brevity
const todoText = document.createElement("div");
+ todoText.id = `todo-text-${todo.id}`;
todoText.classList.add("todo-text");
if (todo.completed) {
todoText.classList.add("line-through");
}
todoText.textContent = todo.text;
todoItem.appendChild(todoText);
// code not shown for brevity
}
}
Next, we will create a function to handle the click event on the todo list and toggle the completed status of the clicked todo.
// Function to toggle the completed status of a todo
function handleClickOnTodoList(event) {
if (event.target.id.includes("todo-text")) {
const todoId = event.target.id.split("-").pop();
const todoIdNumber = Number(todoId);
for (let i = 0; i < todos.length; i++) {
if (todos[i].id === todoIdNumber) {
todos[i].completed = !todos[i].completed;
}
}
renderTodos();
}
}
const todoListElement = document.getElementById("todo-list");
todoListElement.addEventListener("click", handleClickOnTodoList);
Let’s break down the handleClickOnTodoList function:
-
Identifying the Clicked Todo:
if (event.target.id.includes("todo-text")) {We first check if the clicked element’s
idincludes the string"todo-text". This check makes sure the rest of the code only runs when the click landed on the text of a todo item, and not on some other part of the todo or the page. -
Extracting the Todo ID:
const todoId = event.target.id.split("-").pop(); const todoIdNumber = Number(todoId);We split the
idof the clicked element by the-character and extract the last part, which corresponds to theidof the todo. We convert this value to a number usingNumber(). -
Toggling the Completed Status:
for (let i = 0; i < todos.length; i++) { if (todos[i].id === todoIdNumber) { todos[i].completed = !todos[i].completed; } }We loop through the
todosarray to find the todo that matches the clickedid. Once found, we toggle thecompletedstatus of that todo using!todos[i].completed, which inverts the boolean value. -
Re-rendering the App UI:
renderTodos();Finally, we call the
renderTodosfunction to update the displayed todos with the updated completed status.
We added the click listener to todoListElement, which is event delegation: one listener on the parent handles clicks on all of the todos inside it. We could have attached a click listener to each todo item instead, but then we would have one listener per todo, and that can slow the page down as the number of todos grows. Event delegation also makes dynamic content easier to handle. A new todo is added inside todoListElement, so the listener already on the parent picks up its clicks, and we do not have to set anything else up.
Putting It All Together
Here is the complete main.js file with all the functions and event listeners we have implemented so far. Notice that we have structured the code slightly differently to group related statements together.
import "../style.css";
// Get the necessary DOM elements
const todoListElement = document.getElementById("todo-list");
const inputNewTodo = document.getElementById("new-todo");
const todoNav = document.getElementById("todo-nav");
// Define the state of our app
const todos = [
{ id: 1, text: "Buy milk", completed: false },
{ id: 2, text: "Buy bread", completed: false },
{ id: 3, text: "Buy jam", completed: true },
];
let nextTodoId = 4;
let filter = "all"; // can be 'all', 'active', or 'completed'
// Function to render the todos based on the current filter
function renderTodos() {
// Clear the current list to avoid duplicates
todoListElement.innerHTML = "";
// Filter todos based on the current filter setting
let filteredTodos = [];
for (let i = 0; i < todos.length; i++) {
const todo = todos[i];
if (filter === "all") {
filteredTodos.push(todo);
} else if (filter === "completed" && todo.completed === true) {
filteredTodos.push(todo);
} else if (filter === "active" && todo.completed === false) {
filteredTodos.push(todo);
}
}
// Loop through the filtered todos and add them to the DOM
for (let i = 0; i < filteredTodos.length; i++) {
const todo = filteredTodos[i];
const todoItem = document.createElement("div");
todoItem.classList.add("p-4", "todo-item");
const todoText = document.createElement("div");
todoText.id = `todo-text-${todo.id}`;
todoText.classList.add("todo-text");
if (todo.completed) {
todoText.classList.add("line-through");
}
todoText.innerText = todo.text;
todoItem.appendChild(todoText);
const todoEdit = document.createElement("input");
todoEdit.classList.add("hidden", "todo-edit");
todoEdit.value = todo.text;
todoItem.appendChild(todoEdit);
todoListElement.appendChild(todoItem);
}
}
// Function to handle adding a new todo
function handleKeyDownToCreateNewTodo(event) {
const newTodoInput = event.target;
const todoText = newTodoInput.value.trim();
if (event.key === "Enter" && todoText !== "") {
todos.push({ id: nextTodoId++, text: todoText, completed: false });
newTodoInput.value = ""; // clear the input
renderTodos();
}
}
// Function to handle marking a todo as completed
function handleClickOnNavbar(event) {
// if the clicked element is an anchor tag
if (event.target.tagName === "A") {
const hrefValue = event.target.href;
const action = hrefValue.split("/").pop();
filter = action === "" ? "all" : action;
// render the app UI
renderTodoNavBar(hrefValue);
renderTodos();
}
}
// Function to update the navbar anchor elements
function renderTodoNavBar(href) {
const elements = todoNav.children;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element.href === href) {
element.classList.add(
"underline",
"underline-offset-4",
"decoration-rose-800",
"decoration-2",
);
} else {
element.classList.remove(
"underline",
"underline-offset-4",
"decoration-rose-800",
"decoration-2",
);
}
}
}
// Function to toggle the completed status of a todo
function handleClickOnTodoList(event) {
if (event.target.id.includes("todo-text")) {
const todoId = event.target.id.split("-").pop();
const todoIdNumber = Number(todoId);
for (let i = 0; i < todos.length; i++) {
if (todos[i].id === todoIdNumber) {
todos[i].completed = !todos[i].completed;
}
}
renderTodos();
}
}
// Add the event listeners
todoListElement.addEventListener("click", handleClickOnTodoList);
inputNewTodo.addEventListener("keydown", handleKeyDownToCreateNewTodo);
todoNav.addEventListener("click", handleClickOnNavbar);
document.addEventListener("DOMContentLoaded", renderTodos);
At this point the todo app has its basic functionality, written in a structured programming style. We can add todos, mark them as completed, and filter them. We keep the state of the app in arrays and variables, and we update the DOM with event listeners and functions.
We have not implemented the behavior of the elements in the todo-actions section yet, and a few other features are still missing. That was on purpose. We wanted to get the core functionality working first. We will add more features as we work through the different programming paradigms.
Checkpoint: Commit your progress.
git add .
git commit -m "todos-09: Implement core functionality with structured programming"
git push