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:
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 `
${plate.text}
`
})
.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:
Persisting Status with Event Delegation
Our list is stored safely in localStorage but the status of the item (checked/not checked) is not persisted:
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 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)
}
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:
Delete
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:
Check All
Uncheck All
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.