Using dexie

Using dexie.js

Using the dexie.js wrapper for IndexedDB

IndexedDB

Although localStorage may be good enough for storing the data locally for small amounts of data, a far more powerful form of storing data locally is to use IndexedDB. This can store a lot more data than localStorage and can store more complex data than just strings.

Dexie.js

IndexedDB is more powerful than localStorage. But, IndexedDB can be tedious to use. So, we will use the Dexie.js wrapper that provides a simpler API for interacting with an IndexedDB database. Here is a link to getting started with Dexie.js.

If you visited the link above, you can see that Dexie.js can be used with several popular libraries including React. We will start with a Vanilla JS example. This is just using HTML, CSS and JavaScript (along with Dexie.js). Here is the starting code:

student_app.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script>
         addEventListener('DOMContentLoaded', init);
         let db;

         function initStudentDatabase() {
            db = new Dexie("Student database");
            db.version(1).stores({
               students: `
                  id,
                  first_name,
                  last_name,
                  major
               `,
            });
            db.students.bulkPut([
               { id: 1, first_name: 'Jane', last_name: 'Doe', major: 'ICS' },
               { id: 2, first_name: 'John', last_name: 'Smith', major: 'MATH' },
               { id: 3, first_name: 'Bob', last_name: 'Data', major: 'CHEM' }
            ]).catch(Dexie.BulkError, function(error) {
               console.log(error);
            });
         }

         function init() {
            console.log('init started');
            initStudentDatabase();
            console.log(db.students.toArray());
         }
      </script>
   </head>
   <body>
   </body>
</html>

Looking at lines 35-36, you can see that there is nothing in the <body> element at this point. So, everything is taking place in the <head> section right now. Inside our <script> element the first thing that is done on line 6, is that we make it so that when the document completes loading, the init() function will be called.

Line 7 declares the db object at the document level, so that it can be accessed from any functions defined in this document. The db object will be our connection to the IndexedDB database through a Dexie object.

Lines 9-26 define the initStudentDatabase() function. Line 10 constructs a new Dexie object that we assign to the document level variable db. On lines 11-12, we create a collection (like a table) called students. Lines 12-16 define the schema for this collection. Lines 19 - 23 attempt to sore three objects inside the students collection. Finally, the catch block going from lines 23-25 will catch any errors that are thrown when trying to bulkPut the objects.

Lines 23 -32 define the init() function. Line 29 just will show that the init() function has been called. Line 30 calls the initStudentDatabase() function that defines the students collection schema and populates that collection with three objects. Finally, on line 31, we query students collection to return all records. No .where() clause is used. The result is returned as an array.

When this page is loaded, you would see the following in the console.

initial promise

JavaScript code executes asynchronously. This means that any function calls that take time to process could complete after lines in the code that follow that function call. A Promise is a way to pause execution until the function call is resolved without having to use callback functions explicitly.

Note that what is returned is a Promise. This makes sense as querying the database takes some finite amount of time. So, to ensure that we get the values after the query has completed, we should use a Promise. If you open up the Promise, you will see that the returned values are inside an object called _value. Let’s modify the code to handle the promise and display the values correctly. Here is the new code:

student_app.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script>
         addEventListener('DOMContentLoaded', init);
         let db;

         function initStudentDatabase() {
            db = new Dexie("Student database");
            db.version(1).stores({
               students: `
                  id,
                  first_name,
                  last_name,
                  major
               `,
            });
            db.students.bulkPut([
               { id: 1, first_name: 'Jane', last_name: 'Doe', major: 'ICS' },
               { id: 2, first_name: 'John', last_name: 'Smith', major: 'MATH' },
               { id: 3, first_name: 'Bob', last_name: 'Data', major: 'CHEM' }
            ]).catch(Dexie.BulkError, function(error) {
               console.log(error);
            });
         }

         function init() {
            console.log('init started');
            initStudentDatabase();
            //console.log(db.students.toArray());
            let students = db.students.toArray();
            students.then(() => console.log(students._value));
         }
      </script>
   </head>
   <body>
   </body>
</html>

The new or modified lines are lines 31-33. Line 31 just comments out the code that printed the Promise to the console. Line 32 stores the Promise in the variable students. Line 33 uses the .then() clause that will be run after the query is completed. Inside the .then() clause we have an anonymous function that just displays the _value part of the completed Promise. So, the console looks like this, after expanding the array.

completed promise value

Anytime you are querying the database, Dexie will use Promises, to make it straightforward to use the results of the query after the query completes.

Storing the collection objects in a table

