TODO APP using HTML, CSS and JS - Local Storage [Interactivity - JavaScript]
Tags: webdev, html, css, javascript
Published: 2021-01-19 18:01:34

Hello developers, This is the continuation of my previous post on [TODO APP Design](https://dev.to/hariramjp777/todo-app-using-html-css-and-js-local-storage-design-html-and-css-1m0j) where I covered the design part (HTML and CSS).

Here, In this post, We're going to give interactivity to our page using Vanilla JavaScript. 

Here's a [gif](https://dev-to-uploads.s3.amazonaws.com/i/bafb83a608yccbl69ew6.gif) of what we're going to make.

Here's live [URL](https://hariramjp777.github.io/frontend-todo-app/) and [repository](https://github.com/hariramjp777/frontend-todo-app/)

## Local Storage

Local storage is a place where we can store data locally within the user's browser.

Click ` F12 ` and It'll open developer tools and you'll find local storage section in ` Application ` tab.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/79kif8d90u3qqfyp4gev.PNG)

Data should be stored in local storage in ` key ` : ` value ` pairs.

Local storage can only store strings. Strings are the series of characters enclosed in quotes. 

Ex. ` "Hello" `, ` "1" `, ` "true" `, ` "false" `.

## Set and Get

Methods available in ` localStorage ` to set and get items,

**` setItem(key, value) `**

` setItem ` takes two arguments ` key ` and ` value ` which updates the value associated with the key. If the key doesn't exist, it'll create a new one.

Say,

```js
localStorage.setItem("name", "Dev");
```

| key | Value |
| --- | --- |
| name | Dev |

If you want to update something, say you want to change the name to "David",

```js
localStorage.setItem("name", "David");
```

| key | Value |
| --- | --- |
| name | David |

**` getItem(key) `**

` getItem ` takes an argument ` key ` which returns the ` value ` associated with the ` key `.

Say if you want to get the value of key ` name `,

```js
localStorage.getItem("name"); // returns 'David'
```

**` clear() `**

If you want to clear all data in the ` localStorage `, Use ` clear() ` method.

```js
localStorage.clear(); // clear all
```

## How's ours?

In our case i.e. TODO App, we need to store,

* an actual TODO 
* a boolean to indicate whether the todo is completed or not.

A better way of storing this is by using Javascript ` object `.

```js

/* Data model */

{
  item: "To complete javascript",
  isCompleted: false
}

```

We need to store a lot of TODOS. So, we can use array of objects. Here is the model,

```js
const todos = [
  {
    item: "To complete JavaScript",
    isCompleted: false
  },
  {
    item: "Meditation",
    isCompleted: true
  }
]
```

As I said earlier, ` localStorage ` only stores String. To store an array of objects, we need to convert it into string.

Using JSON methods,

**` stringify(arr) `**

` stringify ` takes an single argument and converts it into string.

```js

localStorage.setItem("todos", JSON.stringify(todos));

```

Data table looks like this,

