A grocery list web application
Let’s create a grocery list application using HTML, CSS, and JavaScript. The user will be able to add items to a grocery list, which will be displayed to the user, of course. The user may edit an item’s name, or delete it. The user may delete all items from the list with a click of a button. The data will be persistent, so that if the user closes the web browser and reopens it, the list is still there.
Here’s what the application will look like:
The starter HTML
Let’s start with the following HTML document:
<!DOCTYPE html>
<html>
<head>
<title>Grocery list</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="fontawesome/css/all.min.css">
</head>
<body>
<section class="centered">
<h3>Grocery list</h3>
<form class="grocery">
<div class="form-elements">
<input type="text" id="item" placeholder="Bread">
<button type="submit" class="btn-add" disabled>Add</button>
</div>
</form>
<div class="grocery-container">
<div class="grocery-list">
<article class="grocery-item">
<p class="item-title">Bananas</p>
<div class="btn-container">
<button class="btn-edit">
<i class="fas fa-edit"></i>
</button>
<button class="btn-delete">
<i class="fas fa-trash"></i>
</button>
</div>
</article>
</div>
<button type="button" class="btn-clear">Clear list</button>
</div>
</section>
<script src="app.js"></script>
</body>
</html>
In the head
section, I am linking to the Fontawesome stylesheet and our own stylesheet. In the body
section, we have a section
element enclosing everything. The first thing in the section
element is a heading followed by a form
element that has a text box input for the user to type in the grocery list item name, and a button to add the item to the list. Note that we use the placeholder
attribute to set a helper text for the user as a guide to what the text box is for. Also, the button is initially disabled since adding an item with no name doesn’t make sense. We’ll enable the button once the user types in an item name. We enclose the text box and the button inside a div
element for ease of styling.
Next up, we have a div
element with a class of grocery-container
that encloses the grocery list and the “Clear list” button. The grocery list itself is a div
element that has an article
for each grocery item. We show a sample grocery item markup in the above example. There’s a p
element with the item’s name, and a div
element with a class of btn-container
enclosing two buttons, which we render using Fontawesome icons.
At the end of the body
element, we link to our JavaScript file. Before we put anything in the JavaScript file, let’s apply some CSS styles so that the page’s outlook starts to improve.
The starter CSS
Let’s examine the global styles first:
*, ::before, ::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
background: #ccc;
}
We start with a CSS reset to get rid of differences due to browser-specific default stylesheets. We give the page a grey background color.
Next, let’s style the page so that the contents appear centred horizontally and a bit spaced from the top.
.centered {
margin: 2rem auto;
max-width: 600px;
min-height: 200px;
padding: 2rem;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.3);
}
.centered h3 {
text-align: center;
margin-bottom: 1.5rem;
}
We use the .centered
CSS class selector to style the top-level section
element. We use the margin
property to space the div
container 2rem
apart from the top and bottom while the left and right margins are adjusted accordingly. We set the maximum width to 600 pixels. We shouldn’t need more than that. We set the minimum height to 200 pixels. That way, the page wouldn’t look half bad even when it doesn’t have any items in the list. We set the padding
property to space the container’s contents comfortably away from the edges. We give this container a white background color, rounded edges, and a box shadow. We then select the h3
heading and configure it to center align the text. We also set the bottom margin so that the form
element is spaced away from the heading.
Next, let’s style the form
element.
.form-elements {
display: grid;
grid-template-columns: 2fr 1fr;
}
.form-elements input, .btn-submit {
padding: 8px;
}
#item {
border-radius: 8px 0 0 8px;
}
.btn-submit {
border: none;
border-radius: 0 8px 8px 0;
}
We control the element layout in the form using CSS grid. We configure it to split two-thirds of the width to the first column, i.e., the text box, and 1/3 to the button. We give a bit of padding to the form elements so that the text is comfortably separated from the control’s edges. We select the text box using the ID selector and set its top left and bottom left edges to rounded, while the others are kept straight. When we specify four values for the border-radius
property, the first one is the top left border radius, the second one top right, the third one bottom right, and the last one bottom left. Think of it as starting at the top left and then going clockwise around the text box. We do the opposite rounding on the button. This creates an effect where the text box and the button appear to be part of the same control group like a capsule. We also get rid of the border on the button.
Next, let’s style the grocery list.
.grocery-item {
width: 100%;
display: flex;
justify-content: space-between;
margin: 1rem auto;
}
.grocery-item p {
display: block;
width: 80%;
}
We set the grocery item to occupy all of its parent’s width. We use CSS Flexbox to layout its contents, i.e., the item name and the control buttons. We configure Flexbox to put the two items in the container at the opposite edges with the space-between
directive. We set the top and left margins so that the items are somewhat displaced from each other. Next, we select the item name element and set its width to occupy most of the parent’s width. But for that, we have to set the p
element’s display
property to block
.
Next, let’s style the edit and delete buttons.
.btn-delete {
color: rgba(255, 0, 0, 0.6);
margin-left: 10px;
cursor: pointer;
border: none;
}
.btn-edit {
color: rgba(0, 255, 0, 0.6);
cursor: pointer;
border: none;
}
For both buttons, we set an appropriate background color, set the cursor
to pointer
so that hovering over the button gives the user a visual clue as to the active nature of the element. We also get rid of the border around the Fontawesome icons. To space the two buttons horizontally, we set the left margin on the delete button.
With this styling, our page looks like the following:
Before we dive into JavaScript, let’s create some styles for a “toast” element. This is a div
element that we’ll display momentarily horizontally centred near the top edge of the page to indicate the success or failure of an action.
.toast {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
height: 2rem;
text-align: center;
width: 300px;
line-height: 2rem;
border-radius: 4px;
}
.success {
background-color: #198754;
color: rgba(255, 255, 255, 0.8)
}
.danger {
background-color: #d9534f;
color: rgba(255, 255, 255, 0.8)
}
We define a class named toast
. To be able to position the toast element independent of the page contents, we set its position
to absolute
. To specify its position, we set the top
property to 10 pixels to set the toast to display near the top edge of the parent element. We set the left
property to 50%
, followed by a translate transform in the horizontal (x) direction to centre the toast element horizontally. We give the toast element a matching height
and line-height
values so that the text is vertically centred. We give the toast element a width of 300 pixels. Finally, to give it rounded edges, we use the border-radius
property.
To display successful “item added” messages, we define a class of success
with a green background color. For “item deleted” messages, we define a class of danger
with a red background color. Both of these classes have a greyish text color.
Enabling the “Add” button
We need to enable the “Add” button once the user types in an item’s name. To do that, we’ll subscribe to the input
event of the text box.
const newItem = document.getElementById('item')
const submitButton = document.querySelector('.btn-add')
newItem.addEventListener('input', () => {
if (newItem.value === '') {
submitButton.disabled = true
}
else {
submitButton.disabled = false
}
})
We acquire objects corresponding to the text box and the “Add” button using the document.getElementById()
method. We use the addEventListener()
method to subscribe to the input
method using an anonymous function. Our function disables the “Add” button if the value in the text box is empty, or enables it otherwise.
Adding an item
Now, let’s write the code to add a new item to the list when the “Add” button is clicked.
const form = document.querySelector('.grocery')
const list = document.querySelector('.grocery-list')
form.addEventListener('submit', addItem)
function addItem(e) {
e.preventDefault()
const value = newItem.value
const article = createArticle(value)
list.appendChild(article)
displayMessage(`${value} successfully added to the list`, 'success')
newItem.value = ''
}
function createArticle(name) {
const article = document.createElement('article')
article.classList.add('grocery-item')
article.innerHTML = `<p class="item-title">${name}</p>
<div class="btn-container">
<button class="btn-edit">
<i class="fas fa-edit"></i>
</button>
<button class="btn-delete">
<i class="fas fa-trash"></i>
</button>
</div>`
return article
}
function displayMessage(message, level) {
const toast = document.createElement('div')
toast.classList.add('toast')
toast.innerText = message
toast.classList.add(level)
document.body.appendChild(toast)
setTimeout(() => {
document.body.removeChild(toast)
}, 2000)
}
We acquire objects corresponding to the form
element and the grocery list using the document.querySelector()
method. Eventually, we’d enable editing an existing item in the grocery list. To keep track of the state of the application, we declare a flag named editing
and set it initially to false
. We then subscribe to the form’s submit
event using a function named addItem()
. The thing about form
elements is that they submit
to the server. But we don’t have a server. So, the first thing we do in the submit
handler is to call preventDefault()
so that the form isn’t submitted to the server. Next, we acquire the value in the text box and store it in the variable named value
.
We call the function createArticle()
to create an article
element representing the item. In that function, we use the document.createElement()
method to create a div
element. We give it a CSS class of grocery-item
. We copy-paste the static HTML that we created for the grocery item, and turn it into a template literal string assigned to the innerHTML
property of the div
element. We replace the name of the item with the variable substitution ${value}
. Finally, we return the newly created div
element. Back in the addItem()
function, we append the new div
element to the grocery item list container element.
To indicate the status of the action, we call the displayMessage()
function which accepts a status message, and a severity level as arguments. In this case, we pass in a success message, and the level of success
. This function creates a div
element, sets its innerText
property to the message passed as argument, assigns it two classes, namely toast
, and the string passed as the severity level. We add this div
element to the page DOM. Since the toast
class is positioned absolute
, it is displayed prominently horizontally centred near the top of the screen. But we don’t want this message to be displayed permanently, so we use the setTimeout()
method to remove this div
element from the DOM after two seconds.
Delete event handler
Let’s enable the delete button and implement the delete item functionality. Here’s what we need to do:
- Write a click event handler for the delete button.
- In the click event handler, first we need to prompt the user if they are certain they want to delete the item.
- Next, in the event handler, we want to find the parent
article
element and remove it from the DOM.
Let’s begin.
function addItem(e) {
/* Code from earlier */
delButton.onclick = deleteItem
/* Code from earlier */
}
First, we modify the addItem()
event handler function so that we hook up a named function to the delete button. Let’s implement that function now. Recall that the markup for a shopping list item looks like the following:
<article class="grocery-item">
<p class="item-title">Bananas</p>
<div class="btn-container">
<button class="btn-edit">
<i class="fas fa-edit"></i>
</button>
<button class="btn-delete">
<i class="fas fa-trash"></i>
</button>
</div>
</article>
Our delete event handler is tied to the button
element. So, when we receive a click event on the button
element, we need to get the article
element, which is the parent of its parent element. Here’s the event handler:
async function deleteItem(e) {
const item = e.currentTarget.parentElement.parentElement
const value = item.querySelector('p').innerText
try {
const choice = await showModal(`Are you sure you want to delete ${value}`)
if (choice){
list.removeChild(item)
const value = item.querySelector('p').innerText
displayMessage(`Item ${value} removed.`, 'danger')
if (list.children.length === 0) {
itemContainer.classList.remove('show-groceries')
}
}
else {
}
}
catch(error) {
console.log(error)
}
}
Note that the button
element also has a child i
element. The click may originate from the i
element. In that case, the e.target
property would point to the i
element. However, since the i
element does not have a click event handler, that event will be bubbled up to its parent, the button
element, where it is caught and handled.
As discussed, we reach the parent of the button
element’s parent. We obtain the item’s name using the innerText
property. Now, we need to display a modal dialog to let the user confirm the delete. Since this is going to be an asynchronous operation — the user may click the buttons at a time of their chosing — we declare deleteItem()
as async
. Accordingly, we call showModal()
with the await
keyword. The showModal()
function will return a Promise
, which will be resolved with a true
if the user presses “Yes”, otherwise, it will be resolved with a false
. Our code will await
that resolution on this call to showModal()
.
If the user presses “Yes”, and a true
is returned, we remove the grocery item from the list. We then display a toast message to inform the user of the completion of the request. If the list has now become empty, we hide it from the display by removing the CSS class show-groceries
from it.
Now, let’s look at the showModal()
function.
function showModal(message) {
return new Promise( (resolve) => {
preModal()
const div = document.createElement('div')
div.classList.add('modal')
div.innerHTML = `<p>${message}</p><div class="btn-row"><button id="yes">Yes</button><button id="no">No</button></div>`
document.body.appendChild(div)
document.getElementById('yes').addEventListener('click', function() {
document.querySelector('.modal').remove()
postModal()
resolve(true)
})
document.getElementById('no').addEventListener('click', function() {
document.querySelector('.modal').remove()
postModal()
resolve(false)
})
})
}
function preModal() {
submitButton.disabled = true
newItem.disabled = true
const editButtons = document.querySelectorAll('.btn-edit')
const deleteButtons = document.querySelectorAll('.btn-delete')
editButtons.forEach( btn => btn.onclick = null)
deleteButtons.forEach( btn => btn.onclick = null)
const clearButton = document.querySelector('.btn-clear')
clearButton.disabled = true
}
function postModal() {
submitButton.disabled = false
newItem.disabled = false
const editButtons = document.querySelectorAll('.btn-edit')
const deleteButtons = document.querySelectorAll('.btn-delete')
editButtons.forEach( btn => {
btn.onclick = editItem
btn.style.cursor = 'pointer'
})
deleteButtons.forEach( btn => {
btn.onclick = deleteItem
btn.style.cursor = 'pointer'
})
const clearButton = document.querySelector('.btn-clear')
clearButton.disabled = false
}
The showModal()
function accepts the message to be displayed in the modal dialog. The entire function implementation is wrapped inside a return
statement. The function needs to return a Promise
. The argument to the Promise
constructor is a function reference named resolve
. We can call this function with an argument of true
if the user clicks the “Yes”, button or with false
otherwise. This will cause the waiting call to showModal()
to be resolved with a value of either true
or false
.
When a modal dialog is displayed, every other control on the page should be disabled. That’s what preModal()
achieves. First, we disable the “Add” button, and the new item name text box. Next, we locate all the “Edit” and “Delete” buttons and disable them by setting their onclick
properties to null
. For this, we use the forEach()
method. Finally, we locate and disable the “Clear” button.
We create a div
element with a p
element and two button
elements. We use a template literal string to assign the message
argument o the p
element. We give the “Yes” and “No” buttons unique IDs. We assign a CSS class of modal
to the div
element, and a class of btn-row
to the container enclosing the two button
elements. We define these CSS classes as:
.modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 30rem;
height: 5rem;
overflow: auto;
background-color: rgba(0,0,0,0.4);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.btn-row {
display: flex;
margin-top: 1rem;
}
.btn-row button{
margin-right: 1rem;
padding: 0.2rem 0.5rem;
}
We first use a class selector to select the outer div
element. We set its position
to fixed
so that it is displayed in a fixed place. To place it horizontally, and vertically centered on the page, we set its top
and left
positions to 50%
and then apply a transform of translation along the x and y directions by 50%. We assign a width and a height to the modal dialog. We assign auto
to the overflow
property so that if the message is too big, it is automatically handled. We specify the use of Flexbox to centre the contents of the modal dialog horizontally and vertically centred. Since we want the text and the buttons to be displaed on separate rows, we assign column
to the flex-direction
property.
We want the buttons to be shown in a row, so we set the display
property to flex
on the btn-row
class as well. This will have a default value of row
for the flex-direction
property, which achieves our goal. We give a bit of margin on the top of the row of buttons so that they are spaced away from the dialog text.
By default, the buttons will be just big enough for their content, so we apply a bit of padding to the button
elements that are successors of the btn-row
class. Also, the two buttons will be too close to each other, so we apply a bit of right margin.
Back to the JavaScript. We use the addEventListener()
method to hook up anonymous functions to the “Yes” and “No” buttons. In each event handler, we remove the modal dialog from the DOM, and call a postModal()
function to re-enable all the active elements on the page, which were disabled in the preModal()
function. We resolve(true)
in the “Yes” event handler, and resolve(false)
in the “No” event handler.
Edit event handler
When the user clicks the edit button, we’ll copy the respective item’s name to the “New item” text box, change the “Add” button to a “Save” button. and set focus to the text box. Once the user has edited the item’s name, we’ll save the changes to the respective grocery list item. Here’s the code:
let editing
let elementEditing
function editItem(e) {
preModal()
newItem.disabled = false
submitButton.disabled = false
elementEditing = e.currentTarget.parentElement.parentElement
const pElement = elementEditing.querySelector('p')
newItem.value = pElement.innerText
newItem.focus()
submitButton.innerText = 'Save'
editing = true
}
When editing an item, we want to disable all the clickable elements, except for the text box and the “Save” button. We have already written the code to disable and enable these elements, except the existing code disables the text box, too. No worries, we just call that preModal()
function, and enable the text box and the button afterwards. The e
argument represents the button
element for the grocery list item that was clicked. We acquire an object representing the corresponding article
element and store it in a global variable named elementEditing
. This variable will come handy in the “Save” button event handler. We also acquire an object representing the p
element descendant of this article
element. We copy the name of the selected item to the text box, set focus on the text box and set focus to it. We change the button text from “Add” to “Save”. We set a global flag variable editing
to true
to indicate to the “Save” event handler that we are editing an item, and not adding a new item. The distinction is necessary since we use the same form for both actions.
We already have a form submit event handler registered. We repurpose it to handle “Save” event as well as the “Add” event.
function addItem(e) {
e.preventDefault()
const value = newItem.value
if (editing) {
const pElement = elementEditing.querySelector('p')
pElement.innerText = newItem.value
postModal()
submitButton.innerText = 'Add'
newItem.value = ''
editing = false
}
else {
const id = new Date().getTime().toString()
const article = createArticle(value)
list.appendChild(article)
const delButton = article.querySelector('.btn-delete')
const editButton = article.querySelector('.btn-edit')
delButton.onclick = deleteItem
editButton.onclick = editItem
displayMessage(`${value} successfully added to the list`, 'success')
}
if (list.children.length > 0) {
itemContainer.classList.add('show-groceries')
}
newItem.value = ''
}
We put all of the “Add” code inside an else
clause and below, while we add an if
statement that checks if the editing
flag is true
. If so, we acquire an object representing the p
element descendant of the article
being edited. We set its innerText
to the value from the text box. We call postModal()
to enable all the active elements that we disabled. We change the button text back to “Add”, set the text box blank, and reset the editing
flag to false
so that the form is ready to add a new item.
Saving the list
Our application is fully functional, except that the list that you create would be available only as long as your browser tab is open. Let’s provide persistence. There are two client-side persistence libraries available. One is session storage, and the other is local storage. The session storage retains key-value pairs as long as your session is intact. If the page is closed and opened in a new browser window or tab, the session storage is lost. The local storage on the other hand is persistent even if you restart your computer. For our purpose, local storage seems appropriate.
It is convenient to store one key for the application and under that key’s value store the grocery list as a collection. It could be an array or a dictionary. A dictionary has efficient lookups and updates, so we’ll go with that.
OK, so our main data structure will be a dictionary. The values in the dictionary will be the grocery list item names. But what would the keys be? Each key needs to be unique. If we use the same key name for two or more grocery items, our data could be overwritten.
A good easy option for a unique ID would be the current time. Whenever we add an item, we’ll get the current time and use it as the key, and the item’s name as the value.
Let’s begin!
const appKeyName = 'MyGroceryApp'
init()
function init() {
const dict = localStorage.getItem(appKeyName)
if (!dict) {
localStorage.setItem(appKeyName, JSON.stringify({}))
}
}
Near the top of our JavaScript, after the other const
declarations, we insert the above code. We declare a string to hold our application’s name, which we’ll use as a key for the local storage. We declare a function named init()
since it is for initialization, and call it as soon as the const
variables have been initialized. The init()
function retrieves the key with our application’s name from the local storage and stores it in a variable named dict
. Of course, that key wouldn’t exist at the moment. When we call the getItem()
method and the requested item doesn’t exist, a null
is returned. We check if dict
is null
, and if so, we store the JSON representation of an empty dictionary in the local storage, under our application’s name as the key.
Now, let’s change the “Add item” functionality:
function addItem(e) {
e.preventDefault()
const value = newItem.value
let dict = JSON.parse(localStorage.getItem(appKeyName))
if (editing) {
/* Same code as before */
}
else {
const id = new Date().getTime().toString()
const article = createArticle(value, id)
list.appendChild(article)
const delButton = article.querySelector('.btn-delete')
const editButton = article.querySelector('.btn-edit')
delButton.onclick = deleteItem
editButton.onclick = editItem
dict[id] = value
localStorage.setItem(appKeyName, JSON.stringify(dict))
displayMessage(`${value} successfully added to the list`, 'success')
}
if (list.children.length > 0) {
itemContainer.classList.add('show-groceries')
}
newItem.value = ''
}
function createArticle(name, id) {
const article = document.createElement('article')
article.setAttribute('data-id', id)
/* All other code from earlier */
}
In the addItem()
function, we acquire the dictionary corresponding to our application from local storage. At this time, we can be sure that it wouldn’t be null
since our init()
function created it. In the else
clause, where we are adding a new item, we acquire the representation of the current time as a string and store it in the variable named id
. We store the new item’s name as a value under id
as the key in the statement data[id] = value
. Then, we store the dictionary back into local storage, overwriting its previous empty value. Note that we converted the dictionary to a JSON string before storing it in local storage.
One other change we make at this time is in the createArticle()
function that creates the markup for the grocery item. We set a data-id
attribute on the article
element and set it equal to the id
that we generated based on the current time. This will come in handy when we edit an item’s name.
Now, let’s write the code to update an existing item’s name when the user edits it:
function addItem(e) {
e.preventDefault()
const value = newItem.value
let dict = JSON.parse(localStorage.getItem(appKeyName))
if (editing) {
const id = elementEditing.getAttribute('data-id')
const pElement = elementEditing.querySelector('p')
pElement.innerText = newItem.value
dict[id] = newItem.value
localStorage.setItem(appKeyName, JSON.stringify(dict))
postModal()
submitButton.innerText = 'Add'
newItem.value = ''
editing = false
}
else {
/* Same as earlier */
}
if (list.children.length > 0) {
itemContainer.classList.add('show-groceries')
}
newItem.value = ''
}
When editing an item in the dictionary, we will need its key, which was the current time when the item was added to the grocery list. We stored this in the data-id
attribute of the article
element. We easily acquire it in the if
clause of the addItem()
function. We update the item in the dictionary using dict[id] = newItem.value
. Then, as we did when adding a new item, we convert the dictionary into a JSON string and store it in local storage using the setItem()
method.
Fantastic! Now, we can add items, and edit items, all in sync with local storage. Delete item remains to be synchronized with local storage, though. We need to spot where we removed the article
element in response to the delete button click, and remove the item from local storage as well. You’ll find this in the deleteItem()
function.
async function deleteItem(e) {
/* Code from earlier */
if (choice){
list.removeChild(item)
const id = item.getAttribute('data-id')
const data = JSON.parse(localStorage.getItem(appKeyName))
delete data[id]
localStorage.setItem(appKeyName, JSON.stringify(data))
/* Code from earlier */
}
We have access to the article
corresponding to the item being deleted in the item
variable. So we obtain the value of the data-id
attribute from it. We retrieve the dictionary from local storage and parse it into a JSON object. We delete the key corresponding to the item being deleted, and write the dictionary back into local storage.
Auto displaying the stored list
Wait a minute! Didn’t we say that the local storage presists across tab and browser closes? So, why don’t we auto display a saved grocery list from earlier? We can piggyback this functionality on the init()
function we declared earlier.
function init() {
const dict = localStorage.getItem(appKeyName)
if (!dict) {
localStorage.setItem(appKeyName, JSON.stringify({}))
}
else {
const data = JSON.parse(dict)
for (const [key, value] of Object.entries(data)) {
const markup = createArticle(value, key)
const delButton = markup.querySelector('.btn-delete')
const editButton = markup.querySelector('.btn-edit')
delButton.onclick = deleteItem
editButton.onclick = editItem
list.appendChild(markup)
}
if (Object.keys(data).length > 0) {
itemContainer.classList.add('show-groceries')
}
}
}
The else
part corresponds to the case when the key for our application already exists in the local storage, which means that there is some data stored in local storage. We parse the dictionary into a JSON object and iterate over its keys and values. We want to convert each key-value pair into an article
element. We already have a function (createArticle()
) that does that, so we just call it with the right arguments. What that function doesn’t do is hook up the event handlers to the edit and delete buttons. So, we do that, here. Then, we append the article
element to the grocery list. If the dictionary has non-zero elements and apply the show-groceries
class to the grocery list. And that’s that!
The clear button
Having deleted an individual item, deleting all items shouldn’t be hard.
const clearButton = document.querySelector('.btn-clear')
clearButton.onclick = clear
async function clear() {
try {
const choice = await showModal('Are you sure you want to delete all items?')
if (choice){
localStorage.setItem(appKeyName, JSON.stringify({}))
list.innerHTML = ''
displayMessage('All items removed from list.', 'danger')
itemContainer.classList.remove('show-groceries')
}
else {
}
}
catch(error) {
console.log(error)
}
}
None of this should need an explanation by now. Rather than delete individual items from local storage and children from the grocery list container DOM element, we just inserted a new empty dictionary into local storage and set the list’s innerHTML
property to an empty string.
That’s all folks!
That concludes our Grocery list application. You may download the entire code from this repository. If you want to copy-paste, here is the HTML:
<!DOCTYPE html>
<html>
<head>
<title>Grocery list</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="fontawesome/css/all.min.css">
</head>
<body>
<section class="centered">
<h3>Grocery list</h3>
<form class="grocery">
<div class="form-elements">
<input type="text" id="item" placeholder="Bread">
<button type="submit" class="btn-add" disabled>Add</button>
</div>
</form>
<div class="grocery-container">
<div class="grocery-list">
<!-- <article class="grocery-item">
<p class="item-title">Bananas</p>
<div class="btn-container">
<button class="btn-edit">
<i class="fas fa-edit"></i>
</button>
<button class="btn-delete">
<i class="fas fa-trash"></i>
</button>
</div>
</article> -->
</div>
<button type="button" class="btn-clear">Clear list</button>
</div>
</section>
<script src="app.js"></script>
</body>
</html>
Here’s the CSS:
*, ::before, ::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
background: #ccc;
}
.centered {
margin: 2rem auto;
max-width: 600px;
min-height: 200px;
padding: 2rem;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.3);
}
.centered h3 {
text-align: center;
margin-bottom: 1.5rem;
}
.form-elements {
display: grid;
grid-template-columns: 2fr 1fr;
}
.form-elements input, .btn-submit {
padding: 8px;
}
#item {
border-radius: 8px 0 0 8px;
}
.btn-submit {
border: none;
border-radius: 0 8px 8px 0;
}
.grocery-item {
width: 100%;
display: flex;
justify-content: space-between;
margin: 1rem auto;
}
.grocery-item p {
display: block;
width: 80%;
}
.btn-delete {
color: rgba(255, 0, 0, 0.6);
margin-left: 10px;
cursor: pointer;
border: none;
}
.btn-edit {
color: rgba(0, 255, 0, 0.6);
cursor: pointer;
border: none;
}
.success {
background-color: #198754;
color: rgba(255, 255, 255, 0.8)
}
.danger {
background-color: #d9534f;
color: rgba(255, 255, 255, 0.8)
}
.toast {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
height: 2rem;
text-align: center;
width: 300px;
line-height: 2rem;
border-radius: 4px;
}
.grocery-container {
visibility: hidden;
}
.show-groceries {
visibility: visible;
}
.modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 30rem;
height: 5rem;
overflow: auto;
background-color: rgba(0,0,0,0.4);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.btn-row {
display: flex;
margin-top: 1rem;
}
.btn-row button{
margin-right: 1rem;
padding: 0.2rem 0.5rem;
}
.btn-clear {
padding: 0.3rem;
}
Here’s the JavaScript:
const form = document.querySelector('.grocery')
const newItem = document.getElementById('item')
const submitButton = document.querySelector('.btn-add')
const itemContainer = document.querySelector('.grocery-container')
const list = document.querySelector('.grocery-list')
let elementEditing;
const appKeyName = 'MyGroceryApp'
const clearButton = document.querySelector('.btn-clear')
let editing = false
form.addEventListener('submit', addItem)
clearButton.onclick = clear
init()
function init() {
const dict = localStorage.getItem(appKeyName)
if (!dict) {
localStorage.setItem(appKeyName, JSON.stringify({}))
}
else {
const data = JSON.parse(dict)
for (const [key, value] of Object.entries(data)) {
const markup = createArticle(value, key)
const delButton = markup.querySelector('.btn-delete')
const editButton = markup.querySelector('.btn-edit')
delButton.onclick = deleteItem
editButton.onclick = editItem
list.appendChild(markup)
}
if (Object.keys(data).length > 0) {
itemContainer.classList.add('show-groceries')
}
}
}
newItem.addEventListener('input', () => {
if (newItem.value === '') {
submitButton.disabled = true
}
else {
submitButton.disabled = false
}
})
function addItem(e) {
e.preventDefault()
const value = newItem.value
let dict = JSON.parse(localStorage.getItem(appKeyName))
if (editing) {
const id = elementEditing.getAttribute('data-id')
const pElement = elementEditing.querySelector('p')
pElement.innerText = newItem.value
dict[id] = newItem.value
localStorage.setItem(appKeyName, JSON.stringify(dict))
postModal()
submitButton.innerText = 'Add'
newItem.value = ''
editing = false
}
else {
const id = new Date().getTime().toString()
const article = createArticle(value, id)
list.appendChild(article)
const delButton = article.querySelector('.btn-delete')
const editButton = article.querySelector('.btn-edit')
delButton.onclick = deleteItem
editButton.onclick = editItem
dict[id] = value
localStorage.setItem(appKeyName, JSON.stringify(dict))
displayMessage(`${value} successfully added to the list`, 'success')
}
if (list.children.length > 0) {
itemContainer.classList.add('show-groceries')
}
newItem.value = ''
}
function createArticle(name, id) {
const article = document.createElement('article')
article.setAttribute('data-id', id)
article.classList.add('grocery-item')
article.innerHTML = `<p class="item-title">${name}</p>
<div class="btn-container">
<button class="btn-edit">
<i class="fas fa-edit"></i>
</button>
<button class="btn-delete">
<i class="fas fa-trash"></i>
</button>
</div>`
return article
}
function displayMessage(message, level) {
const toast = document.createElement('div')
toast.classList.add('toast')
toast.innerText = message
toast.classList.add(level)
document.body.appendChild(toast)
setTimeout(() => {
document.body.removeChild(toast)
}, 2000)
}
async function deleteItem(e) {
const item = e.currentTarget.parentElement.parentElement
const value = item.querySelector('p').innerText
try {
const choice = await showModal(`Are you sure you want to delete ${value}`)
if (choice){
list.removeChild(item)
const id = item.getAttribute('data-id')
const data = JSON.parse(localStorage.getItem(appKeyName))
delete data[id]
localStorage.setItem(appKeyName, JSON.stringify(data))
const value = item.querySelector('p').innerText
displayMessage(`Item ${value} removed.`, 'danger')
if (list.children.length === 0) {
itemContainer.classList.remove('show-groceries')
}
}
else {
}
}
catch(error) {
console.log(error)
}
}
function editItem(e) {
preModal()
elementEditing = e.currentTarget.parentElement.parentElement
newItem.disabled = false
submitButton.disabled = false
const pElement = elementEditing.querySelector('p')
newItem.value = pElement.innerText
newItem.focus()
submitButton.innerText = 'Save'
editing = true
}
function showModal(message) {
return new Promise( (resolve) => {
preModal()
const div = document.createElement('div')
div.classList.add('modal')
div.innerHTML = `<p>${message}</p><div class="btn-row"><button id="yes">Yes</button><button id="no">No</button></div>`
document.body.appendChild(div)
document.getElementById('yes').addEventListener('click', function() {
document.querySelector('.modal').remove()
postModal()
resolve(true)
})
document.getElementById('no').addEventListener('click', function() {
document.querySelector('.modal').remove()
postModal()
resolve(false)
})
})
}
function preModal() {
submitButton.disabled = true
newItem.disabled = true
const editButtons = document.querySelectorAll('.btn-edit')
const deleteButtons = document.querySelectorAll('.btn-delete')
editButtons.forEach( btn => {
btn.onclick = null
btn.style.cursor = 'auto'
})
deleteButtons.forEach( btn => {
btn.onclick = null
btn.style.cursor = 'auto'
})
const clearButton = document.querySelector('.btn-clear')
clearButton.disabled = true
}
function postModal() {
submitButton.disabled = false
newItem.disabled = false
const editButtons = document.querySelectorAll('.btn-edit')
const deleteButtons = document.querySelectorAll('.btn-delete')
editButtons.forEach( btn => {
btn.onclick = editItem
btn.style.cursor = 'pointer'
})
deleteButtons.forEach( btn => {
btn.onclick = deleteItem
btn.style.cursor = 'pointer'
})
const clearButton = document.querySelector('.btn-clear')
clearButton.disabled = false
}
async function clear() {
try {
const choice = await showModal('Are you sure you want to delete all items?')
if (choice){
localStorage.setItem(appKeyName, JSON.stringify({}))
list.innerHTML = ''
displayMessage('All items removed from list.', 'danger')
itemContainer.classList.remove('show-groceries')
}
else {
}
}
catch(error) {
console.log(error)
}
}