A grocery list app — IndexedDB edition

Muhammad Saqib Ilyas
24 min readDec 31, 2023

--

Earlier, I wrote an article on how to create a grocery list application in HTML, CSS, and JavaScript. In that article, I used local storage. Another type of client-side storage is IndexedDB. In this article, I’ll show you how to create the same application using IndexedDB.

About the application

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:

A preview 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 styling has been applied

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.

Initializing IndexedDB storage

First, we need to configure our data store in IndexedDB. Here’s what we need to do:

const dbName = 'MyGroceryApp'
const dbVersion = 3
const osName = 'groceries'
let db;

init()

function init() {
const request = indexedDB.open(dbName, dbVersion);
request.onerror = (event) => {
console.error(`Database error: ${event.target.errorCode}`);
};
request.onsuccess = (event) => {
db = event.target.result;
};

request.onupgradeneeded = (event) => {
db = event.target.result;
db.createObjectStore(osName, { autoIncrement: true });
};
}

We need to give our database a name, and a version number. We also need to have a name for an object store inside that database to store the actual data. We define a string variable dbName to hold the database name. We define another variable to store the database version. We define a string variable to hold the name of the object store inside our database. We define a function named init() and call it. Since our script tag is near the end of the body tag, this function will be called once the DOM has been constructed.

In the init() function, we first call the open() method on the indexedDB object, passing it the database name and version number. We store the result in a variable named request. There are three possible outcomes to this request. First, if our request to open the database succeeds, the callback stored in the onsuccess handler is called. Second, if an error occurs, an onerror callback is called. Finally, if an upgrade is needed, an onupgradeneeded handler is called. This handler is also called if the database didn’t exist and is being created. In this handler, we need to create our object store and store any initial data in it, depending on our application. In our case, there’s no initial data to store. Let’s discuss each of our event handlers one by one.