Let’s start adding the HTML markup inside the <body> element so that we can display the students collection inside a table. Here is the new code:

student_app.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script>
         addEventListener('DOMContentLoaded', init);
         let db;

         function initStudentDatabase() {
            db = new Dexie("Student database");
            db.version(1).stores({
               students: `
                  id,
                  first_name,
                  last_name,
                  major
               `,
            });
            db.students.bulkPut([
               { id: 1, first_name: 'Jane', last_name: 'Doe', major: 'ICS' },
               { id: 2, first_name: 'John', last_name: 'Smith', major: 'MATH' },
               { id: 3, first_name: 'Bob', last_name: 'Data', major: 'CHEM' }
            ]).catch(Dexie.BulkError, function(error) {
               console.log(error);
            });
         }

         function updateStudentsTable() {
            const students_tbody = document.getElementById('students_tbody');
            let students = db.students.toArray();
            let values = []
            students.then(() => {
               values = students._value
               for (let value of values) {
                  let tr = document.createElement('tr');
                  let td = document.createElement('td');
                  let contents = document.createTextNode(value.id);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.first_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.last_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.major);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  students_tbody.appendChild(tr);
               }
            });
         }

         function init() {
            console.log('init started');
            initStudentDatabase();
            //let students = db.students.toArray();
            //students.then(() => console.log(students._value));
            updateStudentsTable();
         }
      </script>
   </head>
   <body>
      <table id="students_table" border="1">
         <thead>
            <tr>
               <th>id</th> <th>First Name</th> <th>Last Name</th> <th>Major</th>
            </tr>
         </thead>
         <tbody id="students_tbody"></tbody>
      </table>
   </body>
</html>

The new lines are lines 28-55, lines 60-62 and lines 67-74. Lines 67-74 add the HTML markup needed for a HTML table. Note on line 73 that we have put in a <tbody> element. This where we will add our table data records. Note that we give this element an id="students_body" so that we can get a reference to this element. +Lines 28-55 define the updateStudentsTable() function. This function will be called each time we want to update our table of students. On line 29 we get a reference to the <tbody> element where we will append all of our data table rows. Line 30 gets a Promise for the query returning all students from the database. Line 31 defines an array called values that will be used to hold those students. At this point, we need to wait for the Promise to be completed before we store those values. Lines 32-54 are the lines that are executed when the Promise if fulfilled. Line 33 stores the values in the values array. Lines 34-53 define a for loop that is used to create <tr> and <td> elements that are used for the table rows for each student. On line 37, we get the id field of the student object and use that to create a <text> element that will be appended to the <td> element. In turn, that <td> element will be appended to the <tr> element. This process is repeated for the first_name, last_name and major fields of a student. Finally, once the table row has all its table data elements, line 52 appends that table row to the <tbody> element.

Lines 60 and 61 comment out the code that displayed the array of students in the console. Line 63 just calls the updateStudentsTable() function. If you reload this page into the Chrome browser, you will see this.

initial table

Adding a student

Now that we can display the students, let’s work on being able to add a student. Before we do that, let’s use the Developer’s Tools (Hit F12 key) to look at the data we have stored. To do this click on the Application tab (instead of Console). Then, expand IndexedDB and finally click on students within IndexedDB → Student database.

IndexedDB students

You can see the three student objects that we inserted. This data will remain until you clear that collection or IndexedDB itself. So, this data will persist in the client’s browser unless it is programatically cleared or the client clears IndexedDB.

If we are to add another student object to the collection, we need to think about the id value. Since the id should be unique, we should use an unique value for the id. In databases like MySQL or PostgreSQL we can define an id field as being of type serial. But, this is not a SQL-based database.

One possibility is that we query the existing data and set the new id to be one larger than the largest existing id. This would work, and might be the way to do this sometimes. But, Dexie.js provides a better solution. With Dexie.js, we can change the schema for the students collection to this:

            db.version(1).stores({
               students: `
                  ++id,
                  first_name,
                  last_name,
                  major
               `,
            });

That is, by changing id to ++id, id will behave like an auto incrementing field. Let’s make changes to our "student_app.html" to use this.

