The internet is built on the foundation of interacting with data: getting data from users, storing data, updating, and deleting data. A to-do app is the best tool to practice these fundamental skills.
In this tutorial, we will cover how to create a fully functional to-do app with HTML, CSS, and JavaScript. Users will be able to do the following:
- Add tasks
- edit tasks,
- delete tasks and
- mark tasks as complete
By the end of this tutorial, we will have something like this:
HTML Structure
Our HTML will have three sections:
- A message section
- A search box section
- A tasks section
<div class="container">
<section class="message">
</section>
<section class="search-box">
<input type="text" placeholder="Add Task" id="addTaskInput" />
<button class="add-btn"><i class="fa-solid fa-plus"></i></button>
</section>
<section class="tasks">
<ul>
<!-- <li>
<input type="radio" class="complete" checked />
<span class="content complete">Create a Todo App with JavaScript</span>
<div class="buttons">
<button class="edit-btn">
<i class="fa-solid fa-pen-to-square"></i>
</button>
<button class="delete-btn">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</li> -->
</ul>
</section>
</div>
The ul element is empty because that is where we will add tasks dynamically with JavaScript. Each task will have the following elements:
- A radio button to mark the task as complete
- a span element to display the task
- an edit button and a delete button
Styling with CSS
We will start by styling the body to ensure all our elements are centered horizontally:
body {
background: #000;
height: 100vh;
display: flex;
justify-content: center;
color: #fff;
}
The container element containing all our sections will have the following styles:
.container {
padding: 60px 50px;
margin-top: 100px;
width: 500px;
height: 500px;
position: relative;
}
Next style the message section to ensure it is always at the center of the container element.
.message{
text-align: center;
position: absolute;
top: 50%;
left: 30%;
}
For the search-box section, use Flexbox to ensure child elements are aligned at the center and also spaced evenly.
.search-box {
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
}
For the input, add the following styles:
.search-box input {
width: 100%;
height: 30px;
border-radius: 20px;
padding: 10px 10px;
background: rgb(41, 39, 39);
border: none;
color: #fff;
margin-left: 30px;
}
To ensure our tasks will be stacked vertically, set the flex-direction to column, and add some padding and margin to ensure space between individual tasks.
.tasks {
margin-top: 40px;
display: flex;
flex-direction: column;
}
.tasks li {
margin-top: 10px;
padding: 20px 10px;
background: rgb(28, 25, 28);
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 5px;
border-radius: 10px;
position: relative;
}
Use flex-basis to ensure the span element for displaying taks takes up 60 % of the width while the buttons take only 20%.
.tasks li span {
display: flex;
flex-basis: 60%;
}
.tasks li .buttons {
display: flex;
flex-basis: 20%;
}
Remove the default button style and add a transparent background on the edit and delete button to ensure the icons are visible:
.tasks li .buttons button {
background: transparent;
border: none;
}
Add the following styles to the icons
.buttons button i {
color: rgb(175, 16, 16);
font-size: 20px;
}
For the radio buttons, we will have these custom styles:
.tasks input[type="radio"] {
appearance: none;
width: 20px;
height: 20px;
border: 1px solid #999;
border-radius: 50%;
outline: none;
box-shadow: 0 0 5px #999;
transition: box-shadow 0.3s ease;
}
.tasks input[type="radio"]:checked {
background-color: #bfb9b9;
}
Finally, add these styles, which will be added dynamically with JavaScript.
.strike-through {
-webkit-text-stroke: 2px rgb(179, 186, 179);
text-decoration: line-through;
}
.complete {
background-color: #bfb9b9;
}
Now our app looks like this:
JavaScript Functionality
To make it possible for users to add tasks, we will use JavaScript. Let's start by getting the following HTML elements using the DOM(Document Object Model):
const message = document.querySelector(".message");
const tasksContainer = document.querySelector(".tasks");
const ulElement = tasksContainer.querySelector("ul");
const taskBtn = document.querySelector(".add-btn");
Next, let's initialize some variables:
let html = "";
let allTasks = [];
The variable html
will store the html string containing HTML markup representing each task.
The allTasks
array will store all the tasks, each task will have an id (timestamp), a name and a completed value which is be either true or false. A sample task will look like this:
{
id:1700000,
name: "Name of task",
completed:false
}
Add New Task
Well, start by adding a click event listener to the add task button. Inside the event listener function, we will get the input value from the user, pass it to an addTask()
function, and set the value of the input to an empty string.
If the user has not entered a value, we will return: This will prevent adding an empty task to the list or performing unnecessary operations when the user hasn't entered any value
const taskBtn = document.querySelector(".add-btn");
taskBtn.addEventListener("click", function () {
let newTaskInput = document.getElementById("addTaskInput");
const newTask =newTaskInput.value;
console.log(newTask);
if (!newTask) return;
addTask(newTask);
newTaskInput.value = "";
});
Define the addTask()
function.
function addTask(task) {
}
Inside the function, we want to do the following:
- define a task id using the current timestamp
- add the task object to the
allTasks
array - assign the html variable to the task HTML markup
- append the
html
to theulElement
Update the function as follows.
function addTask(task) {
taskId = new Date().getTime();
allTasks.push({ id: taskId, task: task, completed: false });
html = ` <li data-id =${taskId}>
<input type="radio" class="complete-btn" />
<span class="content">${task}</span>
<div class="buttons">
<button class = "edit-btn" ><i class="fa-solid fa-pen-to-square"></i>Edit</button>
<button class="delete-btn"><i class="fa-solid fa-trash"></i>Delete</button>
</div>
</li>`;
ulElement.innerHTML += html;
editTask();
}
As you can see, each li element representing the task has a unique id added as a data attribute value (data-id = ${taskId}
): This will allow us to retrieve the id when editing or deleting a task.
Delete Task
Define a function called removeTask()
function removeTask(){
}
Inside the removeTask()
function, we want to get the data attribute of the li element and remove the task from the DOM.
function removeTask(){
deleteBtn = document.querySelectorAll(".delete-btn");
deleteBtn.forEach((element) => {
element.addEventListener("click", function (e) {
const liElement = this.closest("li");
const taskId = liElement.getAttribute("data-id");
liElement.remove();
});
});
}
Let's break down what is happening in the removeTask()
function.
- Since all the delete buttons have the same class, we have used the
querySelectorAll
property to select all the buttons. - Used forEach to iterate over each button
- for each button, we get the li element closest to the button using
this.closest("li)
(where this refers to the button clicked). - then we remove the
liElement
from the DOM. - Finally, we get the data attribute value of the li element and store it in a variable called
taskId
. We will use this value when we implement local storage
Edit Task
Define a function called editTask()
.Inside this function, we want to do the same steps as the delete button: that is:
- get all the edit buttons
- use
forEach()
method to iterate and get the closestli
element - get the data-id attribute
- use the id to find the task in the
allTasks
array - update the task name in the DOM
Update the editTask()
function as follows:
function editTask() {
editBtn = document.querySelectorAll(".edit-btn");
editBtn.forEach((element) => {
element.addEventListener("click", function (event) {
const liElement = event.target.closest("li");
const taskId = liElement.getAttribute("data-id");
const taskIdIndex = allTasks.findIndex(
(task) => task.id.toString() === taskId
);
if (taskIdIndex !== -1) {
const currentTask = allTasks[taskIdIndex].task;
const newTask = prompt("Edit Task:", currentTask);
if (
newTask !== null &&
newTask !== "" &&
newTask !== currentTask
) {
allTasks[taskIdIndex].task = newTask;
const contentElement = liElement.querySelector(".content");
contentElement.textContent = newTask;
}
}
});
});
}
Let's break down what happening in the editTask()
function above:
- After we get the task id from the data attribute, we use the
findIndex()
method to check if the id exists in theallTaksks
array . - When passed on an array, the
findIndex()
method finds the index of the first element that meets the specified condition. If no element is found, it returns -1 - if the
taskIndex
is not -1, we use thetaskIndex
value to get the current task with this codeallTasks[taskIndex].task
const newTask = prompt("Edit Task", currentTask);
:displays a prompt dialog box with the message "Edit Task:", and the input value is set to the current task content (currentTask
). - The new value is then stored in the
newTask
variable. - The if statement validates the new value entered by the user.
-
allTasks[taskIndex].task = newTask
: updates the new task name in the array. - finally, we update the span content of the current li element with this code:
contentElement.textContent = new Task;
Now, if you click the edit button for any task, you should see this prompt.
Mark Tasks as Complete
To mark a task as complete, we will apply the following CSS classes to the radio button and the content in the li element.
.tasks input[type="radio"]:checked {
background-color: #bfb9b9;
}
.strike-through {
-webkit-text-stroke: 2px rgb(179, 186, 179);
text-decoration: line-through;
}
.complete {
background-color: #bfb9b9;
}
Create the completeTask()
function, which will look like this:
function completeTask() {
completeBtn = document.querySelectorAll(".complete-btn");
completeBtn.forEach((element) => {
element.addEventListener("click", function (e) {
const liElement = event.target.closest("li");
console.log(liElement);
const contentSpan = liElement.querySelector(".content");
contentSpan.classList.add("strike-through");
const taskId = liElement.getAttribute("data-id");
const taskIndex = allTasks.findIndex(
(task) => task.id.toString() === taskId
);
if (taskIndex !== -1) {
allTasks[taskIndex].completed = this.checked;
}
});
});
}
In the completeTask()
function, we are doing the following:
- attaching event listeners to the radio buttons, and for each button, we are getting the task id from the data attribute of the closest li element.
- adding the strike-through CSS classes to the span of the current li element
- using the
findIndex()
method to get the index of the current task from theallTasks
array, then updating the state of the button to checked.
Local Storage Functionality
Even after adding tasks, they will disappear once you refresh the page. To persist storage, we will add local storage functionality.
Local storage is an object that allows you to store data in the browser. The data is stored as strings in key-value pairs. Data stored in the browser will exist even after you close the browser. It will only be deleted if you clear the cache.
Adding this functionality to our project will allow data added to persist even after the page is refreshed or closed.
To store data in local storage, you use setItem, as shown below.
localStorage.setItem("task", "New task");
Once you store this data, using chrome dev tools, you can see the data under the Application tab.
To get the item stored in local storage, use the key as follows:
localStorage.getItem("tasks")
To remove items from local storage
js
localStorage.clear();
Add Tasks to Local Storage
Let's implement the functionality for addding our tasks in local storage. Since we already have all the tasks in the allTasks
array, all we need to do is add the data to local storage like this:
localStorage.setItem("tasks", JSON.stringify(allTasks));
Since data stored in local storage is in string format, we have used JSON.stringify
to convert the task objects into strings for storage.
Update the addTask()
function as follows:
function addTask(task) {
// the rest of the code
localStorage.setItem("tasks", JSON.stringify(allTasks));
}
Now go back and add some tasks, and you should see them in the browser.
Load from Local Storage
We also need to load the tasks from local storage. Create a function called loadFromStorage()
. This function will check if there are tasks in local storage, and if found, the tasks will be rendered on the page using the renderTasks()
function.
function loadFromStorage() {
const storedTasks = localStorage.getItem("tasks");
if (storedTasks) {
allTasks = JSON.parse(storedTasks);
renderTasks();
}
}
Create the renderTasks()
function and add the code below.
function renderTasks() {
ulElement.innerHTML = ""; // Clear existing tasks
allTasks.forEach((task) => {
const completedClass = task.completed
? "complete strike-through"
: "";
const html = `
<li data-id="${task.id}" class="${completedClass}">
<input type="radio" class="complete-btn" ${
task.completed ? "checked" : ""
} />
<span class="content">${task.task}</span>
<div class="buttons">
<button class="edit-btn"><i class="fa-solid fa-pen-to-square"></i>Edit</button>
<button class="delete-btn"><i class="fa-solid fa-trash"></i>Delete</button>
</div>
</li>`;
ulElement.innerHTML += html;
});
editTask();
completeTask();
removeTask();
}
Let's break down what the renderTasks()
function does:
`
- ulElement.innerHTML = "" `: clears any existing tasks on the page
- then, we use the
forEach()
method to iterate over theallTasks
array and add the HTML markup of each task to theulElement
. -
const completedClass=task.completed? "complete strike-through": "":
is a condition that checks iftask.completed
is true and adds the "complete strike-through" CSS class. Iftask.completed
is false, the no CSS class will be applied. - Lastly we will attach the editTask, completeTask and removeTask event listeners.
Update Tasks in Local Storage
To update tasks in local storage, update the editTask()
function as follows:
function editTask() {
editBtn = document.querySelectorAll(".edit-btn");
editBtn.forEach((element) => {
element.addEventListener("click", function (event) {
const liElement = event.target.closest("li");
const taskId = liElement.getAttribute("data-id");
const taskIdIndex = allTasks.findIndex(
(task) => task.id.toString() === taskId
);
if (taskIdIndex !== -1) {
console.log(allTasks[taskIdIndex]);
console.log(allTasks[taskIdIndex].task);
const currentTask = allTasks[taskIdIndex].task;
const newTask = prompt("Edit Task:", currentTask);
if (
newTask !== null &&
newTask !== "" &&
newTask !== currentTask
) {
allTasks[taskIdIndex].task = newTask;
const contentElement = liElement.querySelector(".content");
contentElement.textContent = newTask;
localStorage.setItem("tasks", JSON.stringify(allTasks)); //update this line
}
}
});
});
}
The line localStorage.setItem("tasks",JSON.stringify(allTasks);
will ensure that the current state of tasks is updated after a task is updated.
To remove a task from local Storage, create a deleteTask()
function and add the code below;
function deleteTask(id) {
const taskIdIndex = allTasks.findIndex(
(task) => task.id.toString() === id
);
if (taskIdIndex !== -1) {
allTasks.splice(taskIdIndex, 1);
localStorage.setItem("tasks", JSON.stringify(allTasks));
}
}
In the deleteTask()
function above, we use the id of a task to check if it exists in the allTasks
array. If found, we use the splice()
method to remove the task from the allTasks
array.
Update the removeTasks()
function as follows:
function removeTask() {
deleteBtn = document.querySelectorAll(".delete-btn");
deleteBtn.forEach((element) => {
element.addEventListener("click", function (e) {
const liElement = this.closest("li");
console.log(this);
const taskId = liElement.getAttribute("data-id");
liElement.remove();
deleteTask(taskId); //add this line
});
});
}
The final thing to do is to show the user a message if they have no pending tasks:
function updateMessage() {
if (ulElement.children.length === 0) {
message.innerHTML = "You are all caught up";
} else {
message.innerHTML = "";
}
}
Now we have a fully functioning to-do application. You can find the final demo on Codepen. If you have any questions leave them in the comment section.
Subscribe to the Practical JavaScript newsletter and Learn JavaScript by building projects.