In this guide, we would go over creating a to-do list app in JavaScript, where you can
- Add a to-do list,
- You can delete an individual item,
- You can search or filter an item,
- You can clear all the items, prevents clearing multiple items by mistake
- Prevents Adding an empty Todo item, prevents deleting an item by mistake
- and persist the todo list even after closing the application
Prerequisite for this guide is as follows:
- HTML, CSS, Basics of JavaScript
- >[Window Objects, Methods, and Properties In JavaScript] + Document Object
- Working With The Local & Session Storage In JavaScript
HTML Structure of the To-Do App
Let's get started, here is what my HTML structure looks like:
-----------------------------
-----------------------------
[[audio]]
<div id ="container" class="content-wrapper text-center">
<div class="content-wrapper text-center">
<div class="container-fluid">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card bg-light">
<div class="card-body">
<h2 class="card-header">To-Do App</h2>
</div>
<div class="container-fluid">
<div class="row justify-content-lg-start">
<!-- Todo Input-->
<div class="col-6 col-sm-5 col-md-4 col-lg-3 m-auto">
<form class="form-group">
<label for="task" class="w-100"></label>
<input type="text" class="form-control " id="input" placeholder="Type Todo" name="email" value="" autofocus>
<input type="submit" value="Add Todo" id = "add-todo" class="btn mt-10">
</form>
</div>
</div>
</div>
<!-- Todo Filter Input-->
<div class="col-6 col-sm-10 mb-10 m-auto">
<label for="filter" class="w-100"></label>
<input type="text" class="form-control" id="filter" placeholder="Filter Todo" name="email" value="" autofocus>
</div>
<div class="col-6 col-sm-5 col-md-4 col-lg-3 m-auto">
<h4>To-do List</h4>
</div>
<!-- Todo Items -->
<ul class="collection row align-items-center">
</ul>
<!-- Todo Clear All Button-->
<div class="col-6 col-sm-5 col-md-4 col-lg-3 m-auto">
<input type="submit" value="Clear All" id = "clear-all" class="btn mt-10">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
----------------------
----------------------
The above structure is within the body element, and besides that, I am using Halfmoon CSS Framework for the styling, here is how it looks on the page:
Let's start with collecting or getting the UI elements, I have comments on the HTML structure, so, you know what to select, I'll be selecting the following elements:
// Select The UI Elements
const todoForm = document.querySelector('.form-group');
const todoInput = document.getElementById('input');
const todoItemParent = document.querySelector('.collection');
const todoClearAllBtn = document.getElementById('clear-all');
const todoFilter = document.getElementById('filter');
We don't need to define an element for the submit button, we would use event delegation to target that since we already called it's parent, in our case, the parent is form.form-group.
Add Event Function
The next thing we are going to do is to load all the event handlers, and our first event type would be of a submit type:
// Load all event listeners
loadallEvents();
// Load all event listeners
function loadallEvents() {
// Add task event
todoForm.addEventListener('submit', addTodo);
}
Recall, that we are storing the form elements and its child in the todoForm variable, here:
const todoForm = document.querySelector('.form-group');
Once, we load all the event listeners, it means they are all in memory, and can be triggered when called. So, the document.querySelector is used to select the form element that houses the input box, and the submit button, we then added an event listeners in our handler that listen to the submit event type and once a submit has been captured, the event listener passes the logic of how it should respond to the addTodo function, in the addTodo function, I'll do something like this:
.................
.................
// Load all event listeners
loadallEvents();
// Load all event listeners
function loadallEvents() {
todoForm.addEventListener('submit', addTodo); // Add Todo event
}
// Add Todo
function addTodo(e) {
if(todoInput.value.replace(/\s/g,"") === "") {
alert('Add a task');
} else {
// Create li element
const li = document.createElement('li');
// Add class
li.className = 'items m-auto mb-20';
// Create text node and append to li
li.appendChild(document.createTextNode(todoInput.value)); // what ever the user types
// Create new link element
const link = document.createElement('a');
// Add class
link.className = 'btn btn-danger ml-20 m-auto';
// Create text node and append to link
link.appendChild(document.createTextNode('Delete'));
// Append the link to li
li.appendChild(link);
// Append li to ul
todoItemParent.appendChild(li);
}
// Clear input
todoInput.value = '';
e.preventDefault();
}
NOTE: This ........ means its a continuation
So, let's decipher what the function is doing:
When the user type something into the input box, and click the submit button, we want to create the following element structure, and get it appended to ul:
<li class="items m-auto mb-20">
User Input
<a href="#" class="btn btn-danger ml-20 m-auto">Delete</a>
</li>
But before creating the structure we checked if the value of the todoInput has an empty string or even blank spaces, if it does, we alert the user to add a task, if otherwise, we create the li element and a link element, I appended the a tag to the li element, and lastly, I appended the li itself to the ul tag, which gives us the structure above.
Still in the function, outside of the if and else block, I cleared the input each time the user clicks the submit button, and lastly we prevented the submit default behaviour. Here is an illustration of how it works:
Let's create a function for removing the item.
Delete Todo Function
Add a click event to the handler loader, we target the ul element:
.......................
.......................
// Load all event listeners
loadallEvents();
// Load all event listeners
function loadallEvents() {
todoForm.addEventListener('submit', addTodo); // Add Todo event
todoItemParent.addEventListener('click', removeTodo); // Remove Todo event
}
.......................
.......................
This just means that we are listening to any click event type in the ul element, and once a click is detected, we fire up the removeTodo function, and we would define it like so:
......................
.....................
// Remove Todo
function removeTodo(e) {
if(e.target.classList.contains('btn-danger')) {
if(confirm('Are You Sure?')) {
e.target.parentElement.remove();
}
}
}
......................
.....................
Once you click the delete button, e.target would return: <a class="btn btn-danger ml-20 m-auto">Delete</a>
we then say if e.target.classList contains a class name btn-danger, if it does, we would ask the user a confirmation about the deletion, if the user click yes, we remove the todo item.
Here is an illustration:
Clear All Todo Function
Add a click event to the handler loader for the clear all button:
// Load all event listeners
loadallEvents();
// Load all event listeners
function loadallEvents() {
todoForm.addEventListener('submit', addTodo); // Add Todo event
todoItemParent.addEventListener('click', removeTodo); // Remove Todo event
todoClearAllBtn.addEventListener('click', clearTodos); // Clear-All Todo event
}
Once you click the cler all button, we would fire the clearTodos function, and define it like so:
// Clear All Todos
function clearTodos() {
if (confirm('Delete All Todos?')) {
while (todoItemParent.firstChild) {
todoItemParent.removeChild(todoItemParent.firstChild);
}
}
}
This is really simple, we first ask the user if they are sure they actually want to delete all of the toDo items, if they enter yes, we use while loop to loop through the firstchild of the todoItemParent, which is li and we then remove all of the li items.
You can also do:
// Clear All Todos
function clearTodos() {
if (confirm('Delete All Todos?')) {
taskList.innerHTML = '';
}
}
But the later is more faster, it's your choice either way.
Filter Todo Function
Add a keyup (this fires once the user release there keystroke) event to the handler loader for the filter input:
....................
....................
// Load all event listeners
loadallEvents();
// Load all event listeners
function loadallEvents() {
todoForm.addEventListener('submit', addTodo); // Add Todo event
todoItemParent.addEventListener('click', removeTodo); // Remove Todo event
todoClearAllBtn.addEventListener('click', clearTodos); // Clear-All Todo event
todoFilter.addEventListener('keyup', filterTodos); // Filter Todo event
}
...................
...................
If the user release their keystroke when typing, it fires the keyup event, and we build the logic of the next action in the filterTodos handler, here is how the function would be define:
// Filter Todos
function filterTodos(e) {
const filterInput = e.target.value.toLowerCase();
getAllTodoItems = document.querySelectorAll('.items')
getAllTodoItems.forEach(function(todo){
const item = todo.firstChild.textContent;
if(item.toLowerCase().indexOf(filterInput) != -1){
todo.style.display = 'block';
} else {
todo.style.display = 'none';
}
});
}
- filterInput would store anything the user types, and convert it to lowercase, this is so we can match the pattern correctly
- getAllTodoItems gets all of the nodelist of the todo items, which we can then loop through
- We then use the forEach loop to loop through the todoItems, each item get passed to the todo parameter
const item = todo.firstchild.textcontent
is used to get all of the text in the todo items, like, if you have<li>buy pizza</li>
and<li>get vacation</li>
it would store onlybuy pizza
for the first loop run, and thenget vacation
for the second loop run. In short, todo.firstchild.textcontent would grab all of the text content in the li tag as the array method loops through.if(item.toLowerCase().indexOf(filterInput) != -1){
The indexOf() method returns the first index at which a given element can be found in the array, or -1 if it is not present. So, we are using the indexOf to locate the value in the filterInput, if the value is not present in the item variable, we set todo.style.display = 'block'; if the value is present, we set it to todo.style.display = 'none';
Here is an illustration of how it works:
Persist Todo Lists To Local Storage
As it is right now, it only takes a browser refresh for your todo items to be gone, and this is because we are not persisting it to any storage. We can persist our todo items in the browser local storage, this way, items won't get deleted even if you close the browser or refreshes the page.
The first thing we should do is to create a function that stores the todo item in the localStorage, we would add that in the addTodo function since that would mean the moment the user add an item, and we are done creating the element and stuff, we also add it to out localStorage, I'll do at the end of the else statement in the addTodo function:
// Add Todo
function addTodo(e) {
if(todoInput.value.replace(/\s/g,"") === "") {
alert('Add a task');
} else {
// Create li element
const li = document.createElement('li');
// Add class
li.className = 'items m-auto mb-20';
// Create text node and append to li
li.appendChild(document.createTextNode(todoInput.value)); // what ever the user types
// Create new link element
const link = document.createElement('a');
// Add class
link.className = 'btn btn-danger ml-20 m-auto';
// Create text node and append to link
link.appendChild(document.createTextNode('Delete'));
// Append the link to li
li.appendChild(link);
// Append li to ul
todoItemParent.appendChild(li);
// Store in LS
storeTodoToLocalStorage(todoInput.value);
}
// Clear input
todoInput.value = '';
e.preventDefault();
}
storeTodoToLocalStorage(todoInput.value);
I passed in the todoInput.value which is the users value to the storeTodoToLocalStorage, so, let's create the function, I'll do that below the addTodo function:
---------------------
---------------------
// Store Todo To localStorage
function storeTodoToLocalStorage(todo){
let values;
if(localStorage.getItem('todos') === null){
values = [];
} else {
values = JSON.parse(localStorage.getItem('todos'));
}
values.push(todo);
localStorage.setItem('todos', JSON.stringify(values));
}
------------------------
-----------------------
The function we called in the addTodo else block has this: storeTodoToLocalStorage(todoInput.value);
The todoInput.value we passed in the function when calling it is what we used in the above function when defining it. So, when defining the function we want it to accept one parameter: function storeTodoToLocalStorage(todo)
, which is what we have passed when calling the function. So, let me explain what the function is doing:
I started by declaring a variable "values" that would hold the input passed if there is any or an empty array if there is none.
In the if block I checked if the key (in my case, my key is called todos
) is empty, and if it is empty, I set the values
variable to an empty array, the values
variable would be the one to store items in the localStorage.
Moving on, if the if statement is false, that is if there is truly a value in the todo key, the else block to store whatever is in the key “todos” value into the values
variable, here:
values = JSON.parse(localStorage.getItem('todos'));
We can’t just get the value, we need to parse it with JSON, and that is because the values or strings should be stored in a JSON format.
Once, we’ve gotten the value, we use values.push(todo) to push whatever is in the todo input (what the user types in the input box) to the values variable, it would append it to the end of what is already in the values variable.
Lastly, to store it in the localStorage we add the key, in my case I want to use the key named todo
and you use JSON.stringify to converts a JavaScript object or value to a JSON string, Here:
localStorage.setItem('todos', JSON.stringify(values));
Now, if you add an item to the todo list they also get saved in the localStorage:
There are a couple of issues we need to fix, the first one is if the user deletes a todo item, we should also delete it from the localStorage, the second one is if the user clears all the todo items with the clear button, we should also remove everything from the localStorage, and the last one is if a page reloads, the data in the localstorage should get prepopulated back to our UI.
Let's start with the simple one, the clear all button: Goto your clearTodos function and add localStorage.clear(); in the function like so:
// Clear All Todos
function clearTodos() {
if (confirm('Delete All Todos?')) {
while (todoItemParent.firstChild) {
todoItemParent.removeChild(todoItemParent.firstChild);
}
localStorage.clear(); // Clear All the data from localStorage
}
}
That's all you ever need to do to clear all the todos in your localStorage. Once the user clicks the clear-all button, it would ask for confirmation, if yes is clicked, it would remove the todo items as well as the localstorage items, if otherwise, nothing would be done.
Now, whenever the page reloads, we want the user to see the data stored in the localStorage in our UI too, so, to do this, we add an event listener of DOMContentLoaded, which means it should fire once the page loads, we then add a getTodos handler that handles the logic of how the data would be prepopulated:
Add: document.addEventListener('DOMContentLoaded', getTodos);
to the top of the function that loads the event handler:
------------------------
------------------------
// Load all event listeners
function loadallEvents() {
document.addEventListener('DOMContentLoaded', getTodos); // DOM Load event
todoForm.addEventListener('submit', addTodo); // Add Todo event
todoItemParent.addEventListener('click', removeTodo); // Remove Todo event
todoClearAllBtn.addEventListener('click', clearTodos); // Clear-All Todo event
todoFilter.addEventListener('keyup', filterTodos); // Filter Todo event
}
------------------------
------------------------
add we would define the getTodos function like so:
// Get Todos from localStorage
function getTodos() {
let values;
if(localStorage.getItem('todos') === null){
values = [];
} else {
values = JSON.parse(localStorage.getItem('todos'));
}
values.forEach(function(todo){
// Create li element
const li = document.createElement('li');
// Add class
li.className = 'items m-auto mb-20';
// Create text node and append to li
li.appendChild(document.createTextNode(todo)); // what ever the user types
// Create new link element
const link = document.createElement('a');
// Add class
link.className = 'btn btn-danger ml-20 m-auto';
// Create text node and append to link
link.appendChild(document.createTextNode('Delete'));
// Append the link to li
li.appendChild(link);
// Append li to ul
todoItemParent.appendChild(li);
});
}
To get the value out from the localStorage, you can either use a forEach, which is an Array method that we can use to execute a function on each item in an array, or you can use for/of loop, you can’t use for loop or while loop because they won’t know how to get the amount of item in the list. I used a forEach loop above.
Before using the loop, we checked if there is a value in the key, if there is, we store an empty array, if otherwise, we get the values and store in the values
variable, and the next point of action is to loop through the items in the values array to re-build the todo items.
The forEach loop takes a callback function. The function will be executed for every single element of the array. It must take at least one parameter which represents the elements or item of the array, in our case, we are telling the function to build elements for each item in the array, and that should get back the todo item even if you close your browser and revisit.
The last thing we need to do is if a todo item is removed, we also remove it from the localStorage.
In the removeTodo function, we get the li element as that is what is holding the todoitem, which we then pass as an argument to a removeTodoFromlocalStorage function like so:
function removeTodo(e) {
if(e.target.classList.contains('btn-danger')) {
if (confirm('Are You Sure?')) {
e.target.parentElement.remove();
}
}
// get li element. and pass it to the removeTodoFromLocalStorage as an argument
let getli = e.target.parentElement
removeTodoFromLocalStorage(getli);
}
We define the function like so:
// Remove Todo From Local Storage
function removeTodoFromLocalStorage(todoItem) {
let values;
if(localStorage.getItem('todos') === null){
values = [];
} else {
values = JSON.parse(localStorage.getItem('todos'));
}
values.forEach(function(todo, index){
if(todoItem.firstChild.textContent === todo){
values.splice(index, 1);
}
});
localStorage.setItem('todos', JSON.stringify(values));
}
As usual, we checked if the key value is empty, if yes, we store an empty array, if no, we get the data and store in a variable name "values"
We then loop through the values with a forEach array method by passing two parameters, one which is todo, would be the one to store individual todo item get from the array, and the other which is index would keep track of the index count.
The if condition is checking if the textContent of the todoItem (the one passed into the function) is strictly equal to the todo (remember todo store individual todo item as loop iterates), if that is true, we use splice function to remove whatever index the item is located. Note that, this isn't actually storing anything yet, we just spliced an item identified by a certain index from the values array.
To then store the value to the localStorage, we use: localStorage.setItem('todos', JSON.stringify(values));
and that's it.