<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script>
         addEventListener('DOMContentLoaded', init);
         let db;

         function initStudentDatabase() {
            db = new Dexie("Student database");
            db.version(1).stores({
               students: `
                  ++id,
                  first_name,
                  last_name,
                  major
               `,
            });
            db.students.bulkPut([
               { id: 1, first_name: 'Jane', last_name: 'Doe', major: 'ICS' },
               { id: 2, first_name: 'John', last_name: 'Smith', major: 'MATH' },
               { id: 3, first_name: 'Bob', last_name: 'Data', major: 'CHEM' }
            ]).catch(Dexie.BulkError, function(error) {
               console.log(error);
            });
         }

         function updateStudentsTable() {
            const students_tbody = document.getElementById('students_tbody');
            let students = db.students.toArray();
            let values = []
            students.then(() => {
               values = students._value
               for (let value of values) {
                  let tr = document.createElement('tr');
                  let td = document.createElement('td');
                  let contents = document.createTextNode(value.id);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.first_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.last_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.major);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  students_tbody.appendChild(tr);
               }
            });
         }

         function init() {
            console.log('init started');
            initStudentDatabase();
            updateStudentsTable();
         }
      </script>
   </head>
   <body>
      <table id="students_table" border="1">
         <thead>
            <tr>
               <th>id</th> <th>First Name</th> <th>Last Name</th> <th>Major</th>
            </tr>
         </thead>
         <tbody id="students_tbody"></tbody>
      </table>
   </body>
</html>

The change has been made to line 13. In order to make the change to the schema, we need to delete the "Student database" so that the "students" table can be recreated. However, to delete the "Student database" we need to change the code so that the database is not being loaded. Here are the modifications that are needed:

<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script>
         addEventListener('DOMContentLoaded', init);
         let db;

         function initStudentDatabase() {
            db = new Dexie("Student database");
            db.version(1).stores({
               students: `++id,
                  first_name,
                  last_name,
                  major`
            });
            /*
            db.students.bulkPut([
               { id: 1, first_name: 'Jane', last_name: 'Doe', major: 'ICS' },
               { id: 2, first_name: 'John', last_name: 'Smith', major: 'MATH' },
               { id: 3, first_name: 'Bob', last_name: 'Data', major: 'CHEM' }
            ]).catch(Dexie.BulkError, function(error) {
               console.log(error);
            });
            */
         }

         function updateStudentsTable() {
            const students_tbody = document.getElementById('students_tbody');
            let students = db.students.toArray();
            let values = []
            students.then(() => {
               values = students._value
               for (let value of values) {
                  let tr = document.createElement('tr');
                  let td = document.createElement('td');
                  let contents = document.createTextNode(value.id);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.first_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.last_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.major);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  students_tbody.appendChild(tr);
               }
            });
         }

         function init() {
            console.log('init started');
            initStudentDatabase();
            //let students = db.students.toArray();
            //students.then(() => console.log(students._value));
            //updateStudentsTable();
         }
      </script>
   </head>
   <body>
      <table id="students_table" border="1">
         <thead>
            <tr>
               <th>id</th> <th>First Name</th> <th>Last Name</th> <th>Major</th>
            </tr>
         </thead>
         <tbody id="students_tbody"></tbody>
      </table>
   </body>
</html>

Lines 17-25 comment out the call to bulkPut(). Line 62 comments out the line to updateStudentsTable(). This will prevent any connection to the database. Then, from the Console, run the following command:

delete database console

Click on the Application tab to make sure that the database is deleted:

database deleted

If you change the schema for a Dexie table, you should delete the database for the new schema to be used. If the change to the schema is not to a primary key, you may be able to just increase the version for the creation of the table. But, it is probably safer to just delete and recreate the table.

Now, we can uncomment those lines and load the web page again. Here is the code with those lines uncommented:

<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script>
         addEventListener('DOMContentLoaded', init);
         let db;

         function initStudentDatabase() {
            db = new Dexie("Student database");
            db.version(1).stores({
               students: `++id,
                  first_name,
                  last_name,
                  major`
            });
            db.students.bulkPut([
               { id: 1, first_name: 'Jane', last_name: 'Doe', major: 'ICS' },
               { id: 2, first_name: 'John', last_name: 'Smith', major: 'MATH' },
               { id: 3, first_name: 'Bob', last_name: 'Data', major: 'CHEM' }
            ]).catch(Dexie.BulkError, function(error) {
               console.log(error);
            });
         }

         function updateStudentsTable() {
            const students_tbody = document.getElementById('students_tbody');
            let students = db.students.toArray();
            let values = []
            students.then(() => {
               values = students._value
               for (let value of values) {
                  let tr = document.createElement('tr');
                  let td = document.createElement('td');
                  let contents = document.createTextNode(value.id);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.first_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.last_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.major);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  students_tbody.appendChild(tr);
               }
            });
         }

         function init() {
            console.log('init started');
            initStudentDatabase();
            //let students = db.students.toArray();
            //students.then(() => console.log(students._value));
            updateStudentsTable();
         }
      </script>
   </head>
   <body>
      <table id="students_table" border="1">
         <thead>
            <tr>
               <th>id</th> <th>First Name</th> <th>Last Name</th> <th>Major</th>
            </tr>
         </thead>
         <tbody id="students_tbody"></tbody>
      </table>
   </body>
