DEV Community

Cover image for How I used LocalStorage and Event delegation in a list of items.
Raquel Santos | RS-coding
Raquel Santos | RS-coding

Posted on

How I used LocalStorage and Event delegation in a list of items.

Hi everyone ๐Ÿ‘‹!
How are you doing?

Today I will show you how I did the JavaScript30 - Wes Bos challenge using localStorage and Event Delegation.
The challenge was creating a form with input and a button to add elements and a ul where items render.
Then I used localStorage to save the items to when I refresh the page they will still be there.
And for checking an item is completed I used event delegation which is creating an event in the parent node to manipulate the children of that parent.

Let's get started!

1.- I created the HTML structure for the challenge


<div class="container__img"> 
  <img src="logo_restaurant.png" alt="logo"> 
</div> 

<div class="container__form> 
 <h2> Local Tapas </h2> 

 <ul class="plates-list"> 
    <li>Loading tapas..</li> 
 </ul> 

 <form class="add-items"> 
    <input type="text" name="item" placeholder="Item Name" required> 
    <input type="submit" value="+Add Item" 
 </form> 
</div> 
Enter fullscreen mode Exit fullscreen mode

2.- Added general and specific CSS style in the elements

body{ 
  min-height:100vh; 
   display:flex; 
   flex-direction:column; 
   justify-content:center; 
   align-items:center; 
} 

