JavaScript30 - Day 15 (LocalStorage & Event Delegation)

This is day 15 in my #javascript30 journey. This is the free course from Wes Bos that lets you brush up on your JavaScript skills via 30 projects.

Yesterday we worked through the differences in Array and Object References and Copies. You can keep track of all the projects we're building here.

Today we're exploring LocalStorage and Event Delegation.


Day 15 - LocalStorage and Event Delegation

Today we are exploring two topics in a longer lesson. The first is LocalStorage and the second is Event Delegation.

Our starter document looks like:

Day 15 of JavaScript30 starter file

We are creating a simple page where we can:

  • Enter names of tapas into the form and add the item.
  • Store the tapas items (along with their 'clicked/not clicked' status) in LocalStorage.
  • Make new tapas clickable using Event Delegation
Saving Tapas

We are able to persist our state with localstorage. We will be storing the tapas objects (name & status) in an array.

First things first, we need to capture the user input from the form. We will add an event listener on the form that triggers when the submit event is fired. This covers off if someone clicks, presses enter or submits by mind control*.

From there we will create an addItem function that:

  • Prevents the default form submission.
  • Grabs the text that the user entered.
  • Creates an object based on the user submission.
  • Push the object into the item array.
  • Reset the form.

This looks like this:

const addItems = document.querySelector('.add-items')  
const itemsList = document.querySelector('.plates')

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

addItems.addEventListener('submit', addItem)  

We will then need to create the second function called populateList that handles the display of the items array in the HTML document. We are passing this function two arguments; the array of objects the user has entered and the HTML element that we want to output the list to.

function populateList(plates = [], platesList) {  
  platesList.innerHTML = plates
    .map((plate, i) => {
      return `
      <li>
      <input type="checkbox" data-index=${i} id="item${i}"
      ${plate.done ? 'checked' : ''} />
        <label for="item${i}">${plate.text}</label>
      </li>
    `
    })
    .join('')
}

*Note: You cannot, at this point, submit forms via mind control.

LocalStorage

Now that we have our array of items we can save this to LocalStorage to persist this list through refreshing the page.

LocalStorage is only a key value store. This means we can only store strings in the value section.

We can set the localStorage within the addItems function:

localStorage.setItem('items', JSON.stringify(items))  

Then we are able to set the default to this on page load. At the very bottom of our script we call:

populateList(items, itemsList)  

Then at the top we check to see whether we can retrieve the array from localStorage and if we can't we load an empty array:

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

So recap:

  • Every time an item is created it is put into localStorage.
  • On page load, localStorage is checked to see if anything is saved. If nothing is saved, we fall back to an empty array.

Our list is now stored in localStorage:

Local Storage persisting the data

Persisting Status with Event Delegation

Our list is stored safely in localStorage but the status of the item (checked/not checked) is not persisted:

Non persisting status

We need to run a toggleDone function when the user clicks on the checkbox beside our items. However, this becomes tricky to do as some of the items are created AFTER the event listener is created. This means they aren't included in the resulting nodeList. This means we are going to need some black magic to select them all.


We are going to use Event Delegation. This allows us to add the event listener on a parent element and have the effect of the event listener bubble to each child.

In our case, we will set the event listener on the <ul class="plates"> element:

itemsList.addEventListener('click', toggleDone)  

This bubbles the event listener down to the children. In our case, there are two children that are clicked when a user clicks the checkbox on our list; the label AND the checkbox. We don't care about clicks on the label so we will discard everything BUT the checkbox.

We then toggle the done property, update the localStorage and visually update the HTML:

function toggleDone(e) {  
  //skip unless this is the input (checkbox)
  if (!e.target.matches('input')) return
  //find the index of the child that was clicked.
  const index = e.target.dataset.index
  //toggle the 'done' attribute
  items[index].done = !items[index].done
  //update the local storage
  localStorage.setItem('items', JSON.stringify(items))
  //update the HTML
  populateList(items, itemsList)
}

Persisting status with event delegation

Additional Buttons

Wes finished the course with a challenge to add 3 buttons:

  • Delete all items.
  • Check all items.
  • Uncheck all items.

To add the delete button I added the following code:
HTML:

<button type="button" name="clearAll" class="delete">Delete</button>  

JavaScript:

const deleteButton = document.querySelector('.delete')

function deleteHandler(e) {  
  localStorage.clear()
  localStorage.setItem('items', JSON.stringify(items))
  populateList([], itemsList)
}

deleteButton.addEventListener('click', deleteHandler)  

To add the Check all and Uncheck all buttons I added the following code:
HTML:

<button type="button" name="checkAll" class="button">Check All</button>  
<button type="button" name="checkNone" class="button">Uncheck All</button>  

JavaScript:

const buttons = document.querySelectorAll('.button')

function handleButton(e) {  
  items.forEach(function(item, index, array) {
    e.target.name === 'checkAll'
      ? (items[index].done = true)
      : (items[index].done = false)
  })
  console.log(items)
  localStorage.setItem('items', JSON.stringify(items))
  populateList(items, itemsList)
}

buttons.forEach(button => button.addEventListener('click', handleButton))  

These work perfectly. Now that everything has been done I have pushed this project live to a Firebase project. You can view it live here.

You can keep track of all the projects in this JavaScript30 challenge here.