</html>

Note that we still need to supply the id when we use bulkPut() on lines 19-25. This is because bulkPut only listens to the success events of the last object put in. This is so that bulkPut runs faster. This also means, that bulkPut will not be able to calculate the next id, and so the id must be supplied.

The Table.add() method of Dexie.js

Dexie.js refers to IndexedDB object storage as a Table. When you query the database, a Collection is used to help perform that query. The Collection methods toArray(), keys(), count() or each() perform a query that returns a Promise.

So, if you are manipulating the contents of a data store, then you should use Table methods. If you are trying to perform selection queries on a Table, you are using Collection methods.

Creating a dialog box for adding a student

We are finally ready to add a student. We will use a dialog box for this. The HTML <dialog> element has been improved a lot in the last year or so. This makes it a viable choice for implementing a dialog box for input entry. Here is the starting code to using the <dialog> for user input entry:

student_app.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script>
         addEventListener('DOMContentLoaded', init);
         let db;

         function initStudentDatabase() {
            db = new Dexie("Student database");
            db.version(1).stores({
               students: `++id,
                  first_name,
                  last_name,
                  major`
            });
            db.students.bulkPut([
               { id: 1, first_name: 'Jane', last_name: 'Doe', major: 'ICS' },
               { id: 2, first_name: 'John', last_name: 'Smith', major: 'MATH' },
               { id: 3, first_name: 'Bob', last_name: 'Data', major: 'CHEM' }
            ]).catch(Dexie.BulkError, function(error) {
               console.log(error);
            });
         }

         function updateStudentsTable() {
            const students_tbody = document.getElementById('students_tbody');
            let students = db.students.toArray();
            let values = []
            students.then(() => {
               values = students._value
               for (let value of values) {
                  let tr = document.createElement('tr');
                  let td = document.createElement('td');
                  let contents = document.createTextNode(value.id);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.first_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.last_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.major);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  students_tbody.appendChild(tr);
               }
            });
         }

         function showAddDialog() {
            console.log('showAddDialog() called');
            const add_dialog = document.getElementById('add_dialog');
            add_dialog.showModal();
         }

         function addStudent() {
            const first_name_box = document.getElementById('first_name_box');
            const last_name_box = document.getElementById('last_name_box');
            const major_box = document.getElementById('major_box');
            const fn = first_name_box.value.trim();
            const ln = last_name_box.value.trim();
            const maj = major_box.value.trim();
            db.students.add({ first_name: fn, last_name: ln, major: maj});
            updateStudentsTable();
            const add_dialog = document.getElementById('add_dialog');
            add_dialog.close();
         }

         function init() {
            console.log('init started');
            initStudentDatabase();
            updateStudentsTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            const add_ok = document.getElementById('add_ok');
            add_ok.addEventListener('click', addStudent);
         }
      </script>
   </head>
   <body>
      <button id="add_button">Add</button>
      <dialog id="add_dialog">
         First Name:
         <input type="text" id="first_name_box"><br>
         Last Name:
         <input type="text" id="last_name_box"><br>
         Major:
         <input type="text" id="major_box"><br>
         <br>
         <button id="add_cancel">Cancel</button>
         <button id="add_ok">Ok</button>
      </dialog>
      <table id="students_table" border="1">
         <thead>
            <tr>
               <th>id</th> <th>First Name</th> <th>Last Name</th> <th>Major</th>
            </tr>
         </thead>
         <tbody id="students_tbody"></tbody>
      </table>
   </body>
</html>

The new lines are lines 55-59, 61-72, 78-81 and 86-97. Line 86 creates a button that will be clicked on to bring up the dialog box used to add a student. Lines 87-97 define a <dialog> element with an id="add_dialog". Lines 88-93 just add text for prompts and <input elements of type "text". These will be used to obtain the user input. Line 95 adds a Cancel button that will eventually be used to cancel the process of adding a student. Right now, this Cancel button does not do anything yet. Line 96 adds an Ok button that will be used to submit the student to be added. Note that all elements that need to be accessed using JavaScript have id attributes.

Lines 78-79 get a reference to the add button and associate the showAddDialog() function with clicking on that button. Lines 80-81 get a reference to the add Ok button and associate the addStudent() function with clicking on that Ok button.