![Local Storage data table](https://dev-to-uploads.s3.amazonaws.com/i/wrrf8ctp7cj2uch91qxj.PNG)


**` parse(str) `**

If you get ` todos ` from ` localStorage `, it'll return a string.

Say,

```js

localStorage.getItem("todos"); // returns a string

```

You'll get,

```js
"[{"item":"To complete Javascript","isCompleted":false},{"item":"Meditation","isCompleted":true}]"
```

To work on that, we need to convert it back. To do so, we use ` parse `.

` parse ` takes a string and convert it back to an array.

```js
JSON.parse(localStorage.getItem("todos")); // returns an array.
```

![json.parse](https://dev-to-uploads.s3.amazonaws.com/i/za8odah9wf4i22u4tph2.PNG)


## Get all TODOS when page loads

When user loads page, we need to get all todos from localStorage and render them. 

We're going to render a card (todo) like this,

```html
<li class="card">
  <div class="cb-container">
    <input type="checkbox" class="cb-input" />
    <span class="check"></span>
  </div>
  <p class="item">Complete online Javascript course</p>
  <button class="clear">
    <img src="./assets/images/icon-cross.svg" alt="Clear it" />
  </button>
</li>
```
But using javascript, here we go,

**` addTodo() `**

```js
function addTodo() {
  // code
}
```

**code**

First we need to check whether todos exist, if not return ` null `.

```js
if (!todos) {
    return null;
}
```

If exists, select ` #itemsleft ` which says number of items uncompleted.

```js
const itemsLeft = document.getElementById("items-left");
```

and

run ` forEach ` on them and create card and initialize listeners.

```js

// forEach

todos.forEach(function (todo) {

 // create necessary elements

  const card = document.createElement("li");
  const cbContainer = document.createElement("div");
  const cbInput = document.createElement("input");
  const check = document.createElement("span");
  const item = document.createElement("p");
  const button = document.createElement("button");
  const img = document.createElement("img");

  // Add classes

  card.classList.add("card");
  button.classList.add("clear");
  cbContainer.classList.add("cb-container");
  cbInput.classList.add("cb-input");
  item.classList.add("item");
  check.classList.add("check");
  button.classList.add("clear");

  // Set attributes

  card.setAttribute("draggable", true);
  img.setAttribute("src", "./assets/images/icon-cross.svg");
  img.setAttribute("alt", "Clear it");
  cbInput.setAttribute("type", "checkbox");

  // set todo item for card

  item.textContent = todo.item;

  // if completed -> add respective class / attribute

  if (todo.isCompleted) {
    card.classList.add("checked");
    cbInput.setAttribute("checked", "checked");
  }

  // Add click listener to checkbox - (checked or unchecked)

  cbInput.addEventListener("click", function () {
    const correspondingCard = this.parentElement.parentElement;
    const checked = this.checked;
    // state todos in localstorage i.e. stateTodo(index, boolean)
    stateTodo(
      [...document.querySelectorAll(".todos .card")].indexOf(
        correspondingCard
      ),
      checked
    );
    // update class
    checked
      ? correspondingCard.classList.add("checked")
      : correspondingCard.classList.remove("checked");
    // update itemsLeft
    itemsLeft.textContent = document.querySelectorAll(
      ".todos .card:not(.checked)"
    ).length;
  });

  // Add click listener to clear button - Delete

  button.addEventListener("click", function () {
    const correspondingCard = this.parentElement;
    // add class for Animation
    correspondingCard.classList.add("fall");
    // remove todo in localStorage i.e. removeTodo(index)
    removeTodo(
      [...document.querySelectorAll(".todos .card")].indexOf(
        correspondingCard
      )
    );
    // update itemsLeft and remove card from DOM after animation 
    correspondingCard.addEventListener("animationend", function(){
      setTimeout(function () {
        correspondingCard.remove();
        itemsLeft.textContent = document.querySelectorAll(
          ".todos .card:not(.checked)"
        ).length;
      }, 100);
    });
  });

  // parent.appendChild(child)

  button.appendChild(img);
  cbContainer.appendChild(cbInput);
  cbContainer.appendChild(check);
  card.appendChild(cbContainer);
  card.appendChild(item);
  card.appendChild(button);
  document.querySelector(".todos").appendChild(card);
});
```

and finally update ` #items-left ` on start

```js
// Update itemsLeft
itemsLeft.textContent = document.querySelectorAll(
  ".todos .card:not(.checked)"
).length;
```

## Spread operator **` [...] `**

We're using `[...]` in our code and it is called spread syntax. 

Actually ` .querySelectorAll() ` returns ` NodeList ` on which we can't run array methods. 

To update/delete data in localStorage, ` removeTodo ` and ` stateTodo ` needs index.

So, we should convert it into an array and run ` indexOf() ` to get the index of a card.

` [...document.querySelectorAll(".todos .card")] ` returns an array and we can run array methods on it.


## ` stateTodo `

```js
function stateTodo(index, completed) {
  const todos = JSON.parse(localStorage.getItem("todos"));
  todos[index].isCompleted = completed;
  localStorage.setItem("todos", JSON.stringify(todos));
}
```

In this code block,

* Getting todos from `localStorage`.
* Update isCompleted based on the ` completed ` boolean argument and ` index `.
* Set todos back to localStorage.

## ` removeTodo `

```js
function removeTodo(index) {
  const todos = JSON.parse(localStorage.getItem("todos"));
  todos.splice(index, 1);
  localStorage.setItem("todos", JSON.stringify(todos));
}
```

In this code block,

* Getting todos from localStorage.
* Using ` splice ` method to delete a particular todo with ` index `.
* Setting todos back to localStorage.


## When user adds new Todo

Above code renders todo only when page loads. But we should make it to render live when user adds new Todo using input field.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/h2erc0l02zzyan3amka8.PNG)

We need to select DOM first,

```js
const add = document.getElementById("add-btn");
const txtInput = document.querySelector(".txt-input");
```

Add click listener to button,

```js
add.addEventListener("click", function () {
  const item = txtInput.value.trim(); // del trial and lead space
  if (item) {
    txtInput.value = "";
    const todos = !localStorage.getItem("todos")
      ? []
      : JSON.parse(localStorage.getItem("todos"));
    const currentTodo = {
      item,
      isCompleted: false,
    };
    addTodo([currentTodo]); // add Todo to DOM
    todos.push(currentTodo); // push todo to localStorage
    localStorage.setItem("todos", JSON.stringify(todos));
  }
  txtInput.focus();
});
```

**` addTodo([currentTodo]) `**

Instead of writing new function to render todos live on input, just we can make a small change to our existing function ` addTodo() `.

we can make use of default arguments.

```js
function addTodo(todos = JSON.parse(localStorage.getItem("todos"))){
  // code
}
```

This means by default, ` todos ` equals array in localStorage if no arguments provided. (Used at start when page loads)

When it is user action, we provide arguments like we did, ` addTodo([currentTodo]) `.

` currentTodo ` is an object but ` addTodo ` requires an array in order to run ` forEach `.

So, ` [currentTodo] ` will help us i.e., create a new array and push object ` currentTodo ` onto it.

## That's it

Now we create a ` main ` function and call addTodo() from the ` main `.

```js
function main(){
  addTodo(); // add all todos, here no arguments i.e., load all
  
  // add todo on user input  

  const add = document.getElementById("add-btn");
  const txtInput = document.querySelector(".txt-input");
  add.addEventListener("click", function () {
    const item = txtInput.value.trim();
    if (item) {
      txtInput.value = "";
      const todos = !localStorage.getItem("todos")
        ? []
        : JSON.parse(localStorage.getItem("todos"));
      const currentTodo = {
        item,
        isCompleted: false,
      };
      addTodo([currentTodo]); // with an argument i.e. add current
      todos.push(currentTodo);
      localStorage.setItem("todos", JSON.stringify(todos));
    }
    txtInput.focus();
  });
}
```

## Now call ` main ` when our page loads completely

```js
document.addEventListener("DOMContentLoaded", main);
```

` DOMContentLoaded ` fires when our page (HTML DOM) loads completely.

If the event fires, it'll call ` main ` function which then handles the rest.

That's it for this post guys. If you've trouble understanding here, you can check out my [repository](https://github.com/hariramjp777/frontend-todo-app).

If you've any questions, you can leave them in the comments or feel free to message me.

:thumbsup: