Task 4: Functional Programming

Let's refactor the code to adopt a functional programming paradigm. In functional programming, we'll focus on:

  1. Pure Functions: Functions that always produce the same output for the same input and have no side effects.
  2. Higher-Order Functions: Functions that take other functions as arguments or return functions.
  3. Closures: Functions that have access to the variables in their outer (enclosing) scope, even after the outer function has returned.
  4. Immutable Data: Avoid changing data. Instead, create new data structures.

Step 1: Create the Todo App Factory Function

  1. Define a Factory Function: We'll start by defining a factory function named createTodoApp. This function will encapsulate the state and behavior of our todo app.
  2. State Variables: Inside the factory function, we'll define our state variables: todos, nextTodoId, and filter.
  3. Public Interface: At the end of the factory function, we'll return an object that exposes the public methods of our todo app.
  4. Define the Todo App Methods: Implement these operations inside the createTodoApp function so that they have access to the state variables defined within this enclosing function.
    1. addTodo: This method adds a new todo to the todos array.
    2. toggleTodo: This method toggles the completed state of a todo based on its text.
    3. setFilter: This method sets the current filter.
    4. getVisibleTodos: This method returns the todos based on the current filter.
// Factory function to create a new todo app
function createTodoApp() {
  let todos = [];
  let nextTodoId = 1;
  let filter = "all"; // possible values: 'all', 'active', 'completed'

  // Add a new todo to the list
  function addTodo(text) {
    todos = [...todos, { id: nextTodoId++, text, completed: false }];
  }

  // Toggle the completed state of a todo
  // Pre: assume todos are unique
  function toggleTodo(todoText) {
    todos = todos.map((todo) =>
      todo.text === todoText ? { ...todo, completed: !todo.completed } : todo,
    );
  }

  // Set the current filter
  function setFilter(newFilter) {
    filter = newFilter;
  }

  // Get the todos based on the current filter
  function getVisibleTodos() {
    switch (filter) {
      case "active":
        return todos.filter((todo) => !todo.completed);
      case "completed":
        return todos.filter((todo) => todo.completed);
      default:
        return todos;
    }
  }

  // Return the public interface
  return {
    addTodo,
    toggleTodo,
    setFilter,
    getVisibleTodos,
  };
}

// Create a new todo app
const todoApp = createTodoApp();

Please note that our global variable is now todoApp.

Step 2: Render the Todos

The rendering function remains largely the same, but we'll use the getVisibleTodos function to determine which todos to display:

  1. Clear the Todo List: First, we'll clear the current list of todos in the DOM.
  2. Create Todo Elements: For each todo in the visibleTodos array, we'll create a new DOM element and append it to the todo list.
// Render the todos based on the state
function renderTodos() {
  const visibleTodos = todoApp.getVisibleTodos();
  const todoList = document.getElementById("todo-list");
  todoList.innerHTML = "";

  // Create a new element for each todo
  visibleTodos.forEach((todo) => {
    const todoElement = document.createElement("div");
    todoElement.className = "p-4 todo-item";
    const todoText = document.createElement("div");
    todoText.className = "todo-text";
    todoText.textContent = todo.text;
    if (todo.completed) {
      todoText.style.textDecoration = "line-through";
    }
    todoElement.appendChild(todoText);
    todoList.appendChild(todoElement);
  });
}

Step 3: Event Listeners

We will bind our event handlers to their respective events using anonymous arrow functions.

  1. Add New Todo: We'll add an event listener to the new-todo input. When the user presses the Enter key, we'll add a new todo to the list.
  2. Toggle Todo: We'll add an event listener to the todo-list element. When a todo is clicked, we'll toggle its completed state.
  3. Filter Todos: We'll add an event listener to the todo-nav element. When a filter is clicked, we'll update the current filter and re-render the todos. We'll also update the filter styles to reflect the current selection.
// Handle adding a new todo
document.getElementById("new-todo").addEventListener("keydown", (event) => {
  if (event.key === "Enter" && event.target.value.trim() !== "") {
    todoApp.addTodo(event.target.value.trim());
    event.target.value = "";
    renderTodos();
  }
});

// Handle marking a todo as completed
document.getElementById("todo-list").addEventListener("click", (event) => {
  if (event.target.classList.contains("todo-text")) {
    todoApp.toggleTodo(event.target.textContent);
    renderTodos();
  }
});

// Handle filtering the todos
document.getElementById("todo-nav").addEventListener("click", (event) => {
  if (event.target.tagName === "A") {
    const newFilter = event.target.getAttribute("href").slice(2) || "all";
    todoApp.setFilter(newFilter);
    renderTodos();
  }
});

Note: In the refactored code, arrow functions are used to define event handlers. Arrow functions do not bind this to the element that triggered the event, so we cannot use this to refer to the input field in the handleNewTodoKeyDown function. Instead, we must use event.target to refer to the input field and retrieve its value using event.target.value.

Step 4: Initialize the App

We do not need to make any changes to the event listener for DOMContentLoaded. However, to demonstrate that you can also hook this up on the window object, we will make the following change:

- document.addEventListener('DOMContentLoaded', renderTodos);
+ window.addEventListener("DOMContentLoaded", renderTodos);

Once the DOM content is fully loaded, we will call the renderTodos function to display the initial state of our app.

Aside: In a web browser, the window and document objects are both part of the global scope.

The window object represents the current browser window and provides access to properties like the browser's location, history, and screen size. It also has methods for interacting with the browser, such as opening new windows or alerting the user.

The document object represents the current web page and provides access to the page's elements, such as the HTML tags, CSS styles, and JavaScript code. It has methods for manipulating the contents of the page, such as adding or removing elements, and for interacting with user events, such as clicks or key presses.

