How to Build a To-do App with HTML, CSS, and Vanilla JavaScript with Local Storage
Tags: webdev, javascript, beginners, programming
Published: 2024-03-04 06:45:00

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:

{% embed https://www.youtube.com/watch?v=u-69cl_J_aU  %}



## HTML Structure
Our HTML will have three sections:

- A message section
- A search box section
- A tasks section

```html
<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:

```css
body {
    background: #000;
    height: 100vh;
    display: flex;
    justify-content: center;
    color: #fff;
  }
```
The container element containing all our sections will have the following styles:

```css
.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.

```css
.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.

```css
.search-box {
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: space-around;
    } 
```
For the input, add the following styles:

```css
.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.

```css
.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%.

```css
.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:

```css
.tasks li .buttons button {
      
      background: transparent;
      border: none;
    }
```
Add the following styles to the icons
```css

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

```
For the radio buttons, we will have these custom styles:

```css
.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.

```css
   .strike-through {
      -webkit-text-stroke: 2px rgb(179, 186, 179);
      text-decoration: line-through;
    }
    .complete {
      background-color: #bfb9b9;
    }
```

Now our app looks like this:
 

![TO DO APP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fpltgsd0xnxpscki9dc4.png)

## 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):

```js
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:

```js
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:

```json
{
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

```js
  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.

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

Update the function as follows.

```js
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()`

```js
function removeTask(){

}
```
Inside the `removeTask()` function, we want to get the data attribute of the li element and remove the task from the DOM.

```js

    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 closest `li `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:

```js
      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 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rqa0q6x4vru6q1oiehq2.png)

### 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.

```css
  .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:


```js
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 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g0ep4yeaymbvyj635vi9.png)

```js

localStorage.setItem("task", "New task");
```
Once you store this data, using chrome dev tools, you can see the data under the Application tab.

![view local storage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d097apf4xwp0q5uylpty.png)

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

```js
localStorage.getItem("tasks")
```

![get items from local storage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z46r8ju52qt1tic4f5m5.png)
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:

```js
  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:

```js
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.

![tasks in local storage ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ig4x1afivj1fogt524mc.png)

### 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.

```js
function loadFromStorage() {
    const storedTasks = localStorage.getItem("tasks");
    if (storedTasks) {
      allTasks = JSON.parse(storedTasks);
      renderTasks();
    }
  }
```

Create the `renderTasks()` function and add the code below.

```js
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 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:

```js
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;

```js
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:

```js
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:

```js
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.](https://practicaljavascript.substack.com/)