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.