I am creating this post as notes from Wes Bos Javascript course which you can sign up for and do with me here: https://beginnerjavascript.com/. Here is a link to Wes' GitHub readme.md.
In my last article we created a sarcastic text generator. Today, we will create a shopping list with custom events, delegation (listen on events that happen in the future), and use local storage. This project is similar to one I created recently in jQuery, check it out here. This one is a bit more advanced than any of the previous articles so I'd recommend checking those out first if you haven't already. Without further ado, let's get started.
This is what the final project will look like:
Getting Started
Turn on some tunes, I recommend this Google music station. Get your Visual Studio Code editor ready. I recommend a couple of extensions to make this a bit easier:
- Prettier
- Live Server
- indent-rainbow
- Rainbow Brackets
If you haven't read up on my DOM Manipulation or Events articles yet, everything covered in this guide relies heavily on knowledge dropped there.
First, you need some HTML. You can nab that from Wes' Github Repo here. It contains the beginnings and the final project. Things in that repo change between when this was written to now and I wanted to give you a reference in case you need to see the updated `finished` javascript files. Here is the HTML
HTML Template
Create a directory and name it "Shopping". In it we will need a few files: index.hml
, shopping.css
and shopping.js
. Copy paste the HTML below into the index.html file.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Shopping List</title> <link rel="stylesheet" href="shopping.css"> </head> <body> <div class="shopping-list"> <form class="shopping" autocomplete="off"> <input type="text" name="item" id="item" required> <button type="submit">+ Add Item</button> <br> </form> <ul class="list"></ul> </div> <script src="shopping.js"></script> </body> </html>
You will see a shopping list with a form input and a button. Pretty basic. Something you may not be familiar with on line 13 is autocomplete="off"
, this will prevent your browser from trying to auto complete the input field (kind of obvious after you read that huh?). You can also use autocapitalize="on"
as well if you'd like (I'm not).
CSS Template
Copy paste the CSS below into your shopping.css
/* normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */button,hr,input{overflow:visible}progress,sub,sup{vertical-align:baseline}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}details,main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{padding:.35em .75em .625em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none} /* Variables */ html { --grey: #e7e7e7; --gray: var(--grey); --blue: #0072B9; --pink: #D60087; --yellow: #ffc600; --black: #2e2e2e; --red: #c73737; --green: #61e846; --text-shadow: 2px 2px 0 rgba(0,0,0,0.2); --box-shadow: 0 0 5px 5px rgba(0,0,0,0.2); font-size: 62.5%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; box-sizing: border-box; } *, *:before, *:after { box-sizing: inherit; } body { font-size: 2rem; line-height: 1.5; background-color: var(--blue); background-image: url("data:image/svg+xml,%3Csvg width='20' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 21.184c.13.357.264.72.402 1.088l.661 1.768C4.653 33.64 6 39.647 6 50c0 10.271-1.222 15.362-4.928 24.629-.383.955-.74 1.869-1.072 2.75v6.225c.73-2.51 1.691-5.139 2.928-8.233C6.722 65.888 8 60.562 8 50c0-10.626-1.397-16.855-5.063-26.66l-.662-1.767C1.352 19.098.601 16.913 0 14.85v6.335zm20 0C17.108 13.258 16 8.077 16 0h2c0 5.744.574 9.951 2 14.85v6.334zm0 56.195c-2.966 7.86-4 13.123-4 22.621h2c0-6.842.542-11.386 2-16.396v-6.225zM6 0c0 8.44 1.21 13.718 4.402 22.272l.661 1.768C14.653 33.64 16 39.647 16 50c0 10.271-1.222 15.362-4.928 24.629C7.278 84.112 6 89.438 6 100h2c0-10.271 1.222-15.362 4.928-24.629C16.722 65.888 18 60.562 18 50c0-10.626-1.397-16.855-5.063-26.66l-.662-1.767C9.16 13.223 8 8.163 8 0H6z' fill='%23fff' fill-rule='nonzero' fill-opacity='.1' opacity='.349'/%3E%3C/svg%3E%0A"); background-size: 15px; } /* Table Styles */ table { border-radius: 5px; overflow: hidden; margin-bottom: 2rem; border-collapse: collapse; } td, th { border: 1px solid var(--grey); padding: 0.5rem; } /* Helper Divs */ .wrapper { max-width: 1000px; margin: 4rem auto; padding: 2rem; background: white; } .box, .wrapper { box-shadow: 0 0 3px 5px rgba(0,0,0,0.08653); } a { color: var(--blue); text-decoration-color: var(--yellow); } a.button, button, input[type="button"] { color: white; background: var(--pink); padding: 1rem; border: 0; border: 2px solid transparent; text-decoration: none; font-weight: 600; font-size:2rem; } :focus { outline-color: var(--pink); } fieldset { border: 1px solid black; } input:not([type="checkbox"]):not([type="radio"]), textarea, select { display: block; padding: 1rem; border: 1px solid var(--grey); } .success { border: 1px solid red; } h1, h2, h3, h4, h5, h6 { color: white; margin-top: 0; line-height: 1; text-shadow: var(--text-shadow); } .wrapper h1, .wrapper h2, .wrapper h3, .wrapper h4, .wrapper h5, .wrapper h6 { color: var(--black); text-shadow: none; } /* PROJECT SPECIFIC CSS BELOW HERE*/ body { min-height: 100vh; display: grid; align-items: start; justify-items: center; } /* Shopping Form */ .shopping { display: grid; grid-template-columns: 1fr auto; } .shopping-list { background: white; padding: 3rem; border-radius: 1rem; width: 500px; margin: 4rem 0; } .shopping-list ul { list-style: none; margin: 0; padding: 0; } .shopping-item { padding: 1rem 0; display: grid; grid-template-columns: auto 1fr auto; grid-gap: 1rem; align-items: center; border-bottom: 1px solid var(--gray); } .shopping-item input[type="checkbox"] { margin-right: 1rem; } .shopping-item input[type="checkbox"]:checked + .itemName { opacity: 0.5; } .shopping-item button { padding: 0; font-size: 1rem; cursor: pointer; transition: transform 0.2s; } .shopping-item button:hover { transform: scale(1.4); } .shopping-list-item{ text-overflow: wrap; } li > input { margin: auto; padding-right: 10px; } li > .itemName{ display: inline-block; width: 80%; word-wrap: break-word; vertical-align: bottom; padding-left: 4px; text-transform: capitalize; } li > button { margin-left: auto; margin-top: 2px; margin-bottom: 0; margin-right: 0; width: 14.6%; } ul > hr { opacity: .2; }
Set up your local Server (Parcel)
So that we can run this site on a local server with our own local storage, we are going to use a tool called parcel from NPM. A prerequisite is that you need to have npm and node installed. Open up a terminal and cd
into your shopping directory.
You can verify that it is by running node -v
and npm -v
.
run npm i -g parcel-bundler
and mash ⮐
This will install it globally, in some cases you will need to run it with
sudo
first.
You can tell it installed by typing parcel --version
in terminal. At the time of writing this, the version is 1.12.4.
To run this, just run parcel index.html
and it will create a new live server for you to interact with, test, and show off when you are completed! It will look something like the image below:
In terminal you'll see something like:
Server running at http://localhost:1234
Open that localhost url in your chrome browser.
✨ Built in 2.69s.
Let's Build Some Javascript!
Javascript Variables & Selectors
Let's make some "sudo code" to understand what we have to do in this project:
//SELECTORS AND CONSTANTS INCLUDING AN ARRAY TO HOLD OUR STATE //HANDLE SUBMIT EVENT //LISTEN FOR WHEN A USER SUBMITS //PUSH ITEMS INTO OUR STATE //DISPLAY ITEMS //KEEP TRACK OF THE ITEMS TO SEE IF THEY ARE COMPLETE
What does "state" mean? It refers to a bunch of data that reflects a snapshot of the current "state" of the data. It will allow you to recreate a visual representation by the object, array, or data. An analogy: each section of this article would be a "state" of the full project.
Let's create an empty array called items
. Create a function that handles a submission from the form and an event that listens for the submit button.
//SELECTORS AND CONSTANTS INCLUDING AN ARRAY TO HOLD OUR STATE const shoppingForm = document.querySelector('.shopping'); const list = document.querySelector('.list'); let items = [];
//LISTEN FOR WHEN A USER SUBMITS
If you haven't read up on my DOM Manipulation or Events articles yet, now would be a good time because now we are going to select elements and create events on it.
We are going to listen on the shopingForm
's submit button and call a function haven't created just yet. Place this code at the bottom of your Javascript document:
//LISTEN FOR WHEN A USER SUBMITS shoppingForm.addEventListener('submit', handleSubmit);
//HANDLE SUBMIT EVENT (Function)
We want to prevent the default behavior of the submit button. I covered this in my Events article (link to the specific section) if you aren't familiar with it's use.
//HANDLE SUBMIT EVENT function handleSubmit(event){ event.preventDefault(); console.log('submitted!!') };
Now that we have a function that handles an event listener We need to pull the data out of that form submission event. Let's create a variable that contains the submit event's value and console log it.
//HANDLE SUBMIT EVENT function handleSubmit(event){ event.preventDefault(); console.log('submitted!'); const name = event.currentTarget.item.value; console.log(namedItem); };
We can't just store that namedItem
as a string and we need some more details. For example, we need to know if it's removed, completed, or the index number. Let's create a variable that has some objects in it.
function handleSubmit(event){ event.preventDefault(); console.log('submitted!'); const name = event.currentTarget.item.value; console.log(namedItem); const item = { name, // this could read name: name, id: Date.now(), complete: false, }; };
//PUSH ITEMS INTO OUR STATE
This is fairly simple to do with arrays and since my articles haven't covered arrays just yet, I'll just say that using the .push()
method allows you to push in a new value to the end of the array without having to know the index of the last item in the array. Currently, items
is just an empty array so the first push would make the index = 0, then the next push would have an index 1 and so on.
Bear in mind this is still inside the
handleSubmit()
function!
//PUSH ITEMS INTO OUR STATE items.push(item); console.log(`There are now ${items.length} in your state.`);
// CLEAR THE FORM
After you submit a new item on the form, you don't want the text to linger in the input field. There are two ways we will cover how to reset it.
You could take the name variable (event.currentTarget.item.value;
) and set it to ''
and that would clear it out but for education, let's see how else we could do it.
In forms, the form is the target. That sounds a little weird but if you remember bubbling (propagation) in the Event's article, events on the form such as submit will not bubble outside of the form. You can use currentTarget
or target
in this use case and it will still work 🙂
event.target.reset();
//DISPLAY ITEMS
One of the best ways to loop over an array and return the data you want is an array method called map()
. Here is an MDN article that can help you learn more about the various applications of the map method.
Why? Because if there is a simple array like:
const name = ['drew', 'audrey'];
We can use map to loop over the names in the array and then create a function (arrow function in this case) that outputs html by returning a "name" interpolation (${}
) -- remember you have to use back-ticks to make these work. Then we use join
on the end to put them back together as 1 html string. like this:
name.map(name => `<li>${name}</li>`.join('');
It would output: <li>drew</li><li>audrey</li>
, can you see how powerful and awesome this is yet? Let's apply this to the current shopping project.
//DISPLAY ITEMS function displayItems() { console.log(items); const html = items.map(item=> `<li> ${item.name} </li>`).join(''); // console.log(html); list.innerHTML = html; };
You need to display them after you submit them, so call the displayItems()
function after we clear the form. Bear in mind we are going to create some custom events later which will refactor this code. Let's look at where we are currently:
//SELECTORS AND CONSTANTS INCLUDING AN ARRAY TO HOLD OUR STATE const shoppingForm = document.querySelector('.shopping'); const list = document.querySelector('.list'); let items = []; //HANDLE SUBMIT EVENT function handleSubmit(event){ event.preventDefault(); console.log('submitted!'); const name = event.currentTarget.item.value; console.log(name); const item = { name, id: Date.now(), complete: false, }; //PUSH ITEMS INTO OUR STATE items.push(item); console.log(`There are now ${items.length} in your state.`); event.target.reset(); displayItems(); }; //DISPLAY ITEMS function displayItems() { console.log(items); const html = items.map(item=> `<li> ${item.name} </li>`).join(''); // console.log(html); list.innerHTML = html; }; //KEEP TRACK OF THE ITEMS TO SEE IF THEY ARE COMPLETE //LISTEN FOR WHEN A USER SUBMITS shoppingForm.addEventListener('submit', handleSubmit);
You should be able to add items to the form and submit, each new item will create another list item and push it into our items array.
We can put the list's html
variable into the HTML using innerHTML
and then we are going to add some classes to the elements we just created.
Remember we used back-ticks, so it can be a multiline string inside, let's add class="shopping-list-item"
to the html
variable so that our CSS will style it correctly. Then we need to add an input with a checkbox <input type="checkbox">
, a span for the item name with a class of "itemName". Lastly, we will add a button with ×
(makes an "X" or technically a multiplication sign).
For accessibility, users will just see a button with "multiplication" read to them with a screen reader - which doesn't make any sense. What we can do is add an
aria-label="Remove ${item.name}"
in the button's opening tag.
If you just press enter, you would be able to create new list items by just pressing enter, unfortunately they would be empty. To fix that, on line 14 of our HTML we can add required
preventing submission of an empty item. another thing we can do inside the handleSubmit()
function at the top is create a truthy statement that kills the whole function if it's empty. Here is all that code:
//SELECTORS AND CONSTANTS INCLUDING AN ARRAY TO HOLD OUR STATE
const shoppingForm = document.querySelector('.shopping');
const list = document.querySelector('.list');
let items = [];
//HANDLE SUBMIT EVENT
function handleSubmit(event){
event.preventDefault();
console.log('submitted!');
const name = event.currentTarget.item.value;
if (!name) return;
//***************** DON'T MISS THIS PART
console.log(name);
const item = {
name,
id: Date.now(),
complete: false,
};
//PUSH ITEMS INTO OUR STATE
items.push(item);
console.log(`There are now ${items.length} in your state.`);
event.target.reset();
displayItems();
};
//DISPLAY ITEMS
function displayItems() {
console.log(items);
const html = items.map(item=> `<li class="shopping-list-item">
<input type="checkbox">
<span class="itemName">${item.name}</span>
<button aria-label="Remove ${item.name}">×</button>
</li>`).join('');
// console.log(html);
list.innerHTML = html;
};
//KEEP TRACK OF THE ITEMS TO SEE IF THEY ARE COMPLETE
//LISTEN FOR WHEN A USER SUBMITS
shoppingForm.addEventListener('submit', handleSubmit);
Would this still work with the truthy if you typed in 0
, false
, null
, or undefined
in the input field? Would it trip that switch and kill the handler?
It' outputs a string, so the answer is no! It should still allow you to submit any input.
More About Display Items & Custom Events
This function will need to run many times during this application so an event would be more appropriate for this application.
Remove the display()
function from the handleSubmit function, we are about to replace it in the next paragraph.
To do custom events from the browser, or "Dispatch". This means "An event happens". Let's fire off the custom event from the class="list"
. It'll look like this: list.dispatchEvent(new CustomEvent('itemsUpdated'));
but nothing will happen since we aren't listening for itemsUpdated
.
//LISTEN FOR WHEN A USER SUBMITS shoppingForm.addEventListener('submit', handleSubmit); list.addEventListener('itemsUpdated', displayItems); list.addEventListener('itemsUpdated', event => { console.log(event); });
Let's test it!
You can remove the 3rd event listener above now that you have tested it.
Local Storage
Every page you've accessed in a browser has local storage, you can access it like this:
Let's create a function that will save items
array to local storage:
function mirrorToLocalStorage(){ console.info('Saving items to localStorage'); }
Now we can duplicate the eventListener we created to display items but instead of passing "displayItems" we pass "mirrorToLocalStorage".
function mirrorToLocalStorage(){
console.info('Saving items to localStorage');
}
//KEEP TRACK OF THE ITEMS TO SEE IF THEY ARE COMPLETE
//RENDER OUT A LIST OF ALL ITEMS CREATED
//LISTEN FOR WHEN A USER SUBMITS
shoppingForm.addEventListener('submit', handleSubmit);
list.addEventListener('itemsUpdated', displayItems);
//removed console.log event
list.addEventListener('itemsUpdated', mirrorToLocalStorage);
How Do We Access LocalStorage?
In the Javascript console of your browser, you can type in localStorage.setItem('name', 'drew');
allows you to add to local storage an array (key:value pair). We can then use localStorage.getItem('name');
(the key) and will result in a console output of "drew".
Let's apply that to this project.
function mirrorToLocalStorage(){ console.info('Saving items to localStorage'); localStorage.setItem('items', items); };
The above won't work. Local storage is Text only. It will try to convert the object to text first but will fail. You can use JSON.stringify()
to convert it into a string in order to store it in the local storage and you can use JSON.parse()
when you pull it out of local storage to put it back into an array.
function mirrorToLocalStorage(){ console.info('Saving items to localStorage'); localStorage.setItem('items', JSON.stringify(items)); };
Now we need to restore from local storage. Let's create a function for that.
function restoreFromLocalStorage(){ console.info('Restored from localStorage'); const lsItems = JSON.parse(localStorage.getItem('items')); if(lsItems.length) { //truthy to check if there is anything in the array lsItems.forEach(item => items.push(item)); list.dispatchEvent(new CustomEvent('itemsUpdated')); }; }; restoreFromLocalStorage()
Don't forget to call this function at the bottom of the document. Add a
restoreFromLocalStorage();
down there!
We can now use localStorageItem = items;
to update the items array on reload from local storage. Another way to do this is with the "spread" feature of arrays in Javascript like this: items.push(...localStorageItem);
or you could use a forEach loop.
function restoreFromLocalStorage(){ console.info('Restored from localStorage'); const localStorageItems = JSON.parse(localStorage.getItem('items')); if(localStorageItems.length) { //truthy to check if there is anything in the array items = localStorageItems; items.push(...localStorageItems); list.dispatchEvent(new CustomEvent('itemsUpdated')); }; };
Currently, when you create an item it will save to localStorage but it won't update localStorage's "completed" or "deleted" features you can interact with.
Remove Items From List
Let's create a selector to grab our buttons then listen for a click on the deletion button. First, let's look at the way you are probably thinking this will work and discuss why it won't.
Let's try to add a function at the bottom of the document followed by an event listener and see what happens..
function deleteItem(id){ console.log('DELETING ITEM'); }; buttons.forEach(button => button.addEventListener('click',deleteItem));
You can try to move that selector up and down the page, the issue is an event listener cannot listen to something that hasn't been created yet. So what do we actually do?
There is a tool we can use called "Event Delegation". Instead of listening for clicks on things that we need, we listen on the parent to see if the thing they clicked on is within.
Remember,
target
is the thing that you clicked on (in this case the list) andcurrentTarget
is the thing that you actually clicked (the remove button).
If you add a new item now, you won't have to create a new eventListener in order to delete it because we are delegating that to the list.
Create a function called deleteItem()
and console.log "Deleting Item" then create this event listener and test it out.
function deleteItem() { console.log("Deleting Item"); };
list.addEventListener('click', (event) => { if (event.target.matches('button')) { deleteItem(id); } });
matches
checks css selectors and compares them to the target.
When Someone Clicks On The X What Id Do We Pass It?
Remember that displayItems
function we created earlier? Well now we can pass in value="id"
on both the input and the button.
function displayItems() { console.log(items); const html = items.map(item=> `<li class="shopping-list-item"> <input type="checkbox" value="${item.id}"> <span class="itemName">${item.name}</span> <button aria-label="Remove ${item.name}" value="${item.id}">×</button> </li><hr>`).join(''); // console.log(html); list.innerHTML = html;
We can now pass that value into our our deleteItem()
function. We have to make sure the id is an number instead of a string to avoid issue with our filter later - It'll look like this:
We can use filter
array method to ignore the deleted item like this:
items = items.filter(item = > item.id !== id);
Now add that to your deleteItem()
function:
function deleteItem(id) { items = items.filter(item => item.id !== id); list.dispatchEvent(new CustomEvent('itemsUpdated')); };
Since the above id will be a string we will have to adjust our id in the event listener to be a number:
list.addEventListener('click', (event) => { const id = parseInt(event.target.value); if (event.target.matches('button')) { deleteItem(id); }; });
We have to now update our localStorage with the new items when we delete so chuck that itemsUpdated
custom event into the deleteItem()
function like this:
function deleteItem(id) { items = items.filter(item => item.id !== id); list.dispatchEvent(new CustomEvent('itemsUpdated')); };
//KEEP TRACK OF THE ITEMS TO SEE IF THEY ARE COMPLETE
Create a function called "markAsComplete and pass it that ID.
function markAsComplete(id){ console.log('Marking as complete', id); }
We need to create an event listener now or we can create another if statement in the delegated event listener we created in the last section.
if (event.target.matches('input[type="checkbox"]')) { markAsComplete(id); };
We need to find the item we want using find()
just like we did in deleteItem()
//KEEP TRACK OF THE ITEMS TO SEE IF THEY ARE COMPLETE functionmarkAsComplete(id){ console.log('Marking as complete', id); const itemReflected = items.find(item => item.id === id); console.log(itemReflected); }
Now we can add a toggle with a "bang", while we could do this with if statements - this is much easier. Set the value of complete
to the opposite value like this:
//KEEP TRACK OF THE ITEMS TO SEE IF THEY ARE COMPLETE functionmarkAsComplete(id){ console.log('Marking as complete', id); const itemReflected = items.find(item => item.id === id); console.log(itemReflected); itemReflected.complete = !itemReflected.complete; list.dispatchEvent(new CustomEvent('itemsUpdated')); }
If you look at the console you will see it's working but the check boxes need to be toggled to reflect the status. The way to resolve this is in the html
variable. We will do this with a turnery function that defaults to blank.
//DISPLAY ITEMS function displayItems() { console.log(items); const html = items.map(item=> `<li class="shopping-list-item"> <input type="checkbox" value="${item.id}" ${item.complete ? 'complete' : ''}> <span class="itemName">${item.name}</span> <button aria-label="Remove ${item.name}" value="${item.id}">×</button> </li><hr>`).join(''); // console.log(html); list.innerHTML = html; };
Finished Project
You can use a text compare tool to see if there is something you missed.
//SELECTORS AND CONSTANTS INCLUDING AN ARRAY TO HOLD OUR STATE const shoppingForm = document.querySelector('.shopping'); const list = document.querySelector('.list'); let items = []; //HANDLE SUBMIT EVENT function handleSubmit(event){ event.preventDefault(); console.log('submitted!'); const name = event.currentTarget.item.value; console.log(name); if (!name) return; let item = { name, id: Date.now(), complete: false, }; //PUSH ITEMS INTO OUR STATE items.push(item); console.log(`There are now ${items.length} in your state.`); event.currentTarget.reset(); //clears the form // displayItems(); list.dispatchEvent(new CustomEvent('itemsUpdated')); }; //DISPLAY ITEMS function displayItems() { // console.log(items); const html = items.map(item=> `<li class="shopping-list-item"> <input type="checkbox" value="${item.id}" ${item.complete ? 'checked' : ''}> <span class="itemName">${item.name}</span> <button aria-label="Remove ${item.name}" value="${item.id}">×</button> </li><hr>`).join(''); // console.log(html); list.innerHTML = html; }; function mirrorToLocalStorage(){ console.info('Saving items to localStorage'); localStorage.setItem('items', JSON.stringify(items)); }; function restoreFromLocalStorage(){ console.info('Restored from localStorage'); const lsItems = JSON.parse(localStorage.getItem('items')); if(lsItems.length) { //truthy to check if there is anything in the array lsItems.forEach(item => items.push(item)); list.dispatchEvent(new CustomEvent('itemsUpdated')); }; }; function deleteItem(id) { items = items.filter(item => item.id !== id); list.dispatchEvent(new CustomEvent('itemsUpdated')); }; //KEEP TRACK OF THE ITEMS TO SEE IF THEY ARE COMPLETE function markAsComplete(id){ console.log('Marking as complete', id); const itemReflected = items.find(item => item.id === id); console.log(itemReflected); itemReflected.complete = !itemReflected.complete; list.dispatchEvent(new CustomEvent('itemsUpdated')); } //RENDER OUT A LIST OF ALL ITEMS CREATED //LISTEN FOR WHEN A USER SUBMITS shoppingForm.addEventListener('submit', handleSubmit); list.addEventListener('itemsUpdated', displayItems); list.addEventListener('itemsUpdated', mirrorToLocalStorage); list.addEventListener('click', (event) => { const id = parseInt(event.target.value); if (event.target.matches('button')) { deleteItem(id); }; if (event.target.matches('input[type="checkbox"]')) { markAsComplete(id); }; }); restoreFromLocalStorage()
Final Thoughts
This was a tough project to complete! You got to see some custom event listeners, how to use a no-SQL database (local storage), how to manipulate arrays, and most importantly have a good time. I hope you enjoyed this as much as I did and look forward to the next project - Stay tuned!
If you found this article helpful, share/retweet it and follow me on twitter @codingwithdrewk! There is so much more in Wes' courses I think you will find valuable as I have. I'm learning so much and really enjoying the course, Wes has an amazingly simple way to explain the difficult bits of Javascript that other courses I've taken could only wish. You can view the course over at WesBos.com. (I am in no way getting any referrals or kickbacks for recommending this)
Drew is a seasoned DevOps Engineer with a rich background that spans multiple industries and technologies. With foundational training as a Nuclear Engineer in the US Navy, Drew brings a meticulous approach to operational efficiency and reliability. His expertise lies in cloud migration strategies, CI/CD automation, and Kubernetes orchestration. Known for a keen focus on facts and correctness, Drew is proficient in a range of programming languages including Bash and JavaScript. His diverse experiences, from serving in the military to working in the corporate world, have equipped him with a comprehensive worldview and a knack for creative problem-solving. Drew advocates for streamlined, fact-based approaches in both code and business, making him a reliable authority in the tech industry.