Lines 55-59 define the showAddDialog() function. Line 56 was used for debugging purposes and just lets you know that the function was called. This line will be removed later. Line 57 gets a reference to the <dialog> element with an id="add_dialog". Line 58 makes this dialog show up as a modal dialog. Recall that modal dialog boxes must be dealt with before any other parts of the program can run.

Lines 61-72 define the addStudent() function. This is the function that is called when you click on the Ok button in the dialog box used to add a student. Lines 62-64 just get references to all the text input boxes in the dialog. Lines 65-67 obtain the user input values in those boxes. Line 68 gets the students table (db.students) and calls the add() method to add in the student object to the database table. Note that we don’t supply an id value as Dexie.js will automatically generate the id. Line 69 calls the updateStudentsTable() function to update the HTML table. Line 70 gets a reference to the <dialog> so that we can close it on line 71.

The following shows what happened when we tried to add Alice Brown, an ICS major.

repeats in table

The first three rows of the HTML table are repeated. If you think about it, this is caused by the HTML’s <tbody> element being appended to in the updateStudentsTable() function. To correct this, we will add a function that can clear out the children of a HTML element. We can then call this function as part of the updateStudentsTable() function. Here is the new code:

student_app.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script>
         addEventListener('DOMContentLoaded', init);
         let db;

         function initStudentDatabase() {
            db = new Dexie("Student database");
            db.version(1).stores({
               students: `++id,
                  first_name,
                  last_name,
                  major`
            });
            db.students.bulkPut([
               { id: 1, first_name: 'Jane', last_name: 'Doe', major: 'ICS' },
               { id: 2, first_name: 'John', last_name: 'Smith', major: 'MATH' },
               { id: 3, first_name: 'Bob', last_name: 'Data', major: 'CHEM' }
            ]).catch(Dexie.BulkError, function(error) {
               console.log(error);
            });
         }

         function removeChildren(element) {
            while (element.childNodes.length > 0) {
               element.removeChild(element.childNodes[0]);
            }
         }

         function updateStudentsTable() {
            const students_tbody = document.getElementById('students_tbody');
            removeChildren(students_tbody);
            let students = db.students.toArray();
            let values = []
            students.then(() => {
               values = students._value
               for (let value of values) {
                  let tr = document.createElement('tr');
                  let td = document.createElement('td');
                  let contents = document.createTextNode(value.id);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.first_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.last_name);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  td = document.createElement('td');
                  contents = document.createTextNode(value.major);
                  td.appendChild(contents);
                  tr.appendChild(td);
                  students_tbody.appendChild(tr);
               }
            });
         }

         function showAddDialog() {
            console.log('showAddDialog() called');
            const add_dialog = document.getElementById('add_dialog');
            add_dialog.showModal();
         }

         function addStudent() {
            const first_name_box = document.getElementById('first_name_box');
            const last_name_box = document.getElementById('last_name_box');
            const major_box = document.getElementById('major_box');
            const fn = first_name_box.value.trim();
            const ln = last_name_box.value.trim();
            const maj = major_box.value.trim();
            db.students.add({ first_name: fn, last_name: ln, major: maj});
            updateStudentsTable();
            const add_dialog = document.getElementById('add_dialog');
            add_dialog.close();
         }

         function init() {
            console.log('init started');
            initStudentDatabase();
            updateStudentsTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            const add_ok = document.getElementById('add_ok');
            add_ok.addEventListener('click', addStudent);
         }
      </script>
   </head>
   <body>
      <button id="add_button">Add</button>
      <dialog id="add_dialog">
         First Name:
         <input type="text" id="first_name_box"><br>
         Last Name:
         <input type="text" id="last_name_box"><br>
         Major:
         <input type="text" id="major_box"><br>
         <br>
         <button id="add_cancel">Cancel</button>
         <button id="add_ok">Ok</button>
      </dialog>
      <table id="students_table" border="1">
         <thead>
            <tr>
               <th>id</th> <th>First Name</th> <th>Last Name</th> <th>Major</th>
            </tr>
         </thead>
         <tbody id="students_tbody"></tbody>
      </table>
   </body>
</html>

The new lines are lines 26-30 and line 34. Lines 26-30 define the removeChildren() function. When this function is passed a HTML element, that element is checked to see if it has any child nodes. As long as the element has child nodes, we remove the first child node. This will eventually clear out all the children.

Line 34 calls the removeChildren() function so that the <tbody> element of the HTML table is cleared out before we append the students to the table. Now, the HTML table works as we wanted.

fixed with remove children

Cleaning up the code

