A grocery list web application

Muhammad Saqib Ilyas
23 min readDec 24, 2023

--

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 finished view of our application

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:

Our page after basic styling

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)
}
}

--

--

Muhammad Saqib Ilyas

A computer science teacher by profession. I love teaching and learning programming. I like to write about frontend development, and coding interview preparation