TODO APP using HTML, CSS and JS - Local Storage [Interactivity - JavaScript]
Hari Ram
Posted on January 19, 2021
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.
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");
key | Value |
---|---|
name | Dev |
If you want to update something, say you want to change the name to "David",
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
,
localStorage.getItem("name"); // returns 'David'
clear()
If you want to clear all data in the localStorage
, Use clear()
method.
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
.
/* 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,
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.
localStorage.setItem("todos", JSON.stringify(todos));
Data table looks like this,
parse(str)
If you get todos
from localStorage
, it'll return a string.
Say,
localStorage.getItem("todos"); // returns a string
You'll get,
"[{"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.
JSON.parse(localStorage.getItem("todos")); // returns an array.
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>
But using javascript, here we go,
addTodo()
function addTodo() {
// code
}
code
First we need to check whether todos exist, if not return null
.
if (!todos) {
return null;
}
If exists, select #itemsleft
which says number of items uncompleted.
const itemsLeft = document.getElementById("items-left");
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);
});
and finally update #items-left
on start
// 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
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 andindex
. - 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));
}
In this code block,
- Getting todos from localStorage.
- Using
splice
method to delete a particular todo withindex
. - 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.
We need to select DOM first,
const add = document.getElementById("add-btn");
const txtInput = document.querySelector(".txt-input");
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();
});
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
}
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();
});
}
Now call main
when our page loads completely
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.
If you've any questions, you can leave them in the comments or feel free to message me.
👍
Posted on January 19, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.