At this point, we will clean up the code somewhat. One of the things we need to do is to make the Cancel button in the add dialog work. The other thing we can do is modify the updateStudentsTable() function so that it handles the promise using async and await. Here is the modified code after doing these things.

student_app.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script>
         addEventListener('DOMContentLoaded', init);
         let db;

         function initStudentDatabase() {
            db = new Dexie("Student database");
            db.version(1).stores({
               students: `++id,
                  first_name,
                  last_name,
                  major`
            });
            db.students.bulkPut([
               { id: 1, first_name: 'Jane', last_name: 'Doe', major: 'ICS' },
               { id: 2, first_name: 'John', last_name: 'Smith', major: 'MATH' },
               { id: 3, first_name: 'Bob', last_name: 'Data', major: 'CHEM' }
            ]).catch(Dexie.BulkError, function(error) {
               console.log(error);
            });
         }

         function removeChildren(element) {
            while (element.childNodes.length > 0) {
               element.removeChild(element.childNodes[0]);
            }
         }

         async function updateStudentsTable() {
            const students_tbody = document.getElementById('students_tbody');
            removeChildren(students_tbody);
            let students = await db.students.toArray();
            for (let student of students) {
               let tr = document.createElement('tr');
               let td = document.createElement('td');
               let contents = document.createTextNode(student.id);
               td.appendChild(contents);
               tr.appendChild(td);
               td = document.createElement('td');
               contents = document.createTextNode(student.first_name);
               td.appendChild(contents);
               tr.appendChild(td);
               td = document.createElement('td');
               contents = document.createTextNode(student.last_name);
               td.appendChild(contents);
               tr.appendChild(td);
               td = document.createElement('td');
               contents = document.createTextNode(student.major);
               td.appendChild(contents);
               tr.appendChild(td);
               students_tbody.appendChild(tr);
            }
         }

         function showAddDialog() {
            console.log('showAddDialog() called');
            const add_dialog = document.getElementById('add_dialog');
            add_dialog.showModal();
         }

         function addStudent() {
            const first_name_box = document.getElementById('first_name_box');
            const last_name_box = document.getElementById('last_name_box');
            const major_box = document.getElementById('major_box');
            const fn = first_name_box.value.trim();
            const ln = last_name_box.value.trim();
            const maj = major_box.value.trim();
            db.students.add({ first_name: fn, last_name: ln, major: maj});
            updateStudentsTable();
            const add_dialog = document.getElementById('add_dialog');
            add_dialog.close();
         }

         function init() {
            console.log('init started');
            initStudentDatabase();
            updateStudentsTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            const add_ok = document.getElementById('add_ok');
            add_ok.addEventListener('click', addStudent);
            const add_cancel = document.getElementById('add_cancel');
            add_cancel.addEventListener('click', () => {
               document.getElementById('add_dialog').close();
            });
         }
      </script>
   </head>
   <body>
      <button id="add_button">Add</button>
      <dialog id="add_dialog">
         First Name:
         <input type="text" id="first_name_box"><br>
         Last Name:
         <input type="text" id="last_name_box"><br>
         Major:
         <input type="text" id="major_box"><br>
         <br>
         <button id="add_cancel">Cancel</button>
         <button id="add_ok">Ok</button>
      </dialog>
      <table id="students_table" border="1">
         <thead>
            <tr>
               <th>id</th> <th>First Name</th> <th>Last Name</th> <th>Major</th>
            </tr>
         </thead>
         <tbody id="students_tbody"></tbody>
      </table>
   </body>
</html>

The new or modified lines are lines 32, 35-36, 39, 43, 47, 51 and lines 85-88. Lines 85-88 make it so that the Cancel button in the add dialog works. Line 85 gets a reference to that Cancel button. Lines 86-88 make it so that when that button is clicked, the add dialog is closed.

Line 32 changes the updateStudentsTable() function is defined as an async function. This allows the use of await. Putting await before a call to a Promise will halt the program execution until the Promise has been fulfilled. This effectively makes the JavaScript behave synchronously while the Promise is being fulfilled. So, line 36 will not start to be executed until line 35 is finished obtaining the students data. Also, note that when the Promise is finished, the actual array is returned, not a Promise. So, on line 35, the variable students is the actual array (not part of a Promise). This removes the lines that obtained the array by using Promise._value. So, this is simpler and easier to understand. Line 36 changes the loop variable to student instead of value. Lines 39, 43, 47 and 51 have been modified to use student instead of value.

Editing a student

Now, let’s work on editing a student. When you edit data, you need to be able to select the data you want to edit. Then, you need to put the data into a dialog box where you can make changes. Finally, you need to be able to update the data source and have the table showing the data be refreshed.

