Task 4: Functional Programming
Let's refactor the code to adopt a functional programming paradigm. In functional programming, we'll focus on:
- Pure Functions: Functions that always produce the same output for the same input and have no side effects.
- Higher-Order Functions: Functions that take other functions as arguments or return functions.
- Closures: Functions that have access to the variables in their outer (enclosing) scope, even after the outer function has returned.
- Immutable Data: Avoid changing data. Instead, create new data structures.
Step 1: Create the Todo App Factory Function
- 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. - State Variables: Inside the factory function, we'll define our state variables:
todos
,nextTodoId
, andfilter
. - Public Interface: At the end of the factory function, we'll return an object that exposes the public methods of our todo app.
- 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.addTodo
: This method adds a new todo to thetodos
array.toggleTodo
: This method toggles the completed state of a todo based on its text.setFilter
: This method sets the current filter.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:
- Clear the Todo List: First, we'll clear the current list of todos in the DOM.
- 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.
- 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. - Toggle Todo: We'll add an event listener to the
todo-list
element. When a todo is clicked, we'll toggle its completed state. - 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.
- Remove Styles: We'll start by removing the underline style from all filters.
- 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.