/*for the background image to give less opacity and dont change the elements above - seen this in css tricks*/ 
body::after{ 
   content:''; 
   background: url(https://images.unsplash.com/photo-1504754524776-8f4f37790ca0?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1050&q=80); 
     background-size:cover; 
    opacity:0.5; 

    top:0; 
    left:0; 
    bottom:0; 
    right:0; 
     position:absolute; 
    z-index:-1; 
}  

.container__img{ 
  width:15rem; 
  margin:2rem; 
} 

img{width:100%;} 

.container__form{ 
   padding:2rem; 
    background-color:white; 
   box-shadow: 0 0 0 10px rgba(0,0,0, 0.1); 
} 

h2{ 
    text-align:center; 
   font-size:3rem; 
   font-weight:200; 
   text-transform: uppercase; 
   margin:1rem 0; 
   letter-spacing: .2rem; 
} 

/*ul*/ 
.plates-list{ 
  text-align:left; 
  list-style-type:none; 
} 

/*input text*/ 
.add-items input{ 
   margin-top:1rem; 
   padding:1rem; 
   outline:0; 
   border:1px solid rgba(70, 78, 70, 0.562); 
} 

/*input submit*/ 
.add-items input[type="submit"]{ 
    background-color:rgb(230, 168, 0); 
    color:white; 
    border:none; 
   padding:1.1rem; 
} 

Enter fullscreen mode Exit fullscreen mode

3.- Now it's time for JavaScript

3.1- First I obtained the elements in the DOM

const itemsList = document.querySelector('.plates-list') //ul 
const addItems = document.querySelector('.add-items') // form 
Enter fullscreen mode Exit fullscreen mode

3.2- I create a variable called items that have as value an empty array that in the future is where all the items will be

const items = [] 
Enter fullscreen mode Exit fullscreen mode

3.3- I added an event listener in the form 'add-items' with an event submit and a function with the name addItem. Inside the function, first I called the method preventDefault() because I didn't want the form to submit. The second thing was creating a variable named 'text' to get the input type text value.
Then I created an object that represents each item that is entered in the input type text. After that, I called the array 'items' and push that object 'item'. Also, I reset the form after pushing the new element into the array and put a focus on the input type text.


  const addItem = function(e){ 

     e.preventDefault() 
     const text = (this.querySelector('[name= item]')).value; // **this** represents the form where the event is listenening. we call the input that as the atribute name with value item 

     const item ={ 
        text: text; // text will have the value of the variable text 
        done: false 
    } 

     items.push(item) 

     this.reset() // reset the form 
     const input = this.querySelector('[name=item]') //get input 
     input.focus() // focus the input  
 } 

  addItems.addEventListener('submit' addItem) 

Enter fullscreen mode Exit fullscreen mode

3.4- At this moment we have updated the array items since we add another new item using the input and the submit.
But we need to render that array into HTML elements to show in our browser. So I created a function called 'populateList' that is a generic function that is responsible to create HTML from an array and being insert in the parent Node. So that function will take two arguments. The first one is an array that has as default an empty array value and the second one is the parent element in which the HTML code will be inserted.

  const populateList = function(platesArr = [], platesList){ 


      platesList.innerHTML = platesArr.map( (plate, i) { 
         return ` 
           <li> 
               <input type="checkbox" data-index=${i} id="item${i} ${plate.done} ? checked : '' "/> 
               <label></label> 
           </li> 
         ` 
      }.join('') 
 } 
Enter fullscreen mode Exit fullscreen mode

First I inserted directly to the parent node, using innerHTML, the result of the next code.
I used map() method in platesArr(an argument that represents an array) that returns a new array filled with the results of the provided function call. the result is a new array so I needed to use method join('') to transform it into strings.
Note that in map method I add two arguments, one the element itself and two the index of the element because I need to use the unique id for each element, also created a data attribute with that index for each element. It will be useful for the checkbox.
Note also, that in the input type checkbox I used a conditional ternary operator to add the attribute checked if the 'plate.done' is not I used empty strings.

3.5- I called the function that I did earlier inside the function addItem with correct arguments. like this:


  const addItem = function(e){ 
             e.preventDefault(); 
             const text = (this.querySelector('[name = item]')).value; 

             const item={ 
                 text:text, 
                 done:false 
            } 

             items.push(item) 

             populateList(items,itemsList) //here items - our array and itemList - our ul  


            //reset the form and focus the input  
             this.reset() 

             const input = this.querySelector('[name=item]') 
             input.focus() 

      } 

Enter fullscreen mode Exit fullscreen mode

3.6- This part is done. Now it's time to show you how I did the checkbox toggle using event delegation. But first I updated the CSS styles that were missing that was the li element, the label and input elements inside the li element and create our custom checkbox.


  .plates-list li{ 
    border-bottom:1px solid rgba(0,0,0,0.2); 
    padding:1rem 0; 
    font-size: 1.6rem; 
     display:flex; 
  } 

  .plates-list label{ 
    flex:1; 
     cursor:pointer; 
  } 

   .plates-list input{ 
      display:none;/* to erase the default checkbox */ 
  } 

   /* custom checkbox */ 
   .plates-list input + label:before{ 
     content:"โฌœ๏ธ"; 
     margin-right:1rem; 
  } 

   .plates-list input:checked + label:before{ 
      content:"๐ŸŒฎ"; 
   } 

Enter fullscreen mode Exit fullscreen mode

3.7- Now it's time for our event delegation. I add an event listener in the 'itemsList' variable(parent Node - ul element ) with click event and created a function called toggleDone.
This function is responsible to toggle the checkbox and update in the items array, the property 'done' from false to true or contrariwise.

Note: what is Event Delegation?
if we have a lot of elements handled similarly, then instead of assigning a handler to each of them โ€“ we put a single handler on their common ancestor - parent element.


const toggleDone = function(e){ 

   if(!e.target.matches('input)) return; 

    const el = e.target; 

    const index =el.dataset.index; 

   items[index].data = !items[index].data 

    populateList(items, itemsList) 
} 

itemsList.addEventListener('click',toggleDone) 
Enter fullscreen mode Exit fullscreen mode

The first thing I did in the function was to check if the target is not the input then I return. because we want only to use the input the checkbox.

Then I created a variable called el for an element that takes as value the e.target to make things simple.

Also, I created a variable called index that takes as value the dataset called index (data-index) of the element.

Note : To get a specific data attribute we created in an element we get with dataset property and the name of the data attribute which is the name before the hyphen: data-.

I call the array 'items' with index [index] that is the variable I created before. Note that normally we write like this items[0], items[1], items[2] but we don't want hard code so I written item[index] that is the index we clicked no matter which number.
In the final, we add the property .done because is that property we want to update from false to true or true to false when clicked on the checkbox items[index].done.
After updating the items array we call again the 'populateList ' function to render the updated information in HTML.

3.8- Now it's time to do one last thing. Saving the things we added by using localStorage, so when we refresh the browser we keep that information added before.

Note: What is localStorage?
localStorage allows developers to store and retrieve data in the browser. The data stored in local storage will not expire. This means the data will persist even if the tab or the browser window is closed.
It is important to note that local storage only stores strings so, if you wish to store objects, lists, or arrays, you must convert them into a string using JSON.stringify().

First, we need to use setItem() (This function is used to store items in local storage.) where we create the new items and push them into an array. So in the addItem function, we need to make an update.

const addItem = function(e){ 
     e.preventDefault(); 
     const text = (this.querySelector('[name = item]')).value; 
     const item={ 
         text:text, 
         done:false 
    } 

     items.push(item) 

     populateList(items,itemsList) 

    //localStorage.setItem 
    //'items' is the key that will be used to getItem 
    //items is the value -array that needs tobe converted into a string using JSON.stringify() 
     localStorage.setItem('items',JSON.stringify(items)) 

     this.reset() 
     const input = this.querySelector('[name=item]') 
     input.focus() 

} 

Enter fullscreen mode Exit fullscreen mode

After doing this I went to the array items to get the elements that are in the localStorage using localStorage.getItem()
and call the function 'populateList' at the end of the code for the first time. Because maybe there are already items to get in localStorage.

const items = JSON.parse(localStorage.getItem('items')) || [] 

... 

populateList(items, itemsList) 
Enter fullscreen mode Exit fullscreen mode

Why localStorage.getItem() in the array? Because it's where we get the items to render. If there are no items in localStorage it's an empty array if there is, we use the localStorage.getItem() to get the items that are stored. And don't forget to use the JSON.parse because we need to convert the data into an object again.

Also, we need to use localStorage.setItem() in the function toggleDone because this function updates the done property in the array.

And that is it!๐ŸŽ‰

To check the full code click here
to see the demo click here

Discussion (0)