DEV Community

Cover image for How to Build a To-do App with HTML, CSS, and Vanilla JavaScript with Local Storage
vaatiesther
vaatiesther

Posted on

How to Build a To-do App with HTML, CSS, and Vanilla JavaScript with Local Storage

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

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%;
    }
Enter fullscreen mode Exit fullscreen mode

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;
    } 
Enter fullscreen mode Exit fullscreen mode

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;

  }
Enter fullscreen mode Exit fullscreen mode

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;

    }
Enter fullscreen mode Exit fullscreen mode

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%;

  }
Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

Add the following styles to the icons


.buttons button i {
      color: rgb(175, 16, 16);
      font-size: 20px;
    }

Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

Now our app looks like this:

TO DO APP

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");

Enter fullscreen mode Exit fullscreen mode

Next, let's initialize some variables:

let html = "";
let allTasks = [];
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 = "";

  });
Enter fullscreen mode Exit fullscreen mode

Define the addTask() function.

function addTask(task) {

}

Enter fullscreen mode Exit fullscreen mode

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 the ulElement

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();

}
Enter fullscreen mode Exit fullscreen mode

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(){

}
Enter fullscreen mode Exit fullscreen mode

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();
      });
    });

    }
Enter fullscreen mode Exit fullscreen mode

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 closest lielement
  • 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;

              }
            }
          });
        });
      }

Enter fullscreen mode Exit fullscreen mode

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 the allTaksks 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 the taskIndex value to get the current task with this code allTasks[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.

To do app

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;
  }
Enter fullscreen mode Exit fullscreen mode

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;

      }
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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 the allTasks 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.

add to local storage


localStorage.setItem("task", "New task");
Enter fullscreen mode Exit fullscreen mode

Once you store this data, using chrome dev tools, you can see the data under the Application tab.

view local storage

To get the item stored in local storage, use the key as follows:

localStorage.getItem("tasks")
Enter fullscreen mode Exit fullscreen mode

get items from local storage
To remove items from local storage

localStorage.clear();
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));

}
Enter fullscreen mode Exit fullscreen mode

Now go back and add some tasks, and you should see them in the browser.

tasks in local storage

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();
    }
  }
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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 the allTasks array and add the HTML markup of each task to the ulElement.
  • const completedClass=task.completed? "complete strike-through": "": is a condition that checks if task.completed is true and adds the "complete strike-through" CSS class. If task.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
        }
      }
    });
  });
}

Enter fullscreen mode Exit fullscreen mode

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));
  }
}
Enter fullscreen mode Exit fullscreen mode

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 

    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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 = "";
  }
}

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)