Building the TodoApp Class
Defining the TodoApp Class
Now let’s define the TodoApp class that represents the main application logic. Create a new file named todo-app.js in the src directory. The TodoApp class will interact with the TodoList class to manage the list of todo items and handle user interactions.
import TodoList, { FILTERS } from "./todo-list.js";
class TodoApp {
#todoList;
#filter;
#newTodoInput;
#todoNav;
#todoListElement;
#markAllCompleted;
#clearCompleted;
#activeTodoCount;
constructor() {
this.#todoList = new TodoList();
this.#filter = FILTERS.ALL;
this.#newTodoInput = document.getElementById("new-todo");
this.#todoNav = document.getElementById("todo-nav");
this.#todoListElement = document.getElementById("todo-list");
this.#markAllCompleted = document.getElementById("mark-all-completed");
this.#clearCompleted = document.getElementById("clear-completed");
this.#activeTodoCount = document.getElementById("todo-count");
// Add event listeners
this.#newTodoInput.addEventListener(
"keydown",
this.#handleNewTodoKeyDown.bind(this),
);
this.#todoNav.addEventListener(
"click",
this.#handleClickOnNavbar.bind(this),
);
this.#todoListElement.addEventListener(
"click",
this.#handleClickOnTodoList.bind(this),
);
this.#markAllCompleted.addEventListener(
"click",
this.#handleMarkAllCompleted.bind(this),
);
this.#clearCompleted.addEventListener(
"click",
this.#handleClearCompleted.bind(this),
);
}
#createTodoText(todo) {
const todoText = document.createElement("div");
todoText.classList.add("todo-text");
todoText.setAttribute("id", `todo-text-${todo.id}`);
todoText.textContent = todo.text;
if (todo.completed) {
todoText.classList.add("line-through");
}
return todoText;
}
#createTodoInput(todo) {
const todoInput = document.createElement("input");
todoInput.classList.add("hidden", "todo-edit");
todoInput.value = todo.text;
return todoInput;
}
#createTodoItem(todo) {
const todoItem = document.createElement("div");
todoItem.classList.add("p-4", "todo-item");
todoItem.append(
this.#createTodoText(todo),
this.#createTodoInput(todo),
);
return todoItem;
}
renderTodos() {
this.#todoListElement.replaceChildren(
...this.#todoList
.getTodos(this.#filter)
.map((todo) => this.#createTodoItem(todo)),
);
this.#activeTodoCount.textContent =
`${this.#todoList.getNumberOfActiveTodos()} items left`;
}
#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);
}
}
#renderTodoNavBar(href) {
Array.from(this.#todoNav.children).forEach((e) =>
this.#updateClassList(e, e.href === href),
);
}
#handleNewTodoKeyDown(event) {
const newTodoInput = event.target;
const todoText = newTodoInput.value.trim();
if (event.key === "Enter" && todoText !== "") {
this.#todoList.addTodo(todoText);
newTodoInput.value = "";
this.renderTodos();
}
}
#handleClickOnNavbar(event) {
if (event.target.tagName === "A") {
const href = event.target.href;
this.#filter = href.split("/").pop() || FILTERS.ALL;
this.renderTodos();
this.#renderTodoNavBar(href);
}
}
#handleClickOnTodoList(event) {
if (event.target.id.includes("todo-text")) {
const todoId = event.target.id.split("-").pop();
this.#todoList.toggleTodo(Number(todoId));
this.renderTodos();
}
}
#handleMarkAllCompleted() {
this.#todoList.markAllCompleted();
this.renderTodos();
}
#handleClearCompleted() {
this.#todoList.clearCompleted();
this.renderTodos();
}
}
export default TodoApp;
Let’s break down the code at a high level:
-
We import the
TodoListclass and theFILTERSobject from thetodo-list.jsfile. -
We define the
TodoAppclass with private fields to store references to various elements in the HTML document. These fields represent the todo list, the current filter, and DOM elements for the input, navigation, todo list, buttons, and active count display. -
We export the
TodoAppclass as the default export so it can be imported and used from another file.
Let’s break down the TodoApp class in more detail:
-
The
constructormethod initializes theTodoAppclass by creating aTodoListinstance, setting the default filter toFILTERS.ALL, caching DOM element references, and setting up event listeners for user interactions. -
The
#createTodoText,#createTodoInput, and#createTodoItemmethods are helper methods to create the HTML elements for displaying todo items. -
The
renderTodosmethod renders the list of todos based on the current filter. It replaces the existing list of todos with new todo elements and updates the active todos count. This method is public because it will be called from outside the class to perform the initial render. -
The
#handleNewTodoKeyDownmethod is an event handler that creates a new todo item when the user presses the Enter key in the input field. It adds the new todo item to the list, clears the input field, and re-renders the list of todos. -
The
#handleClickOnTodoListmethod is an event handler that toggles the completion status of a todo item when the user clicks on the todo text. It extracts the todo ID from the element’s ID, toggles the todo, and re-renders the list. -
The
#updateClassListmethod is a helper method to update the class list of a navbar element based on whether it is active or not. The#renderTodoNavBarmethod renders the navbar anchor elements based on the current filter selection. -
The
#handleClickOnNavbarmethod is an event handler that filters the todos based on the navbar selection. It updates the filter, re-renders the list, and updates the navbar styling. -
The
#handleMarkAllCompletedmethod is an event handler that marks all todos as completed when the user clicks the “mark all completed” button. -
The
#handleClearCompletedmethod is an event handler that clears all completed todos when the user clicks the “clear completed” button.
Let’s make a few observations about the TodoApp class:
-
Most fields and methods of the
TodoAppclass are private, indicated by the#symbol. This encapsulates the internal state and behavior of the class and prevents direct access from outside the class. The only public member isrenderTodos, which needs to be called from the entry point to perform the initial render. -
The
TodoAppclass combines the logic for managing the todo list, handling user interactions, and rendering the UI. This is a common pattern in front-end development, where the logic and presentation are closely tied together. -
The
TodoAppclass uses helper methods to create and manipulate HTML elements. Keeping element creation in its own methods keeps the code organized and makes it easier to maintain and extend the application. The way we split the code into pieces here is very similar to what we ended up with in the previous task, where we implemented this app in functional programming style. -
In several cases, we use the
.bind(this)method to bind the context of the event handlers to theTodoAppinstance. This ensures that the event handlers have access to the private fields and methods of the class. Thebindmethod creates a new function that, when called, has itsthiskeyword set to the provided value.
Let’s elaborate on the last point. Consider this code snippet from the TodoApp class:
this.#newTodoInput.addEventListener(
"keydown",
this.#handleNewTodoKeyDown.bind(this),
);
The #handleNewTodoKeyDown is invoked when you press a key down. It uses the this keyword to access #todoList and call the renderTodos method.
#handleNewTodoKeyDown(event) {
const newTodoInput = event.target;
const todoText = newTodoInput.value.trim();
if (event.key === "Enter" && todoText !== "") {
this.#todoList.addTodo(todoText);
newTodoInput.value = "";
this.renderTodos();
}
}
However, when #handleNewTodoKeyDown is called by addEventListener, its execution context is not the TodoApp anymore. Instead, it is the execution context of addEventListener which in this case is the #newTodoInput element. Therefore, we need to explicitly bind its this keyword to the TodoApp when passing it as an argument to addEventListener.
Alternatively, if you declare the #handleNewTodoKeyDown as an arrow function like this:
#handleNewTodoKeyDown = (event) => {
const newTodoInput = event.target;
const todoText = newTodoInput.value.trim();
if (event.key === "Enter" && todoText !== "") {
this.#todoList.addTodo(todoText);
newTodoInput.value = "";
this.renderTodos();
}
}
Then you do not need to bind its this keyword. Think about why that is.
this.#newTodoInput.addEventListener("keydown", this.#handleNewTodoKeyDown);
Using the TodoApp Class
Now let’s update the main.js file to import and use the TodoApp class:
import "./style.css";
import TodoApp from "./todo-app.js";
document.addEventListener("DOMContentLoaded", () => {
const todoApp = new TodoApp();
todoApp.renderTodos();
});
We import the CSS stylesheet and the TodoApp class from todo-app.js. We create a new instance of the TodoApp class when the DOM content is loaded and call renderTodos to perform the initial render.
Summary
In this section, we implemented a simple todo list application using object-oriented programming principles. We defined the Todo class to represent individual todo items, the TodoList class to manage a collection of todo items, and the TodoApp class to handle user interactions and render the UI. The TodoApp class is defined in its own module (todo-app.js) and imported into the entry point (main.js).
Checkpoint: Commit your progress.
git add .
git commit -m "todos-19: Create TodoApp class and complete OOP implementation"
git push