Completing the Functional Implementation
Creating the Todo App Factory Function
In the previous steps we wrote three standalone helper functions: addTodo, toggleTodo, and filterTodos. Each one takes the current state as input and returns new data without mutating anything. But our application state (todos, nextTodoId, filter) is still kept in module-level variables, so any code in the file can reach it.
Let’s put that state inside a factory function. A factory function is a function that creates and returns an object. Here the returned object gives us methods for working with the todo app, and the state itself stays private.
const createTodoApp = () => {
let todos = [];
let nextTodoId = 1;
let filter = "all"; // can be "all", "active", or "completed"
const filterTodos = () => {
if (filter === "active") {
return todos.filter((todo) => !todo.completed);
} else if (filter === "completed") {
return todos.filter((todo) => todo.completed);
} else {
return [...todos];
}
};
return {
addTodo: (newTodoText) => {
todos = [...todos, { id: nextTodoId++, text: newTodoText, completed: false }];
},
toggleTodo: (todoId) => {
todos = todos.map((todo) =>
todo.id === todoId ? { ...todo, completed: !todo.completed } : todo,
);
},
setFilter: (newFilter) => {
filter = newFilter;
},
getTodos: () => filterTodos(),
};
};
There are two important things happening here:
-
Closures: The
todos,nextTodoId, andfiltervariables are defined insidecreateTodoApp, but the returned object’s methods still use them. This works because of closures. The inner functions “close over” the variables in their enclosing scope, so they keep access to those variables even aftercreateTodoApphas finished running. Nothing outside the factory function can reach the variables directly, which is how we get data encapsulation. -
Consolidated helpers: Notice that our standalone helper functions (
addTodo,toggleTodo,filterTodos) have moved into the factory. They were only ever used to manage the factory’s internal state, so there is no reason to keep them as module-level functions.filterTodosis now a private function inside the closure, and theaddTodoandtoggleTodologic is written directly into the returned methods.
We can now create a todo app instance and work with it through the returned methods:
const todoApp = createTodoApp();
todoApp.addTodo("Buy milk"); // adds a new todo
todoApp.toggleTodo(1); // toggles todo with id 1
todoApp.setFilter("completed"); // shows only completed todos
const todos = todoApp.getTodos(); // returns filtered todos
Putting It All Together
Here is the final refactored code, with all the changes we made in the previous steps.
import "./style.css";
const createTodoApp = () => {
let todos = [];
let nextTodoId = 1;
let filter = "all"; // can be "all", "active", or "completed"
const filterTodos = () => {
if (filter === "active") {
return todos.filter((todo) => !todo.completed);
} else if (filter === "completed") {
return todos.filter((todo) => todo.completed);
} else {
return [...todos];
}
};
return {
addTodo: (newTodoText) => {
todos = [...todos, { id: nextTodoId++, text: newTodoText, completed: false }];
},
toggleTodo: (todoId) => {
todos = todos.map((todo) =>
todo.id === todoId ? { ...todo, completed: !todo.completed } : todo,
);
},
setFilter: (newFilter) => {
filter = newFilter;
},
getTodos: () => filterTodos(),
};
};
const todoApp = createTodoApp();
// Get the necessary DOM elements
const todoListElement = document.getElementById("todo-list");
const inputNewTodo = document.getElementById("new-todo");
const todoNav = document.getElementById("todo-nav");
// 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");
todoText.textContent = todo.text;
if (todo.completed) {
todoText.classList.add("line-through");
}
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;
};
// Function to render the todos based on the current filter
const renderTodos = () => {
todoListElement.innerHTML = ""; // Clear the current list to avoid duplicates
const todoElements = todoApp.getTodos().map(createTodoItem);
todoListElement.append(...todoElements);
};
// Helper function to update the class list of a navbar element
const updateClassList = (element, isActive) => {
const classes = [
"underline",
"underline-offset-4",
"decoration-rose-800",
"decoration-2",
];
if (isActive) {
element.classList.add(...classes);
} else {
element.classList.remove(...classes);
}
};
// Helper function to render the navbar anchor elements
const renderTodoNavBar = (href) => {
Array.from(todoNav.children).forEach((element) => {
updateClassList(element, element.href === href);
});
};
// Event handler to create a new todo item
const handleKeyDownToCreateNewTodo = (event) => {
const todoText = event.target.value.trim();
if (event.key === "Enter" && todoText !== "") {
todoApp.addTodo(todoText);
event.target.value = ""; // Clear the input
renderTodos();
}
};
// Event handler to toggle the completed status of a todo item
const handleClickOnTodoList = (event) => {
if (event.target.id.includes("todo-text")) {
const todoId = event.target.id.split("-").pop();
todoApp.toggleTodo(Number(todoId));
renderTodos();
}
};
// Event handler to filter the todos based on the navbar selection
const handleClickOnNavbar = (event) => {
if (event.target.tagName === "A") {
const href = event.target.href;
todoApp.setFilter(href.split("/").pop() || "all");
renderTodos();
renderTodoNavBar(href);
}
};
// Add the event listeners
todoListElement.addEventListener("click", handleClickOnTodoList);
inputNewTodo.addEventListener("keydown", handleKeyDownToCreateNewTodo);
todoNav.addEventListener("click", handleClickOnNavbar);
document.addEventListener("DOMContentLoaded", renderTodos);
Let’s go over the changes we made in this refactoring and see where each one comes from in functional programming:
-
Higher-Order Functions: We used higher-order functions like
map,filter, andforEach. A higher-order function takes another function as an argument or returns a function, and using them let us write the same logic in less code. -
Closures: We used a closure to hold the state of the todo app inside the factory function. The returned object’s methods can read and write
todos,nextTodoId, andfilterbecause the factory function created a closure over them. -
Immutable Data: We did not mutate the original data. We built new data instead. When we add a todo item or toggle its completed status, we build a new array with the updated values and leave the original array alone.
-
Pure Functions: Inside the factory, the code for adding, toggling, and filtering todos works the same way the standalone helpers did. Each operation produces new data instead of changing existing data.
filterTodos, for example, is a pure function: it has no external dependencies and it always returns a new array based on the current state.
Checkpoint: Commit your progress.
git add .
git commit -m "todos-13: Complete functional programming implementation"
git push