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:

  1. Identifying the Clicked Todo:

    if (event.target.id.includes("todo-text")) {
    

    We first check if the clicked element’s id includes 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.

  2. Extracting the Todo ID:

    const todoId = event.target.id.split("-").pop();
    const todoIdNumber = Number(todoId);
    

    We split the id of the clicked element by the - character and extract the last part, which corresponds to the id of the todo. We convert this value to a number using Number().

  3. 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 todos array to find the todo that matches the clicked id. Once found, we toggle the completed status of that todo using !todos[i].completed, which inverts the boolean value.

  4. Re-rendering the App UI:

    renderTodos();
    

    Finally, we call the renderTodos function 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