In the onerror handler, we are simply displaying the error message in the console. Later on, once we’ve defined a helper function to display toast messages, we can use that, instead. In the onsuccess event handler, we store the database object through the event argument into a global variable named db. In the onupgradeneeded handler, we create our object store using the createObjectStore() method of the db object. It takes two arguments. The first is the name of the object store for which we use the variable osName that we declared earlier. The second argument is about the key in the object store. We can either pass the name of an object attribute that should be treated as a key, or, as we are doing, ask IndexedDB to create an auto-increment key itself.

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
storeItem(value)
.then( key => {
const article = createArticle(value, key)
list.appendChild(article)
displayMessage(`${value} successfully added to the list`, 'success')
if (list.children.length > 0) {
itemContainer.classList.add('show-groceries')
}
newItem.value = ''
})
.catch( error => {
console.log(error)
})
}

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>`
const delButton = article.querySelector('.btn-delete')
const editButton = article.querySelector('.btn-edit')
delButton.onclick = deleteItem
editButton.onclick = editItem
return article
}

function storeItem(value) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([osName], "readwrite");
const objectStore = transaction.objectStore(osName);

const addRequest = objectStore.add(value);

addRequest.onsuccess = function(event) {
const key = event.target.result;
resolve(key)
};

addRequest.onerror = function(event) {
reject(`Error storing data: ${event.target.error.message}`)
}

transaction.oncomplete = function(event) {
console.log("Transaction completed.");
};
})
}

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.

Next, we obtain the name typed in by the user from the newItem object that we tied up to the text box. We, then, call a function named storeItem() and pass it the value typed by the user. The function storeItem() returns a Promise. It creates a new transaction on the IndexedDB object store, specifying the name of the object store, and the transaction mode. There are two modes readonly and readwrite. We use readwrite since we want to be able to write data to the object store. We obtain an object representing the store from the transaction object. We then create a write request to save the new item to the object store using the add() method. Similar to the open() method, the add() method also has three event handlers onsuccess, onerror and oncomplete. The onsuccess handler signals the success of the request that we created. The onerror handler signals that an error occurred. The oncomplete handler indicates the transaction (not just the request) has completed.

In the onsuccess callback, we obtain the key that IndexedDB auto-generated for our new value, and resolve the promise with that key. In case of an error, we reject the promise with the error message. On completion of the transaction, we display a success message in the JavaScript console.

In the addItem() function, we have the .then() and .catch() blocks to handle the resolve and reject parts of the promise, respectively. In the .then() part, the item addition to the IndexedDB object store has been successful, but the item hasn’t been added to the page DOM. To do that, we call a createArticle() function, which uses the list item markup that we had in the HTML template, and inserts the item’s name, and the key from the IndexedDB transaction as the data-id attribute. We also query for the edit and delete buttons and hook up event handlers to them. We’ll define these functions later. Finally, we return the top-level DOM element that we created to addItem() which appends it to the grocery list DOM 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.

We still wouldn’t be able to see the grocery list item, because the visibility is set to hidden on the grocery-container. To make the item visible, we check the length of the grocery list and add the show-groceries class to the list container, so that list items are displayed. Finally, we set the text box value to an empty string, so that the user can enter another item.

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.
  • We also want to remove the item using its key from the IndexedDB object store

Let’s begin. 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 id = item.getAttribute('data-id')
removeItem(id)
.then( result => {
displayMessage(`Item ${value} removed.`, 'danger')
if (list.children.length === 0) {
itemContainer.classList.remove('show-groceries')
}
})
.catch(error => {
displayMessage(error, 'danger')
})
}
}
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 obtain the item’s data-id attribute so that we can use that as key to remove the item from the IndexedDB object store. We call a removeItem() function with that key as argument. That function is defined as:

function removeItem(key) {
return new Promise ( (resolve, reject) => {
const request = db
.transaction([osName], "readwrite")
.objectStore(osName)
.delete(parseInt(key));
request.onsuccess = (event) => {
resolve('Item deleted')
};
request.onerror = (event) => {
reject(event.target.error.message)
}
})
}

Since this is an asynchronous operation, we rely on returning a Promise. As before, we create a readwrite transaction on the database with a request using the delete() method to remove the item with index equal to the value of key. We have the familiar onsuccess and onerror handlers that either resolve and reject the promise, respectively.

Back in the calling function, if the Promise is resolved, we display a success message with a class of danger. If there are no longer any items in the list, we remove the show-groceries class from the container. In case the Promise was rejected, we display an error message. 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()
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
}

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 id = elementEditing.getAttribute('data-id')
const pElement = elementEditing.querySelector('p')
pElement.innerText = newItem.value
updateItem(id, newItem.value)
.then( result => {
displayMessage('Item successfully updated', 'success')
if (list.children.length > 0) {
itemContainer.classList.add('show-groceries')
}
newItem.value = ''
})
.catch( error => {
displayMessage('Item could not be updated', 'danger')
})
postModal()
submitButton.innerText = 'Add'
newItem.value = ''
editing = false
}
else {
storeItem(value)
.then( key => {
const article = createArticle(value, key)
list.appendChild(article)
displayMessage(`${value} successfully added to the list`, 'success')
if (list.children.length > 0) {
itemContainer.classList.add('show-groceries')
}
newItem.value = ''
})
.catch( error => {
console.log(error)
})
}
}

function updateItem(id, newValue) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([osName], "readwrite");
const objectStore = transaction.objectStore(osName);
const request = objectStore.get(parseInt(id));
request.onerror = (event) => {
reject(event.target.error.message)
};
request.onsuccess = (event) => {
const requestUpdate = objectStore.put(newValue, parseInt(id));
requestUpdate.onerror = (event) => {
reject(event.target.error.message)
};
requestUpdate.onsuccess = (event) => {
resolve('Item updated')
};
};
})
}

In the addItem() function, the else part has the code that we wrote earlier to add an item. We add an if statement to check if we are in edit mode. If so, we acquire the data-id attribute so that we can use when communicating with IndexedDB to edit the stored value against that key. 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 a function named updateItem() that accepts the key and the new value for it as arguments. In it, we use the get() method to read in the current value against the given key. Technically, that isn’t really needed in our application, but is the usual flow with IndexedDB in case we are storing more complex objects rather than simple strings. We use the put() method to update the value against the key received as argument. The rest of the details should be familiar by now.

Back in the addItem() function, 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.

The clear all button

Finally, let’s implement the “Clear” button that removes all the items from the DOM and the IndexedDB store.

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){
clearStore()
.then( result => {
list.innerHTML = ''
displayMessage('All items removed from list.', 'danger')
itemContainer.classList.remove('show-groceries')
})
.catch( error => {
displayMessage(error, 'danger')
})
}
}
catch(error) {
console.log(error)
}
}

function clearStore(key) {
return new Promise ( (resolve, reject) => {
const request = db
.transaction([osName], "readwrite")
.objectStore(osName)
.clear();
request.onsuccess = (event) => {
resolve('All items deleted')
};
request.onerror = (event) => {
reject(event.target.error.message)
}
})
}

This should all be familiar by now. Except in the clearStore() function, we are using the .clear() method of the object store.

That’s all folks!

That concludes our project. You may get the source code from this github repository. If you’d like to copy-paste, here’s 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">

</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 dbName = 'MyGroceryApp'
const dbVersion = 3
const osName = 'groceries'
const clearButton = document.querySelector('.btn-clear')

let editing = false

let db;

form.addEventListener('submit', addItem)

clearButton.onclick = clear

init()

function displayList() {
const tx = db.transaction(osName, "readonly");
const store = tx.objectStore(osName)

store.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const article = createArticle(cursor.value, cursor.key)
list.appendChild(article)
cursor.continue();
} else {
console.log("No more entries!");
if (list.children.length > 0) {
itemContainer.classList.add('show-groceries')
}
}
};
}

function init() {
const request = indexedDB.open(dbName, dbVersion);

request.onerror = (event) => {
displayMessage(`Database error: ${event.target.errorCode}`), 'danger';
};
request.onsuccess = (event) => {
db = event.target.result;
displayList()
};

request.onupgradeneeded = (event) => {
db = event.target.result;
db.createObjectStore(osName, { autoIncrement: true });
};
}

newItem.addEventListener('input', () => {
if (newItem.value === '') {
submitButton.disabled = true
}
else {
submitButton.disabled = false
}
})

function storeItem(value) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([osName], "readwrite");
const objectStore = transaction.objectStore(osName);

const addRequest = objectStore.add(value);

addRequest.onsuccess = function(event) {
const key = event.target.result;
resolve(key)
};

addRequest.onerror = function(event) {
reject(`Error storing data: ${event.target.error.message}`)
}

transaction.oncomplete = function(event) {
console.log("Transaction completed.");
};
})
}

function updateItem(id, newValue) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([osName], "readwrite");
const objectStore = transaction.objectStore(osName);
const request = objectStore.get(parseInt(id));
request.onerror = (event) => {
reject(event.target.error.message)
};
request.onsuccess = (event) => {
const requestUpdate = objectStore.put(newValue, parseInt(id));
requestUpdate.onerror = (event) => {
reject(event.target.error.message)
};
requestUpdate.onsuccess = (event) => {
resolve('Item updated')
};
};
})
}

function removeItem(key) {
return new Promise ( (resolve, reject) => {
const request = db
.transaction([osName], "readwrite")
.objectStore(osName)
.delete(parseInt(key));
request.onsuccess = (event) => {
resolve('Item deleted')
};
request.onerror = (event) => {
reject(event.target.error.message)
}
})
}

function clearStore(key) {
return new Promise ( (resolve, reject) => {
const request = db
.transaction([osName], "readwrite")
.objectStore(osName)
.clear();
request.onsuccess = (event) => {
resolve('All items deleted')
};
request.onerror = (event) => {
reject(event.target.error.message)
}
})
}

function addItem(e) {
e.preventDefault()
const value = newItem.value
if (editing) {
const id = elementEditing.getAttribute('data-id')
const pElement = elementEditing.querySelector('p')
pElement.innerText = newItem.value
updateItem(id, newItem.value)
.then( result => {
displayMessage('Item successfully updated', 'success')
if (list.children.length > 0) {
itemContainer.classList.add('show-groceries')
}
newItem.value = ''
})
.catch( error => {
displayMessage('Item could not be updated', 'danger')
})
postModal()
submitButton.innerText = 'Add'
newItem.value = ''
editing = false
}
else {
storeItem(value)
.then( key => {
const article = createArticle(value, key)
list.appendChild(article)
displayMessage(`${value} successfully added to the list`, 'success')
if (list.children.length > 0) {
itemContainer.classList.add('show-groceries')
}
newItem.value = ''
})
.catch( error => {
console.log(error)
})
}
}

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>`
const delButton = article.querySelector('.btn-delete')
const editButton = article.querySelector('.btn-edit')
delButton.onclick = deleteItem
editButton.onclick = editItem
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')
removeItem(id)
.then( result => {
displayMessage(`Item ${value} removed.`, 'danger')
if (list.children.length === 0) {
itemContainer.classList.remove('show-groceries')
}
})
.catch(error => {
displayMessage(error, 'danger')
})
}
}
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){
clearStore()
.then( result => {
list.innerHTML = ''
displayMessage('All items removed from list.', 'danger')
itemContainer.classList.remove('show-groceries')
})
.catch( error => {
displayMessage(error, 'danger')
})
}
}
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