Hi @BBualdo, this is one way you can achieve the functionality of the todo list:
- In the text input section add an id to the checkbox and to the input:
<section class="todo-input-container">
<div class="todo-input">
<input
id="add-task"
class="checkbox"
type="checkbox"
/>
<input
id="input-add-task"
class="todo"
placeholder="Create a new todo..."
/>
</div>
</section>
- The "todo-list-container" section can be left empty, since the tasks will be added with js:
<section class="todo-list-container">
<!--Tasks will be added with js-->
</section>
- In the "nav-bar-container" section you can do the following:
- Add an id to the p element inside the "items-left", to be able to dynamically change its text according to the number of tasks.
- Add the class "is-toggled" all button "All", since it will be selected by default.
<section class="nav-bar-container">
<div class="items-left">
<p id="items-count"><!--items left--></p>
</div>
<div class="categories">
<button class="category-button is-toggled">All</button>
<button class="category-button">Active</button>
<button class="category-button">Completed</button>
</div>
<div class="clear-completed">
<button class="clear-completed-button">Clear Completed</button>
</div>
</section>
Now we go with the Javascript code:
// Elements
const todoListContainer = document.querySelector(".todo-list-container");
const checkToAddTask = document.getElementById("add-task");
const inputAddTask = document.getElementById("input-add-task");
const categoryButtons = document.querySelectorAll(".category-button");
const buttonClear = document.querySelector(".clear-completed-button");
const itemsCountElement = document.getElementById("items-count");
- The task array can be updated as follows by adding the id and complete properties which will be false by default:
let todoList = [
{ id: 1, name: "Complete online JavaScript course", completed: false },
{ id: 2, name: "Jog around the park 3x", completed: false },
{ id: 3, name: "10 minutes meditation", completed: false },
{ id: 4, name: "Read for 1 hour", completed: false },
{ id: 5, name: "Pick up groceries", completed: false },
{ id: 6, name: "Complete Todo App on Frontend Mentor", completed: false },
];
- This function receives a parameter called list that if passed this is used, otherwise the list todoList will be used.
- In the variable html add the class "completed" to the container if
task.completed
is true using a ternary task.completed ? "completed" : ""
, also add a data-task-id="${task.id}"
attribute, this will be used later for the update and delete actions and finally to the input the property checked is added if task.completed is true ${task.completed ? "checked" : ""}
.
function renderTodoList(list) {
const filteredList = list || todoList; // 1
todoListContainer.innerHTML = "";
filteredList.forEach((task) => {
const html = `
<div class="todo-list ${
task.completed ? "completed" : ""
}" data-task-id="${task.id}">
<input name="completed" class="checkbox" type="checkbox" ${
task.completed ? "checked" : ""
}>
<div class="todo-content">
<p>${task.name}</p>
<button class="delete-button">Delete</button>
</div>
</div>
`; //2
todoListContainer.innerHTML += html; //Insert the html
});
updateItemsLeftCounter(filteredList); // Update counter
}
- Now the functions to add, update, delete and filter:
- Add task: Create an object with the data of the new task, as id in this case numbers are being used in order and to continue we use the todoList.length to obtain the number of elements of the tasks array and we add 1:
id: todoList.length + 1
, the name will be the value of the parameter name, and completed will be false, you add the new task to the array using todoList.push(newTask)
, render the list, reset the value of the input and finally the setTimeout is to uncheck the input a moment after the task is added
function addTask(name) {
const newTask = {
id: todoList.length + 1,
name,
completed: false,
};
todoList.push(newTask);
renderTodoList();
inputAddTask.value = "";
setTimeout(() => {
checkToAddTask.checked = false;
}, 200);
}
- Filter Task Function: This function receives the filter parameter and will be in charge of keeping the task lists updated in their respective category filters, first we create a copy of the tasks so as not to change the original array and avoid deleting certain tasks when switch categories, then use
tasksCopy.filter
to filter the categories: if filter === "all"
return all tasks else if : filter === "completed" ? task.completed
returns the tasks where task.completed
equals true otherwise those with task.completed
equal false, then stores the filter in the currentFilter variable and calls the renderTodoList(taskFiltered)
function passing the filtered tasks as parameters.
function filterTask(filter) {
const tasksCopy = [...todoList];
const taskFiltered = tasksCopy.filter((task) =>
filter === "all"
? task
: filter === "completed"
? task.completed
: !task.completed
);
currentFilter = filter;
renderTodoList(taskFiltered);
}
- Update task: This function receives the id of the task to update and to update the status of completed to said task we map the todoList, what this code does is that if the task.id is equal to the taskId passed as a parameter to the function
task.id === taskId
returns a copy of the task and updates the property completed ? { ...task, completed: !task.completed }
this ...task
is known as "spread operator" if you want to dig further into the topic, in the completed: ! task.completed
, doing this !task.completed
means it will change to the opposite if it was true the new value will be false and vice versa. Finally we call the function filterTask(currentFilter) passing the variable currentFilter as a parameter
function updateTask(taskId) {
todoList = todoList.map((task) =>
task.id === taskId ? { ...task, completed: !task.completed } : task
);
filterTask(currentFilter);
}
- Delete and Clear completed: Both functions use the filter method, in the deleteTask function the filter method returns the tasks whose id is different from the id of the task to delete
task.id !== taskId
, and in the clearCompletedTasks function it returns the tasks whose value is completed is false
function deleteTask(taskId) {
todoList = todoList.filter((task) => task.id !== taskId);
filterTask(currentFilter);
}
function clearCompletedTasks() {
todoList = todoList.filter((task) => !task.completed);
filterTask(currentFilter);
}
- Update Items Left: It is in charge of updating the number of elements in each category, the variable label is created, the result of which will be: if label is equal to "all" it returns "left", otherwise the name of the filter, then it is dynamically updates the quantity: Resulting in: eg: 5 items left, 2 items active, 3 items completed... this according to the selected category
function updateItemsLeftCounter(list) {
const remainingItemCount = list.filter((task) => !task.completed).length;
const label = currentFilter === "all" ? "left" : currentFilter;
itemsCountElement.textContent = `${remainingItemCount} items ${label}`;
}
At the events listeners will have:
checkToAddTask.addEventListener("click", (e) => {
const name = inputAddTask.value.trim(); //Value of the input without spaces
if (name === "") { //If name is an empty string, we prevent the input from being checked
e.preventDefault();
} else { //Otherwise add the new task
addTask(name);
}
});
inputAddTask.addEventListener("keydown", (e) => {
const name = inputAddTask.value.trim();
if (e.key === "Enter" && name !== "") { // In the input if key Enter is pressed and name is not equal to an empty string add the new task
checkToAddTask.checked = true;
addTask(name);
}
});
-
The clear button listener: buttonClear.addEventListener("click", clearCompletedTasks);
-
For the event listener of task kill buttons instead of selecting all buttons with "querySelectorAll" and using forEach, you can listen for the click event on the task container, for example:
- Here, event.target refers to the element that was clicked. .closest(".delete-button") finds the closest ancestor (parent) that has the class delete-button. This allows you to determine if the delete button was clicked, regardless of whether the click happened directly on the button or on an element internal to the button, the same goes for toggleCompleted.
- If deleteButton exists then we get the taskId which is passed as
data-task-id="${task.id}"
attribute to the task container in the renderTodoList function, the deleteButton.closest(".todo-list")
part will access the parent element having the class "todo-list" and .dataset.taskId
will get the data-task-id
attribute and will calls the deleteTask function using the retrieved id as parameter.
todoListContainer.addEventListener("click", (event) => {
// 1
const deleteButton = event.target.closest(".delete-button");
const toggleCompleted = event.target.closest(".checkbox");
if (deleteButton) {
// 2
const taskId = parseInt(deleteButton.closest(".todo-list").dataset.taskId);
deleteTask(taskId);
}
if (toggleCompleted) {
const taskId = parseInt(
toggleCompleted.closest(".todo-list").dataset.taskId
);
updateTask(taskId);
}
});
And finally, the category filter function
categoryButtons.forEach((button) =>
button.addEventListener("click", () => {
//Find an element with the class "is-toggled"
const toggled = document.querySelector(".is-toggled");
// If there is, remove the class is toggled
if (toggled) toggled.classList.remove("is-toggled");
// And add it to the button pressed
button.classList.add("is-toggled");
// Get the name of the filter, remove the trailing spaces and change to lowercase
const filter = button.textContent.trim().toLowerCase();
// Call the filterTask function with the name of the filter
filterTask(filter);
})
);
In the CSS in the todo-list-container class you are defining a height of 390px, this will mean that when adding new tasks they will not be displayed since the height is fixed (always 390px), you can solve it by changing height by min-height this way it will be at least 390px and the container will be able to increase in size as you add new tasks, You can also add a background-color to the container since when deleting the tasks its background is transparent.
.todo-list-container {
background-color: var(--color)
min-height: 390px;
}
Here are some resources related to the code:
Maybe the feedback is too long: D, but I hope it helps you and that it has helped you learn something new. Good luck!.