To select the data, we will use CSS to make it so that the table row will change color when the mouse hovers over that row. Then, we will add an EventListener to the 'click' event so that when we click on the highlighted row it will call a function that will show a modal dialog box. That modal dialog box will display the current values for the student properties and allow editing them. Then, clicking on the Ok button for that dialog box will update the data stored in the IndexedDB database. After that, all we need to do is call the updateStudentsTable() function to update the HTML table. Here is the new code that does that.

student_app.html
<!DOCTYPE html>
<html>
   <head>
      <style>
         tr:hover { background-color: #ccffcc; color: #000000; }
      </style>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script>
         addEventListener('DOMContentLoaded', init);
         let db;
         let saved_student_id;

         function initStudentDatabase() {
            db = new Dexie("Student database");
            db.version(1).stores({
               students: `++id,
                  first_name,
                  last_name,
                  major`
            });
            db.students.bulkPut([
               { id: 1, first_name: 'Jane', last_name: 'Doe', major: 'ICS' },
               { id: 2, first_name: 'John', last_name: 'Smith', major: 'MATH' },
               { id: 3, first_name: 'Bob', last_name: 'Data', major: 'CHEM' }
            ]).catch(Dexie.BulkError, function(error) {
               console.log(error);
            });
         }

         function removeChildren(element) {
            while (element.childNodes.length > 0) {
               element.removeChild(element.childNodes[0]);
            }
         }

         async function updateStudentsTable() {
            const students_tbody = document.getElementById('students_tbody');
            removeChildren(students_tbody);
            let students = await db.students.toArray();
            for (let student of students) {
               let tr = document.createElement('tr');
               tr.addEventListener('click', showEditDialog);
               tr.setAttribute("id", student.id);
               let td = document.createElement('td');
               let contents = document.createTextNode(student.id);
               td.appendChild(contents);
               tr.appendChild(td);
               td = document.createElement('td');
               contents = document.createTextNode(student.first_name);
               td.appendChild(contents);
               tr.appendChild(td);
               td = document.createElement('td');
               contents = document.createTextNode(student.last_name);
               td.appendChild(contents);
               tr.appendChild(td);
               td = document.createElement('td');
               contents = document.createTextNode(student.major);
               td.appendChild(contents);
               tr.appendChild(td);
               students_tbody.appendChild(tr);
            }
         }

         async function showEditDialog(event) {
            id = event.currentTarget.id;
            id = Number(id);
            saved_student_id = id;
            const first_name_edit_box = document.getElementById('first_name_edit_box');
            const last_name_edit_box = document.getElementById('last_name_edit_box');
            const major_edit_box = document.getElementById('major_edit_box');
            let student = await db.students.where('id').equals(id).toArray();
            student = student[0];
            first_name_edit_box.value = student.first_name;
            last_name_edit_box.value = student.last_name;
            major_edit_box.value = student.major;
            edit_dialog = document.getElementById('edit_dialog');
            edit_dialog.showModal();
         }

         async function editStudent() {
            const first_name_edit_box = document.getElementById('first_name_edit_box');
            const last_name_edit_box = document.getElementById('last_name_edit_box');
            const major_edit_box = document.getElementById('major_edit_box');
            const fn = first_name_edit_box.value.trim();
            const ln = last_name_edit_box.value.trim();
            const maj = major_edit_box.value.trim();
            await db.students.update(saved_student_id, {
               first_name: fn, last_name: ln, major: maj
            });
            updateStudentsTable();
            document.getElementById('edit_dialog').close();
         }

         function showAddDialog() {
            console.log('showAddDialog() called');
            const add_dialog = document.getElementById('add_dialog');
            add_dialog.showModal();
         }

         function addStudent() {
            const first_name_box = document.getElementById('first_name_box');
            const last_name_box = document.getElementById('last_name_box');
            const major_box = document.getElementById('major_box');
            const fn = first_name_box.value.trim();
            const ln = last_name_box.value.trim();
            const maj = major_box.value.trim();
            db.students.add({ first_name: fn, last_name: ln, major: maj});
            updateStudentsTable();
            const add_dialog = document.getElementById('add_dialog');
            add_dialog.close();
         }

         function init() {
            console.log('init started');
            initStudentDatabase();
            updateStudentsTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            const add_ok = document.getElementById('add_ok');
            add_ok.addEventListener('click', addStudent);
            const add_cancel = document.getElementById('add_cancel');
            add_cancel.addEventListener('click', () => {
               document.getElementById('add_dialog').close();
            });
            const edit_ok = document.getElementById('edit_ok');
            edit_ok.addEventListener('click', editStudent);
            const edit_cancel = document.getElementById('edit_cancel');
            edit_cancel.addEventListener('click', () => {
               document.getElementById('edit_dialog').close();
            });
         }
      </script>
   </head>
   <body>
      <button id="add_button">Add</button>
      <dialog id="add_dialog">
         First Name:
         <input type="text" id="first_name_box"><br>
         Last Name:
         <input type="text" id="last_name_box"><br>
         Major:
         <input type="text" id="major_box"><br>
         <br>
         <button id="add_cancel">Cancel</button>
         <button id="add_ok">Ok</button>
      </dialog>
      <dialog id="edit_dialog">
         First Name:
         <input type="text" id="first_name_edit_box"><br>
         Last Name:
         <input type="text" id="last_name_edit_box"><br>
         Major:
         <input type="text" id="major_edit_box"><br>
         <br>
         <button id="edit_cancel">Cancel</button>
         <button id="edit_ok">Ok</button>
      </dialog>
      <table id="students_table" border="1">
         <thead>
            <tr>
               <th>id</th> <th>First Name</th> <th>Last Name</th> <th>Major</th>
            </tr>
         </thead>
         <tbody id="students_tbody"></tbody>
      </table>
   </body>
</html>

The new lines are lines 4-6, 11, 42-43, 64-78, 80-92, 125-130 and 147-157. Lines 4-6 are needed to make the table row (<tr>) seem to be highlighted. The background color of #ccffcc is a light green color and the color of #000000 is black. You should check to see that when you highlight in this manner that there is enough contrast between the background color and the foreground color. This particular combination was checked at the following site: Color-pair Contrast Testing. As can be seen in the following screenshot this has a high contrast ratio of 18.74 to 1.

contrast pair

As you can see in the screen shot, this is higher than the ratio of 7:1, and passes the test. You could just add lines 4-6 and you will see that you can highlight rows in the table.

Line 11 adds a document level variable called saved_student_id. This is because we need to save this value when handling the event of clicking on a table row. Saving this id will be done inside of the showEditDialog() function. This saved id value will be needed when we are updating the student inside the editStudent() function. By declaring the saved_student_id variable at the document level, that variable will be accessible in any of the functions.

Lines 42 and 43 add two very important features to our table rows. Line 42 makes it so that clicking on a <tr> element will cause the showEditDialog() function to be run. This function is used to fill in the input text boxes and display the dialog containing those input boxes. Line 43 gives each <tr> element an id attribute with the value set to the student’s id.

Lines 64-78 define the showEditDialog() function that is called when the user click on a table row. Note that this function is declared as async. This is because when we query the database on line 71, we need to await the result. So, line 72 is not executed until the result has been returned. Line 65 obtains the id from the table row that has been clicked on. Note that event.currentTarget returns the entire <tr> element. If you wanted one of the <td> elements, you would use event.target instead. Line 66 converts the id attribute value into a number. This is important for the query on line 71 to succeed. Line 67 saves this (numerical) id in saved_student_id for use in the editStudent() function. Lines 68-70 obtain references to all the input text boxes. Line 71 will query the database for the student matching the id and halt execution until the student is returned. Note that the toArray() function will return an array. This is because a select query can return more than one record in general. Line 72 gets the first record, which is the only record returned in this case (as the id is unique). Lines 73-75 sets the values from the database record into the input text boxes. Finally, lines 76 and 77 get a reference to the edit dialog (defined on lines 147-157 in the markup), and show that as a modal dialog.

Lines 80-92 define the editStudent() function. Note that this function is declared as async so that we can use await. This is the function that is used to updated the student object in the database. Lines 81-83 just obtain references to the input text boxes. Line 84-86 just get the values stored in the input text boxes. Lines 87-89 use await to call the update() method to update the student object. Note that the basic syntax for the update function is this:

Table.update(id, {})

   where {} holds the updated object values

Line 90 updates the HTML table using the updated database values. Line 91 just closes the edit dialog box.

Line 125 gets a reference to the Ok button inside the edit dialog box. Line 126 makes it so that clicking that button will cause the editStudent() function to be called. Line 127 gets a reference to the Cancel button inside the edit dialog box. Lines 128-129 make it so that the anonymous function closes the edit dialog box if the user hits that Cancel button.

Finally, lines 147-157 supply the markup for the edit dialog box. Lines 148-153 set up input text boxes for editing the student properties. Line 155 sets up the Cancel button that the user can hit to cancel the edits. Line 156 sets up the Ok button that is clicked when the user wants to submit the changes.