Functional Programming
We will refactor the code to use a functional programming style. In functional programming, we will focus on four things:
- Higher-Order Functions: These are functions that either accept other functions as arguments or return functions.
- Closures: Functions that retain access to variables from their outer (enclosing) scope, even after the outer function has finished executing.
- Immutable Data: Instead of changing existing data, we create new data.
- Pure Functions: These are functions that return the same output every time they are given the same input, and that have no side effects.
Rendering Todos with forEach
Let’s focus on the renderTodos function. We are using two for loops to filter the todos based on the current filter setting and then render them to the DOM.
// Function to render the todos based on the current filter
function renderTodos() {
// Clear the current list to avoid duplicates
todoListElement.innerHTML = "";
let filteredTodos = [];
// Filter todos based on the current filter setting
for (let i = 0; i < todos.length; i++) {
// Code to filter todos based on the current filter setting
}
// Loop through the filtered todos and add them to the DOM
for (let i = 0; i < filteredTodos.length; i++) {
// Code to render each todo item
}
}
In JavaScript, arrays have built-in higher-order functions like filter, map, reduce, and forEach. We can use these functions to refactor the code and make it more “functional.” For instance, for the second loop where we iterate over the filteredTodos array, we can use the forEach function to loop through the filteredTodos array and render each todo item to the DOM. Here is an example of how you can use the forEach function to render the todos:
filteredTodos.forEach(/* Function to render each todo item */);
We can define a separate function to render each todo item and pass it as an argument to the forEach function.
function renderTodoItem(todo) {
// Code to render each todo item
}
filteredTodos.forEach(renderTodoItem);
Notice that the forEach function takes a function as an argument, which will be called for each element in the filteredTodos array. This is an example of a higher-order function where we pass a function as an argument to another function. In this case, the forEach function is the higher-order function, and the renderTodoItem function is the callback function.
The forEach function calls the renderTodoItem function (hence it is called the callback function) for each todo item in the filteredTodos array, passing the todo item as an argument. The renderTodoItem function then renders the todo item to the DOM.
We can define the renderTodoItem function right before where we are calling the forEach function, inside the renderTodos function. In JavaScript, functions can be defined anywhere in the code, including inside other functions. Alternatively, we can define renderTodoItem separately at the top of the file. This alternative will be useful if we want to reuse the renderTodoItem function in other parts of the code.
In our case, we will not be reusing the renderTodoItem function, so defining it inside the renderTodos function is a good choice. In fact, we can define the renderTodoItem function right where we are calling the forEach function as shown below:
filteredTodos.forEach(function renderTodoItem(todo) {
// Code to render each todo item
});
If you define the renderTodoItem function right where you are calling the forEach function, you would not be able to reuse it in other parts of the code. If this is not a concern, you can define it inline like this. Moreover, you can make it an anonymous function by removing the function name.
filteredTodos.forEach(function (todo) {
// Code to render each todo item
});
The anonymous function will still be called for each todo item in the filteredTodos array, but you will not be able to reuse it in other parts of the code. It has a more concise syntax and is useful when you do not need to reuse the function. You can also use arrow functions for an even more concise syntax.
filteredTodos.forEach((todo) => {
// Code to render each todo item
});
Arrow functions have a shorter syntax and are typically used for anonymous functions that are passed as arguments to other functions.
Let’s complete the implementation of this arrow function to render each todo item to the DOM.
// 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");
todoListElement.appendChild(todoItem);
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);
});
The body of the arrow function is the same as the code that was inside the second loop. We are creating a div element for each todo item, setting the text content, adding classes based on the completed property, and appending the elements to the DOM. Replace the second loop with this code snippet to render the todos using the forEach function.
We can further refactor the code by extracting the logic to create the todo text element and the todo edit input element into separate helper functions. This will make the code more modular and easier to read.
// Helper function to create todo text element
const createTodoText = (todo) => {
const todoText = document.createElement("div");
todoText.id = `todo-text-${todo.id}`;
todoText.classList.add(
"todo-text",
...(todo.completed ? ["line-through"] : []),
);
todoText.innerText = todo.text;
return todoText;
};
// Helper function to create todo edit input element
const createTodoEditInput = (todo) => {
const todoEdit = document.createElement("input");
todoEdit.classList.add("hidden", "todo-edit");
todoEdit.value = todo.text;
return todoEdit;
};
// Helper function to create a todo item
const createTodoItem = (todo) => {
const todoItem = document.createElement("div");
todoItem.classList.add("p-4", "todo-item");
todoItem.append(createTodoText(todo), createTodoEditInput(todo));
return todoItem;
};
Now we can use these helper functions inside the forEach function to create the todo text and edit input elements and append them to the todo item.
filteredTodos.forEach((todo) => {
todoListElement.appendChild(createTodoItem(todo));
});
Here is another way to do the same thing, using the map function.
const todoElements = filteredTodos.map(createTodoItem);
todoListElement.append(...todoElements);
The map function calls a function on every element of an array and collects the results into a new array. In this case, we are creating an array of todo elements by mapping each todo item to a todo element using the createTodoItem function. We then append all the todo elements to the todoListElement using the spread operator ....
The map function is often preferred when you want to transform each element in an array and create a new array with the transformed elements. The forEach function is used when you want to perform an operation on each element in an array without creating a new array.
Checkpoint: Commit your progress.
git add .
git commit -m "todos-10: Refactor rendering with forEach and helper functions"
git push