DEV Community

loading...
Cover image for TODO APP using HTML, CSS and JS - Local Storage [Interactivity - JavaScript]

TODO APP using HTML, CSS and JS - Local Storage [Interactivity - JavaScript]

Hari Ram
Hello Everyone, I'm Hari Ram
Updated on ・6 min read

Hello developers, This is the continuation of my previous post on TODO APP Design 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 of what we're going to make.

Here's live URL and repository

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

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,

localStorage.setItem("name", "Dev");
Enter fullscreen mode Exit fullscreen mode
key Value
name Dev

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

localStorage.setItem("name", "David");
Enter fullscreen mode Exit fullscreen mode
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,

localStorage.getItem("name"); // returns 'David'
Enter fullscreen mode Exit fullscreen mode

clear()

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

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

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.


/* Data model */

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

Enter fullscreen mode Exit fullscreen mode

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

const todos = [
  {
    item: "To complete JavaScript",
    isCompleted: false
  },
  {
    item: "Meditation",
    isCompleted: true
  }
]
Enter fullscreen mode Exit fullscreen mode

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.


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

Enter fullscreen mode Exit fullscreen mode

Data table looks like this,

Local Storage data table

parse(str)

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

Say,


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

Enter fullscreen mode Exit fullscreen mode

You'll get,

"[{"item":"To complete Javascript","isCompleted":false},{"item":"Meditation","isCompleted":true}]"
Enter fullscreen mode Exit fullscreen mode

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.

JSON.parse(localStorage.getItem("todos")); // returns an array.
Enter fullscreen mode Exit fullscreen mode

json.parse

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,

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

But using javascript, here we go,

addTodo()

function addTodo() {
  // code
}
Enter fullscreen mode Exit fullscreen mode

code

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

if (!todos) {
    return null;
}
Enter fullscreen mode Exit fullscreen mode

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

const itemsLeft = document.getElementById("items-left");
Enter fullscreen mode Exit fullscreen mode

and

run forEach on them and create card and initialize listeners.


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

and finally update #items-left on start

// Update itemsLeft
itemsLeft.textContent = document.querySelectorAll(
  ".todos .card:not(.checked)"
).length;
Enter fullscreen mode Exit fullscreen mode

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

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

In this code block,

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

removeTodo

function removeTodo(index) {
  const todos = JSON.parse(localStorage.getItem("todos"));
  todos.splice(index, 1);
  localStorage.setItem("todos", JSON.stringify(todos));
}
Enter fullscreen mode Exit fullscreen mode

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

We need to select DOM first,

const add = document.getElementById("add-btn");
const txtInput = document.querySelector(".txt-input");
Enter fullscreen mode Exit fullscreen mode

Add click listener to button,

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

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.

function addTodo(todos = JSON.parse(localStorage.getItem("todos"))){
  // code
}
Enter fullscreen mode Exit fullscreen mode

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.

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

Now call main when our page loads completely

document.addEventListener("DOMContentLoaded", main);
Enter fullscreen mode Exit fullscreen mode

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.

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

👍

Discussion (0)