While the window object provides global properties and methods related to the browser, the document object provides properties and methods specifically related to the current page.

Step 5: Update Filter Styles

All previously implemented features have been replicated. In order to improve the app, let's add a feature that was left out in the previous task. When a user clicks on a filter, the selected filter should be highlighted with a red underline.

  1. Remove Styles: We'll start by removing the underline style from all filters.
  2. Apply Styles: Then, we'll apply the underline style to the selected filter.
// Update the styles of the filters
function updateFilterStyles(selectedFilter) {
  // Get all filter elements
  const filters = document.querySelectorAll("#todo-nav a");

  // Remove the underline from all filters
  filters.forEach((filter) => {
    filter.classList.remove(
      "underline",
      "underline-offset-4",
      "decoration-rose-800",
      "decoration-2",
    );
  });

  // Add the underline to the selected filter
  selectedFilter.classList.add(
    "underline",
    "underline-offset-4",
    "decoration-rose-800",
    "decoration-2",
  );
}
  // Handle filtering the todos
  document.getElementById("todo-nav").addEventListener("click", (event) => {
    if (event.target.tagName === "A") {
      const newFilter = event.target.getAttribute("href").slice(2) || "all";
      todoApp.setFilter(newFilter);
      renderTodos();
+     updateFilterStyles(event.target); // Update the styles for the selected filter
    }
  });

Step 6: Putting all together

This is the main.js file:

import "../style.css";

// Factory function to create a new todo app
function createTodoApp() {
  let todos = [];
  let nextTodoId = 1;
  let filter = "all"; // Initial filter setting

  // Add a new todo to the list
  function addTodo(text) {
    // Spread the existing todos and add the new one to the end
    todos = [...todos, { id: nextTodoId++, text, completed: false }];
  }

  // Toggle the completed state of a todo
  function toggleTodo(todoText) {
    // Map through todos and toggle the completed state of the matched todo
    todos = todos.map((todo) =>
      todo.text === todoText ? { ...todo, completed: !todo.completed } : todo,
    );
  }

  // Set the current filter
  function setFilter(newFilter) {
    filter = newFilter;
  }

  // Get the todos based on the current filter
  function getVisibleTodos() {
    switch (filter) {
      case "active":
        return todos.filter((todo) => !todo.completed);
      case "completed":
        return todos.filter((todo) => todo.completed);
      default:
        return todos;
    }
  }

  // Return the public interface of the factory function
  return {
    addTodo,
    toggleTodo,
    setFilter,
    getVisibleTodos,
  };
}

// Create a new todo app instance
const todoApp = createTodoApp();

// Function to render the todos based on the current filter
function renderTodos() {
  const todoListElement = document.getElementById("todo-list");
  todoListElement.innerHTML = ""; // Clear the current list

  const filteredTodos = todoApp.getVisibleTodos();

  // Loop through the filtered todos and add them to the DOM
  filteredTodos.forEach((todo) => {
    const todoItem = document.createElement("div");
    todoItem.classList.add("p-4", "todo-item");

    const todoText = document.createElement("div");
    todoText.classList.add("todo-text");
    todoText.textContent = todo.text;
    if (todo.completed) {
      todoText.style.textDecoration = "line-through";
    }

    const todoEdit = document.createElement("input");
    todoEdit.classList.add("hidden", "todo-edit");
    todoEdit.value = todo.text;

    todoItem.appendChild(todoText);
    todoItem.appendChild(todoEdit);
    todoListElement.appendChild(todoItem);
  });
}

// Update the styles of the filters
function updateFilterStyles(selectedFilter) {
  // Get all filter elements
  const filters = document.querySelectorAll("#todo-nav a");

  // Remove the underline from all filters
  filters.forEach((filter) => {
    filter.classList.remove(
      "underline",
      "underline-offset-4",
      "decoration-rose-800",
      "decoration-2",
    );
  });

  // Add the underline to the selected filter
  selectedFilter.classList.add(
    "underline",
    "underline-offset-4",
    "decoration-rose-800",
    "decoration-2",
  );
}

// Event handler for adding a new todo
document.getElementById("new-todo").addEventListener("keydown", (event) => {
  const value = event.target.value.trim();
  if (event.key === "Enter" && value !== "") {
    todoApp.addTodo(value);
    event.target.value = ""; // Clear the input field
    renderTodos();           // Re-render the todos
  }
});

// Event handler for marking a todo as completed
document.getElementById("todo-list").addEventListener("click", (event) => {
  if (event.target.classList.contains("todo-text")) {
    todoApp.toggleTodo(event.target.textContent);
    renderTodos(); // Re-render the todos
  }
});

// Event handler for changing the filter
document.getElementById("todo-nav").addEventListener("click", (event) => {
  if (event.target.tagName === "A") {
    // Extract the filter value from the href attribute
    const hrefValue = event.target.getAttribute("href").slice(2);
    const filter = hrefValue === "" ? "all" : hrefValue;
    todoApp.setFilter(filter);
    updateFilterStyles(event.target); // Update the filter styles
    renderTodos();                    // Re-render the todos
  }
});

// Event listener to initialize the app after the DOM content is fully loaded
window.addEventListener("DOMContentLoaded", renderTodos);

In this task, we refactored a todo app from a structured programming paradigm to a functional programming paradigm. We defined a factory function to encapsulate the state and behavior of the app, implemented pure functions to manage the state, and used higher-order functions and closures to create modular code. We also updated the event handlers to work with the new paradigm and added a feature that highlights the selected filter.