In this task, we will implement the core functionality of our ToDo app with a focus on structured and procedural programming. These programming paradigms aim to improve the clarity, quality, and development time of a computer program by making extensive use of subroutines (also known as procedures or functions), block structures, loops for iteration, and conditional statements for decision-making.
Before we start manipulating the DOM, we need to initialize the state of our application. To that end, we’ll use an array to store our todos and a few other variables to manage the app’s state.
In the main.js
file, add the following code:
// Define the state of our app
const todos = [
{ id: 1, text: "Buy milk", completed: false },
{ id: 2, text: "Buy bread", completed: false },
{ id: 3, text: "Buy jam", completed: true },
];
let nextTodoId = 4;
let filter = "all"; // can be 'all', 'active', or 'completed'
Notice each todo is an object with three properties: id
, text
, and completed
. The id
property is a unique identifier for each todo, the text
property contains the description of the todo, and the completed
property is a boolean that indicates whether the todo is done or not.
The nextTodoId
variable is used to generate unique id
values for new todos. We start with 4
because we already have three todos in the initial state.
We’ll create a function to render the todos based on the current state.
// Function to render the todos
function renderTodos() {
const todoListElement = document.getElementById("todo-list");
todoListElement.innerHTML = ""; // clear the current list
// Loop through the filtered todos and add them to the DOM
for (let i = 0; i < todos.length; i++) {
const todo = todos[i];
const todoItem = document.createElement("div");
todoItem.classList.add("p-4", "todo-item");
todoListElement.appendChild(todoItem);
const todoText = document.createElement("div");
todoText.classList.add("todo-text");
todoText.textContent = todo.text;
todoItem.appendChild(todoText);
const todoEdit = document.createElement("input");
todoEdit.classList.add("hidden", "todo-edit");
todoEdit.value = todo.text;
todoItem.appendChild(todoEdit);
}
}
At a high level, this function does the following:
Clear the existing list of todos by setting the innerHTML
of the todoListElement
to an empty string. This ensures that we start with a clean slate each time we render the todos.
Loop through the todos and create a new div
element for each todo. This element contains the todo text and an input field for editing it based on the samples we have in the index.html
file.
Instead of directly calling the renderTodos
function, we’ll bind it to an event listener that triggers when the entire content of the page is loaded.
// Event listener to initialize the app after the DOM content is fully loaded
document.addEventListener('DOMContentLoaded', renderTodos);
This approach ensures that our app initializes only after all the DOM elements are available. This concept will be especially beneficial when you delve into frameworks like React, where understanding component lifecycle methods and their relation to the DOM is crucial.
Run the app and check if the todos are rendered correctly on the page.
When the user types a new todo and presses Enter, we’ll add it to the list. To implement this functionality, we’ll create a function that listens for the keydown
event on the input field.
// Function to handle adding a new todo
function handleNewTodoKeyDown(event) {
const newTodoInput = event.target;
const todoText = newTodoInput.value.trim();
if (event.key === 'Enter' && todoText !== '') {
todos.push({ id: nextTodoId++, text: todoText, completed: false });
newTodoInput.value = ''; // clear the input
renderTodos();
}
}
const newTodoInput = document.getElementById('new-todo');
newTodoInput.addEventListener('keydown', handleNewTodoKeyDown);
Let’s break down the handleNewTodoKeyDown
function:
Event Parameter: The function accepts an event
parameter, which is automatically passed by the browser when an event listener triggers this function. This event
object contains information about the event, such as which element was clicked.
Extracting the Input Value:
const newTodoInput = event.target;
const todoText = newTodoInput.value.trim();
We first extract the input element (newTodoInput
) from the event object using event.target
. We then retrieve the value of the input field and remove any leading or trailing whitespace using the trim()
method. This ensures that we don’t add todos that are just empty spaces.
Checking the Key Pressed:
if (event.key === 'Enter' && todoText !== '') {
We check if the key pressed by the user is the ‘Enter’ key (event.key === 'Enter'
) and if the input field is not empty (todoText !== ''
). This condition ensures that we only add a new todo when the user has entered some text and pressed the ‘Enter’ key.
Adding the New Todo:
todos.push({ id: nextTodoId++, text: todoText, completed: false });
If the conditions are met, we add a new todo to the todos
array. We generate a unique id
for the new todo using nextTodoId++
, set the text
property to the trimmed input value, and initialize the completed
property to false
.
Clearing the Input Field:
newTodoInput.value = ''; // clear the input
Then, we clear the input field to prepare it for the next todo entry.
Re-rendering the Todos:
renderTodos();
Finally, we call the renderTodos
function to update the displayed todos with the new addition.
Note that every time we change the state of the app, we must call renderTodos
to update the UI. One of the advantages of using a UI library like React is that it takes care of these changes automatically (and efficiently), allowing you to focus on managing the state. But more on that in future chapters!
We’ll add a line-through style to the text of completed todos to visually distinguish them from active todos. You must update the renderTodos
function to apply this style based on the completed
property of each todo.
// Function to render the todos
function renderTodos() {
const todoListElement = document.getElementById("todo-list");
todoListElement.innerHTML = ""; // clear the current list
// Loop through the filtered todos and add them to the DOM
for (let i = 0; i < todos.length; i++) {
const todo = todos[i];
const todoItem = document.createElement("div");
todoItem.classList.add("p-4", "todo-item");
todoListElement.appendChild(todoItem);
const todoText = document.createElement("div");
todoText.classList.add("todo-text");
+ if (todo.completed) {
+ todoText.classList.add("line-through");
+ }
todoText.textContent = todo.text;
todoItem.appendChild(todoText);
const todoEdit = document.createElement("input");
todoEdit.classList.add("hidden", "todo-edit");
todoEdit.value = todo.text;
todoItem.appendChild(todoEdit);
}
}
Run the app and check if the todos are rendered correctly on the page.
The anchor elements in the navbar act as filters to display all todos, active todos, or completed todos. We’ll add an event listener to the navbar and create a function to handle the filter selection.
// Function to handle marking a todo as completed
function handleClickOnNavbar(event) {
// if the clicked element is an anchor tag
if (event.target.tagName === "A") {
const hrefValue = event.target.href;
const action = hrefValue.split("/").pop();
filter = action === "" ? "all" : action;
console.log(filter);
}
}
const todoNav = document.getElementById("todo-nav");
todoNav.addEventListener("click", handleClickOnNavbar);
Let’s break down the handleClickOnNavbar
function:
Checking the Clicked Element:
if (event.target.tagName === "A") {
We check if the clicked element is an anchor tag (<a>
). This condition ensures that the function only runs when the user clicks on one of the filter links.
Extracting the Filter Action:
const hrefValue = event.target.href;
const action = hrefValue.split("/").pop();
filter = action === "" ? "all" : action;
We extract the href
attribute of the clicked anchor element (event.target.href
) to get the URL. We then split the URL by the /
character and extract the last part of the URL using pop()
. This part corresponds to the filter action (e.g., all
, active
, or completed
). If the action is an empty string, we set the filter to 'all'
.
Aside: We could have added the click
event to each anchor element and handled the filter selection for each. Instead, we chose to add the event listener to the parent element (todoNav
) and use event delegation to handle the filter selection.
We’ll update the renderTodos
function to filter the todos based on the selected filter and render only the relevant todos.
We can update the renderTodos
function to filter the todos based on the current filter setting like this:
// Filter todos based on the current filter setting
let filteredTodos = [];
for (let i = 0; i < todos.length; i++) {
const todo = todos[i];
if (filter === "all") {
filteredTodos.push(todo);
} else if (filter === "completed" && todo.completed) {
filteredTodos.push(todo);
} else if (filter === "active" && !todo.completed) {
filteredTodos.push(todo);
}
}
Next we’ll update the loop that renders the todos to use the filteredTodos
array instead of the todos
array.
// Loop through the filtered todos and add them to the DOM
- for (let i = 0; i < todos.length; i++) {
+ for (let i = 0; i < filteredTodos.length; i++) {
- const todo = todos[i];
+ const todo = filteredTodos[i];
// code not shown for brevity
}
Here is the updated renderTodos
function:
// Function to render the todos based on the current filter
function renderTodos() {
const todoListElement = document.getElementById("todo-list");
todoListElement.innerHTML = ""; // clear the current list
// Filter todos based on the current filter setting
let filteredTodos = [];
for (let i = 0; i < todos.length; i++) {
const todo = todos[i];
if (filter === "all") {
filteredTodos.push(todo);
} else if (filter === "completed" && todo.completed === true) {
filteredTodos.push(todo);
} else if (filter === "active" && todo.completed === false) {
filteredTodos.push(todo);
}
}
// Loop through the filtered todos and add them to the DOM
for (let i = 0; i < filteredTodos.length; i++) {
const todo = filteredTodos[i];
const todoItem = document.createElement("div");
todoItem.classList.add("p-4", "todo-item");
todoListElement.appendChild(todoItem);
const todoText = document.createElement("div");
todoText.classList.add("todo-text");
if (todo.completed) {
todoText.classList.add("line-through");
}
todoText.textContent = todo.text;
todoItem.appendChild(todoText);
const todoEdit = document.createElement("input");
todoEdit.classList.add("hidden", "todo-edit");
todoEdit.value = todo.text;
todoItem.appendChild(todoEdit);
}
}
Make sure to call renderTodos
after updating the filter to display the filtered todos.
// Function to handle marking a todo as completed
function handleClickOnNavbar(event) {
// if the clicked element is an anchor tag
if (event.target.tagName === "A") {
const hrefValue = event.target.href;
const action = hrefValue.split("/").pop();
filter = action === "" ? "all" : action;
- console.log(filter);
+ renderTodos();
}
}
Run the app and check if the todos are filtered correctly based on the selected filter.
Currently, there is an underline style applied to the first anchor element in the navbar. This is hardcoded in the HTML file. Let’s create a new function, renderTodoNavBar
, to apply this style dynamically based on the selected filter.
// Function to update the navbar anchor elements
function renderTodoNavBar(href) {
const elements = todoNav.children;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element.href === href) {
element.classList.add(
"underline",
"underline-offset-4",
"decoration-rose-800",
"decoration-2",
);
} else {
element.classList.remove(
"underline",
"underline-offset-4",
"decoration-rose-800",
"decoration-2",
);
}
}
}
This function iterates over the child elements of the todoNav
element (the navbar) and applies or removes the underline style based on the href
value. The anchor element that matches the href
value will have the underline style applied, while the others will have it removed.
To put this function into action, call it after updating the filter in the handleClickOnNavbar
function.
// Function to handle marking a todo as completed
function handleClickOnNavbar(event) {
// if the clicked element is an anchor tag
if (event.target.tagName === "A") {
const hrefValue = event.target.href;
const action = hrefValue.split("/").pop();
filter = action === "" ? "all" : action;
renderTodos();
+ renderTodoNavBar(hrefValue);
}
}
Run the app and check if the active filter is styled correctly based on the selected filter.
When a todo is clicked, we want to toggle its completed status. We’ll add an event listener to the todo list and create a function to handle this behavior. First, we’ll update the renderTodos
function to add an id
attribute to the todo text element. This will help us identify the clicked todo later.
// Function to render the todos
function renderTodos() {
// code not shown for brevity
// Loop through the filtered todos and add them to the DOM
for (let i = 0; i < todos.length; i++) {
// code not shown for brevity
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.textContent = todo.text;
todoItem.appendChild(todoText);
// code not shown for brevity
}
}
Next, we’ll create a function to handle the click event on the todo list and toggle the completed status of the clicked todo.
// Function to toggle the completed status of a todo
function handleClickOnTodoList(event) {
let todo = null;
if (event.target.id !== null && event.target.id.includes("todo-text")) {
todo = event.target;
}
let todoIdNumber = -1;
if (todo) {
const todoId = event.target.id.split("-").pop();
todoIdNumber = Number(todoId);
}
for (let i = 0; i < todos.length; i++) {
if (todos[i].id === todoIdNumber) {
todos[i].completed = !todos[i].completed;
}
}
// Re-render the app UI
renderTodos();
}
const todoListElement = document.getElementById("todo-list");
todoListElement.addEventListener("click", handleClickOnTodoList);
Let’s break down the handleClickOnTodoList
function:
Identifying the Clicked Todo:
let todo = null;
if (event.target.id !== null && event.target.id.includes("todo-text")) {
todo = event.target;
}
We first check if the clicked element has an id
and if it includes the string "todo-text"
. This condition ensures that the subsequent code only runs if the text of a todo item was clicked, and not any other part of the todo or the page.
Extracting the Todo ID:
let todoIdNumber = -1;
if (todo) {
const todoId = event.target.id.split("-").pop();
todoIdNumber = Number(todoId);
}
If the clicked element is a todo text, we extract the id
of the todo from the id
attribute of the clicked element. We split the id
by the -
character and extract the last part, which corresponds to the id
of the todo. We convert this value to a number using Number()
.
Toggling the Completed Status:
for (let i = 0; i < todos.length; i++) {
if (todos[i].id === todoIdNumber) {
todos[i].completed = !todos[i].completed;
}
}
We loop through the todos
array to find the todo that matches the clicked id
. Once found, we toggle the completed
status of that todo using !todos[i].completed
, which inverts the boolean value.
Re-rendering the App UI:
renderTodos();
Finally, we call the renderTodos
function to update the displayed todos with the updated completed status.
Note that we added the click event to the todoListElement and utilized event delegation to manage the click event on the todo list. We could have attached the click event to each todo item, but this would necessitate multiple event listeners, possibly causing performance issues as the number of todos increases. Event delegation is advantageous here as it reduces the number of event listeners, saving memory and enhancing performance, especially as the todo list expands. It also streamlines handling dynamic content, since new todos inherit the listener from their parent, removing the need for additional setup.
Here is the complete main.js
file with all the functions and event listeners we’ve implemented so far. Notice that we’ve structured the code slightly differently to group related statements together.
import "../style.css";
// Get the necessary DOM elements
const todoListElement = document.getElementById("todo-list");
const inputNewTodo = document.getElementById("new-todo");
const todoNav = document.getElementById("todo-nav");
// Define the state of our app
const todos = [
{ id: 1, text: "Buy milk", completed: false },
{ id: 2, text: "Buy bread", completed: false },
{ id: 3, text: "Buy jam", completed: true },
];
let nextTodoId = 4;
let filter = "all"; // can be 'all', 'active', or 'completed'
// Function to render the todos based on the current filter
function renderTodos() {
// Clear the current list to avoid duplicates
todoListElement.innerHTML = "";
// Filter todos based on the current filter setting
let filteredTodos = [];
for (let i = 0; i < todos.length; i++) {
const todo = todos[i];
if (filter === "all") {
filteredTodos.push(todo);
} else if (filter === "completed" && todo.completed === true) {
filteredTodos.push(todo);
} else if (filter === "active" && todo.completed === false) {
filteredTodos.push(todo);
}
}
// Loop through the filtered todos and add them to the DOM
for (let i = 0; i < filteredTodos.length; i++) {
const todo = filteredTodos[i];
const todoItem = document.createElement("div");
todoItem.classList.add("p-4", "todo-item");
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);
todoListElement.appendChild(todoItem);
}
}
// Function to handle adding a new todo
function handleKeyDownToCreateNewTodo(event) {
const newTodoInput = event.target;
const todoText = newTodoInput.value.trim();
if (event.key === "Enter" && todoText !== "") {
todos.push({ id: nextTodoId++, text: todoText, completed: false });
newTodoInput.value = ""; // clear the input
renderTodos();
}
}
// Function to handle marking a todo as completed
function handleClickOnNavbar(event) {
// if the clicked element is an anchor tag
if (event.target.tagName === "A") {
const hrefValue = event.target.href;
const action = hrefValue.split("/").pop();
filter = action === "" ? "all" : action;
// render the app UI
renderTodoNavBar(hrefValue);
renderTodos();
}
}
// Function to update the navbar anchor elements
function renderTodoNavBar(href) {
const elements = todoNav.children;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element.href === href) {
element.classList.add(
"underline",
"underline-offset-4",
"decoration-rose-800",
"decoration-2",
);
} else {
element.classList.remove(
"underline",
"underline-offset-4",
"decoration-rose-800",
"decoration-2",
);
}
}
}
// Function to toggle the completed status of a todo
function handleClickOnTodoList(event) {
let todo = null;
if (event.target.id !== null && event.target.id.includes("todo-text")) {
todo = event.target;
}
let todoIdNumber = -1;
if (todo) {
const todoId = event.target.id.split("-").pop();
todoIdNumber = Number(todoId);
}
for (let i = 0; i < todos.length; i++) {
if (todos[i].id === todoIdNumber) {
todos[i].completed = !todos[i].completed;
}
}
// Re-render the app UI
renderTodos();
}
// Add the event listeners
todoListElement.addEventListener("click", handleClickOnTodoList);
inputNewTodo.addEventListener("keydown", handleKeyDownToCreateNewTodo);
todoNav.addEventListener("click", handleClickOnNavbar);
document.addEventListener("DOMContentLoaded", renderTodos);
With this implementation, we’ve set up the basic functionality for our todo app using a structured programming approach. The todos can be added, marked as completed, and filtered. The state of the app is managed using arrays and variables, and the DOM is manipulated using various event listeners and functions. This structured approach is easy to understand and maintain, especially for beginners.
It’s worth noting that we haven’t yet implemented the behavior of the elements in the todo-actions
section (as well as some of the other features). This was intentional, as we wanted to focus on the core functionality first. As we progress through different programming paradigms, we’ll gradually enhance the app’s features.