Using List.js

List.js for tables

List.js

Although DataTables 2 is a powerful jQuery plugin for handling tables, it may not always be the best choice. A simpler alternative is List.js

DataTables controls many aspects of the table, and so mixing HTML and Javascript with DataTables can make things overly complicated. In addition, there are parts of DataTables that can only be used in the paid versions.

List.js is free to use and is a lot more lightweight than DataTables. Unlike DataTables, List.js does not have any dependencies. DataTables requires jQuery. By contrast, List.js is written in vanilla JavaScript. But, List.js can add sorting and filtering to HTML tables and sometimes that is all that is needed.

Simple example using List.js

Here is the file "showList.html" that is a very simple example of using List.js to display a table. This is the first version of this file so it just displays the table:

showList.html
<!DOCTYPE html>
<html>
   <head>
      <script src="//cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js">
      </script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let options = {
            valueNames: [ 'first_name', 'last_name', 'major' ]
         };
         let mylist;
         function init() {
            mylist = new List('mylist', options);
         }
      </script>
   </head>
   <body>
      <div id="mylist">
         <table border="1">
            <thead>
               <tr>
                  <th>
                     First Name
                  </th>
                  <th>
                     Last Name
                  </th>
                  <th>
                     Major
                  </th>
               </tr>
            </thead>
            <tbody class="list">
               <tr>
                  <td class="first_name">Jane</td>
                  <td class="last_name">Doe</td>
                  <td class="major">ICS</td>
               </tr>
               <tr>
                  <td class="first_name">John</td>
                  <td class="last_name">Smith</td>
                  <td class="major">MATH</td>
               </tr>
               <tr>
                  <td class="first_name">Bob</td>
                  <td class="last_name">Data</td>
                  <td class="major">ICS</td>
               </tr>
               <tr>
                  <td class="first_name">Carol</td>
                  <td class="last_name">White</td>
                  <td class="major">CHEM</td>
               </tr>
               <tr>
                  <td class="first_name">Gordon</td>
                  <td class="last_name">Daniels</td>
                  <td class="major">PHYS</td>
               </tr>
            </tbody>
         </table>
      </div>
   </body>
</html>

Lines 4-5 include the List.js code. Lines 8-10 set up the column names for the table. Lines 11-13 create the List object.

Lines 18-61 define a <div> element that has an id="mylist". This makes it the <div> to use when creating the List. For a simple table, you can just create it like a regular HTML table. The only additions are that the <tbody> must have a class="list" attribute and the <td> elements must have class attributes that match the respective column names. Lines 33-59 define the <tbody> element that provides the data for the List object. Here is what this page looks like presently:

list table1

This is just like a regular HTML table. But, now let’s add the ability to search with the table and to sort by each of the columns.

showList.html
<!DOCTYPE html>
<html>
   <head>
      <script src="//cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js">
      </script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let options = {
            valueNames: [ 'first_name', 'last_name', 'major' ]
         };
         let mylist;
         function init() {
            mylist = new List('mylist', options);
         }
      </script>
   </head>
   <body>
      <div id="mylist">
         <input class="search">Search<br>
         <table border="1">
            <thead>
               <tr>
                  <th>
                     <button class="sort" data-sort="first_name">
                     First Name
                     </button>
                  </th>
                  <th>
                     <button class="sort" data-sort="last_name">
                     Last Name
                     </button>
                  </th>
                  <th>
                     <button class="sort" data-sort="major">
                     Major
                     </button>
                  </th>
               </tr>
            </thead>
            <tbody class="list">
               <tr>
                  <td class="first_name">Jane</td>
                  <td class="last_name">Doe</td>
                  <td class="major">ICS</td>
               </tr>
               <tr>
                  <td class="first_name">John</td>
                  <td class="last_name">Smith</td>
                  <td class="major">MATH</td>
               </tr>
               <tr>
                  <td class="first_name">Bob</td>
                  <td class="last_name">Data</td>
                  <td class="major">ICS</td>
               </tr>
               <tr>
                  <td class="first_name">Carol</td>
                  <td class="last_name">White</td>
                  <td class="major">CHEM</td>
               </tr>
               <tr>
                  <td class="first_name">Gordon</td>
                  <td class="last_name">Daniels</td>
                  <td class="major">PHYS</td>
               </tr>
            </tbody>
         </table>
      </div>
   </body>
</html>

The new lines are lines 19, 24, 26, 29, 31, 34 and 36. Line 19 adds in the capability to Search within the table. Lines 24 and 26 place a <button> element inside the <th> element for the first_name. By using the class="sort" and data-sort="first_name this will allow clicking on that button to sort by the first name column. Lines 29, 31, 34 and 36 do the same thing for the other two columns. With these changes, the table can now be searched and sorted by columns. But, the column headings look funny because they look like buttons. So, we can add some CSS styles to make the buttons look more like column headings. Here is the new code:

showList.html
<!DOCTYPE html>
<html>
   <head>
      <style>
         thead button {
            border: none;
            font-weight: bold;
         }
      </style>
      <script src="//cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js">
      </script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let options = {
            valueNames: [ 'first_name', 'last_name', 'major' ]
         };
         let mylist;
         function init() {
            mylist = new List('mylist', options);
         }
      </script>
   </head>
   <body>
      <div id="mylist">
         <input class="search">Search<br>
         <table border="1">
            <thead>
               <tr>
                  <th>
                     <button class="sort" data-sort="first_name">
                     First Name
                     </button>
                  </th>
                  <th>
                     <button class="sort" data-sort="last_name">
                     Last Name
                     </button>
                  </th>
                  <th>
                     <button class="sort" data-sort="major">
                     Major
                     </button>
                  </th>
               </tr>
            </thead>
            <tbody class="list">
               <tr>
                  <td class="first_name">Jane</td>
                  <td class="last_name">Doe</td>
                  <td class="major">ICS</td>
               </tr>
               <tr>
                  <td class="first_name">John</td>
                  <td class="last_name">Smith</td>
                  <td class="major">MATH</td>
               </tr>
               <tr>
                  <td class="first_name">Bob</td>
                  <td class="last_name">Data</td>
                  <td class="major">ICS</td>
               </tr>
               <tr>
                  <td class="first_name">Carol</td>
                  <td class="last_name">White</td>
                  <td class="major">CHEM</td>
               </tr>
               <tr>
                  <td class="first_name">Gordon</td>
                  <td class="last_name">Daniels</td>
                  <td class="major">PHYS</td>
               </tr>
            </tbody>
         </table>
      </div>
   </body>
</html>

The new lines are lines 4-9. Those define a <style> element that defines the styles for any <button> element inside a <thead> element. Line 6 removes the border for the button and line 7 uses a bold font. Here is what the resulting page looks like after the first_name header has been clicked on:

list table2

This could be made to look nicer, but this should give you an idea of some of the basic capabilities of List.js.

Using Dexie.js with List.js

When using Dexie.js with DataTables, there are issues because DataTables rewrites the table. This can remove event listeners that were added when updating the table using plain JavaScript and HTML. Let’s look and see how we might use Dexie.js with List.js. Here is the file "DexiePlusList.html":

DexiePlusList.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script src="//cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let students_data = [];
         let options = {
            valueNames: [ 'id', 'first_name', 'last_name', 'major' ]
         };
         let mylist;

         function initDatabase() {
            if (db === undefined) {
               db = new Dexie("Test database");
               db.version(1).stores({
                  students: `++id,
                     first_name,
                     last_name,
                     major`
               });
            }
         }

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

         function handleAdd() {
            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();
         }

         async function updateStudentsTable() {
            students_data = await db.students.toArray();
            mylist.clear();
            for (let student of students_data) {
               let student_value = {
                  id: student.id,
                  first_name: student.first_name,
                  last_name: student.last_name,
                  major: student.major
               };
               mylist.add(student_value);
            }
         }

         function init() {
            initDatabase();
            mylist = new List('student_list', options);
            updateStudentsTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            const add_ok = document.getElementById('add_ok');
            add_ok.addEventListener('click', handleAdd);
         }

      </script>
   </head>
   <body>
      <button id="add_button">Add student</button><br>
      <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>
      <div id="student_list">
         <input class="search"> Search<br>
         <table border="1">
            <thead>
               <tr>
                  <th>
                     <button class="sort" data-sort="id">id</button>
                  <th>
                     <button class="sort" data-sort="first_name">First Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="last_name">Last Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="major">Major</button>
                  </th>
               </tr>
            </thead>
            <tbody class="list">
               <tr>
                  <td class="id"></td>
                  <td class="first_name"></td>
                  <td class="last_name"></td>
                  <td class="major"></td>
               </tr>
            </tbody>
         </table>
      </div>
   </body>
</html>

Line 4 includes the code to make use of Dexie.js. Line 5 includes the code to use List.js. Line 7 makes it so that the init() function is called when the page finishes loading in the browser. Lines 8-13 define some document level variables that can be accessed in any of the functions in this script. On line 8, we define db, which will become the reference to connect to the Dexie object. On line 9, we create an array that will be iterated over to take the student objects in the Dexie database and create the objects to be stored in the List object. Lines 10-12 define the values used for the column headings and fields for the List object. Line 13 defines mylist which will become the reference to the List object.

Lines 15-25 define the initDatabase() function. This function will check to see if db is undefined. If so, then a new IndexedDB database named "Test database" is created and the stores table is created with the schema set on lines 19-22. On the other hand, if db is already defined, nothing is done.

Lines 27-30 define the showAddDialog() function. All it does it gets a reference to the <dialog> element with an id="add_dialog" attribute, and call the showModal() function to display it as a modal dialog.

Lines 32-43 define the handleAdd() function. Lines 33-38 just get references to the input text boxes and obtain the values from those boxes. Line 39 stores those values as an object in the Dexie database. The schema for that table will result in the id being a unique id. This is important because we can use that id when writing to the List object. List.js is used to display the data in a table (or a list) and allow searching and sorting by fields. But, List.js does not generate an id that can be used as a primary key. Line 40 calls the updateStudentsTable() function. That function will update the List object, and that will cause the table to be updated at the same time. Lines 41 and 42 are just used to close the add dialog box.

Lines 45-57 define the updateStudentsTable() function. This is an async function because it needs to await returning the data from the Dexie database. Line 46 uses await to wait for the data to be returned from the database. Line 47 clears mylist. This clears the displayed table. Lines 48-56 define a for loop that creates an object that contains the four values needed for the List. Line 55 adds this object to mylist and this will cause the table to be updated.

This shows how simple it is to use a table with the List.js library. You don’t have to create all the <tr> and <td> elements. You simply set things up for the List.js to do its job, and then you just update the List object. The table gets redrawn automatically.

Lines 59-67 define the init() function. Line 60 will initialize the Dexie database if it does not already exist. Line 61 will create the List object and assign it to mylist. Line 62 will call updateStudentsTable() to get any objects stored in the Dexie database and add them to mylist. This will cause the table to be displayed in the page. Lines 63 and 64 get a reference to the add button above the table and associate the showAddDialog() function with clicking on that button. Lines 65 and 66 will get a reference to the Ok button in the add dialog and associate the handleAdd() function with clicking on that Ok button.

Line 72 adds the markup for the add button. Based on lines 63 and 64 in the init() function, clicking on that button will case the add dialog box to be displayed. Lines 73-83 show the markup for the add dialog box. Lines 74-79 put in the prompts and input text boxes for getting the first name, last name and major for a new student. Line 81 adds a <button> element to cancel adding a student. This button does not yet have a callback function, so it does not do anything yet. Instead you can just hit the ESC key to cancel the add dialog box. Line 82 add a <button> element that when clicked on will add the student to the Dexie database and then update the displayed table.

Lines 84-111 is all the markup used for the List object. Line 85 adds in an input text box that is used for searching (filtering) the table. Lines 86-110 define the <table> element. Lines 87-101 define the <thead> element. Note that we place buttons inside the <th> elements so that we can sort by clicking on the column header.

Lines 102-109 define the <tbody> element. This is a critical element as the objects in mylist will be displayed within this element. Therefore, the value of the class="list" attribute is used when constructing mylist. Lines 103 define what is called a template. This is needed for the List constructor, if the List is initially empty.

It is important to understand how Dexie.js and List.js are being used here. We are using Dexie to create a IndexedDB database with a schema used to define a Dexie table. This means that the data stored in the Dexie object will be available locally until that database is cleared. In addition, using a Dexie object allows us to have an autoincrementing id for each record. List.js is used to control displaying the objects in a table or list. This saves us from programmatically constructing each table row and cell. This also gives us a relatively easy way to provide searching (filtering) and sorting for the table data. List.js does not provide a way to generate an autoincrementing id, so we use Dexie for that purpose. And, Dexie makes the data persist in the client’s browser. So, as long as they are using the same computer and browser, the data is stored until they decide to clear IndexedDB.

One thing that we can add is some CSS styles for the table headers so that they don’t look like buttons. Here is the resulting code:

DexiePlusList.html
<!DOCTYPE html>
<html>
   <head>
      <style>
         thead button {
            border: none;
            font-weight: bold;
         }
      </style>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script src="//cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let students_data = [];
         let options = {
            valueNames: [ 'id', 'first_name', 'last_name', 'major' ]
         };
         let mylist;

         function initDatabase() {
            if (db === undefined) {
               db = new Dexie("Test database");
               db.version(1).stores({
                  students: `++id,
                     first_name,
                     last_name,
                     major`
               });
            }
         }

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

         function handleAdd() {
            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();
         }

         async function updateStudentsTable() {
            students_data = await db.students.toArray();
            mylist.clear();
            for (let student of students_data) {
               let student_value = {
                  id: student.id,
                  first_name: student.first_name,
                  last_name: student.last_name,
                  major: student.major
               };
               mylist.add(student_value);
            }
         }

         function init() {
            initDatabase();
            mylist = new List('student_list', options);
            updateStudentsTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            const add_ok = document.getElementById('add_ok');
            add_ok.addEventListener('click', handleAdd);
         }

      </script>
   </head>
   <body>
      <button id="add_button">Add student</button><br>
      <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>
      <div id="student_list">
         <input class="search"> Search<br>
         <table border="1">
            <thead>
               <tr>
                  <th>
                     <button class="sort" data-sort="id">id</button>
                  <th>
                     <button class="sort" data-sort="first_name">First Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="last_name">Last Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="major">Major</button>
                  </th>
               </tr>
            </thead>
            <tbody class="list">
               <tr>
                  <td class="id"></td>
                  <td class="first_name"></td>
                  <td class="last_name"></td>
                  <td class="major"></td>
               </tr>
            </tbody>
         </table>
      </div>
   </body>
</html>

Lines 4-9 add in the CSS styling needed to make the buttons in the table header have a little nicer appearance. Here is a screen shot of the application after a few students have been added:

dexie list table1

Here is another screen shot that shows a search on the letter 'b':

dexie list table2

Adding edit and delete

In order to add events to trigger an edit or a delete, you need to keep in mind that List.js is taking care of displaying the table (or list). So, you cannot add markup to the table elements and attach event listeners to the HTML markup. This is because the table is drawn by List.js, so the table rows are not accessible using HTML markup.

The template that we place in the <tbody> element is just a template. We cannot attach any events to elements in the template, because these events will not be associated with any of the data rows being displayed. Instead, we need to attach events to the elements drawn as part of mylist, the List object. Here is the new code that starts this process.

DexiePlusList.html
<!DOCTYPE html>
<html>
   <head>
      <style>
         thead button {
            border: none;
            font-weight: bold;
         }
      </style>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script src="//cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let students_data = [];
         let options = {
            valueNames: [ 'id', 'first_name', 'last_name', 'major' ]
         };
         let mylist;

         function initDatabase() {
            if (db === undefined) {
               db = new Dexie("Test database");
               db.version(1).stores({
                  students: `++id,
                     first_name,
                     last_name,
                     major`
               });
            }
         }

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

         function handleAdd() {
            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();
         }

         async function showEditDialog(event) {
            console.log(event.target);
            const row = event.target.parentNode.parentNode;
            const id = row.getElementsByClassName('id')[0].textContent;
            const student = await mylist.get('id',id)[0]._values;
            console.log(student);
         }

         async function updateStudentsTable() {
            students_data = await db.students.toArray();
            mylist.clear();
            for (let student of students_data) {
               let student_value = {
                  id: student.id,
                  first_name: student.first_name,
                  last_name: student.last_name,
                  major: student.major
               };
               mylist.add(student_value);
            }
         }

         async function init() {
            initDatabase();
            mylist = new List('student_list', options);
            await updateStudentsTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            const add_ok = document.getElementById('add_ok');
            add_ok.addEventListener('click', handleAdd);
            for (let i = 0; i < mylist.items.length; i++) {
               if (i == 0) {
                  console.log(mylist.items[i]);
               }
               mylist.items[i].elm.cells[4].addEventListener('click', showEditDialog);
            }
         }

      </script>
   </head>
   <body>
      <button id="add_button">Add student</button><br>
      <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>
      <div id="student_list">
         <!--<input class="search"> Search<br>-->
         <table border="1">
            <thead>
               <tr>
                  <th>
                     <button class="sort" data-sort="id">id</button>
                  <th>
                     <button class="sort" data-sort="first_name">First Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="last_name">Last Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="major">Major</button>
                  </th>
                  <th colspan="2">
                     <input type="text" class="search" placeholder="Search">
                  </th>
               </tr>
            </thead>
            <tbody class="list">
               <tr>
                  <td class="id"></td>
                  <td class="first_name"></td>
                  <td class="last_name"></td>
                  <td class="major"></td>
                  <td class="edit"><button id="edit_button">Edit</button></td>
                  <td class="remove"><button id="remove_button">Delete</button></td>
               </tr>
            </tbody>
         </table>
      </div>
   </body>
</html>

The new or modified lines are lines 51-57, 73, 76, 81-86, 105, 120-122 and 131-132. Lines 131 and 132 just add the Edit and Delete button to template inside of <tbody>. Note that the id attributes are not directly accessible in the table. This is because this is just the template, not the actual table rows drawn by mylist.

Line 105 comments out the line that added the search to the List object. Lines 120, put that search box back in as part of the table header.

Line 73 changes the init() function to be an async function. This is so on line 76 we can await the updateStudentsTable() call. This is so the for loop on lines 81-86 is only called once. This for loop on lines 81-86 is the important part of adding event handlers to the Edit buttons inside the table rows. Line 81 makes the for loop go from 0 up to mylist.items' length. mylist.items is an array of the items in mylist. That is, these are the actual table rows that are drawn. Lines 82-84 are used for debugging purposes. We want to know what mylist.items[0] looks like so we can figure out what we need to attach the event handler too. Line 85 actually attaches the event handler. But, to see how this makes sense, we can look at some screen shots of the console when this program starts up.

In the console, the variable mylist will be the list that is created on line 75 of the code. So, in the console, we can start by typing mylist. Then, as shown next, we can expand this object in the console:

mylist items

As you can see, the items property is the array that holds the 5 rows of data from the table. We can take a look at just one of those items by typing mylist.items[0] in the console:

mylist items 0

As you can see, upon expansion of mylist.items[0], the property elm is the <tr> element. If we now type mylist.items[0].elm in the console, we will see this:

mylist items 0 elm

If you expand on mylist.items[0], you can then expand on the elm element and see that the property we want to look at is cells. The next screen shot shows what mylist.items[0].elm.cells looks like:

mylist items 0 elm cells

This last screen shot shows that to get a reference to the cell for editing, you would use the following:

mylist.items[0].elm.cells[4]

So, in the for loop shown in the code, this is why line 85 shows this:

mylist.items[i].elm.cells[4]

so that each of the edit cells in the table has the showEditDialog function attached to clicking on that cell.

Now that we have attached the event handler as being the showEditDialog() function, lines 51-57 define the showEditDialog() function. Line 52 prints the event.target to the console. That would show this:

<button id="edit_button">Edit</button>

This explains line 53. The Edit button is inside a <td> element, and that <td> element is inside a <tr> element. So, to get to the row, we need to get to the parent of the parent of the Edit button. Line 54 gets the id by getting the array of elements with "id" as the class, and selecting the first item in the array. There is only one item, but we still need to treat it as an array. Finally, we use the textContent property to get the content of that <td> element. Line 55 uses await to get the student from mylist with the corresponding id. Line 56 just prints out the student object returned. If the Edit button in the first table row of data is clicked on, you would see this:

{id: 1, first_name: 'Jane', last_name: 'Doe', major: 'ICS'}

So, we can now test that the correct student is obtained by clicking on any of the Edit buttons in the displayed table. Since this is working, we can create and edit dialog box that will allow us to edit a student. Here is the next version of the code:

DexiePlusList.html
<!DOCTYPE html>
<html>
   <head>
      <style>
         thead button {
            border: none;
            font-weight: bold;
         }
      </style>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script src="//cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let students_data = [];
         let options = {
            valueNames: [ 'id', 'first_name', 'last_name', 'major' ]
         };
         let mylist;
         let saved_student_id;

         function initDatabase() {
            if (db === undefined) {
               db = new Dexie("Test database");
               db.version(1).stores({
                  students: `++id,
                     first_name,
                     last_name,
                     major`
               });
            }
         }

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

         function handleAdd() {
            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();
         }

         async function showEditDialog(event) {
            //console.log(event.target);
            const row = event.target.parentNode.parentNode;
            const id = row.getElementsByClassName('id')[0].textContent;
            saved_student_id = Number(id);
            const student = await mylist.get('id',id)[0]._values;
            //console.log(student);
            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');
            first_name_edit_box.value = student.first_name;
            last_name_edit_box.value = student.last_name;
            major_edit_box.value = student.major;
            const edit_dialog = document.getElementById('edit_dialog');
            edit_dialog.showModal();
         }

         async function handleEdit() {
            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();
            const edit_dialog = document.getElementById('edit_dialog');
            edit_dialog.close();
         }

         async function updateStudentsTable() {
            students_data = await db.students.toArray();
            mylist.clear();
            for (let student of students_data) {
               let student_value = {
                  id: student.id,
                  first_name: student.first_name,
                  last_name: student.last_name,
                  major: student.major
               };
               mylist.add(student_value);
            }
            for (let i = 0; i < mylist.items.length; i++) {
               mylist.items[i].elm.cells[4].addEventListener('click', showEditDialog);
            }
         }

         async function init() {
            initDatabase();
            mylist = new List('student_list', options);
            await updateStudentsTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            const add_ok = document.getElementById('add_ok');
            add_ok.addEventListener('click', handleAdd);
            const edit_ok = document.getElementById('edit_ok');
            edit_ok.addEventListener('click', handleEdit);
            /*
            for (let i = 0; i < mylist.items.length; i++) {
               if (i == 0) {
                  console.log(mylist.items[i]);
               }
               mylist.items[i].elm.cells[4].addEventListener('click', showEditDialog);
            }
            */
         }

      </script>
   </head>
   <body>
      <button id="add_button">Add student</button><br>
      <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>
      <div id="student_list">
         <table border="1">
            <thead>
               <tr>
                  <th>
                     <button class="sort" data-sort="id">id</button>
                  <th>
                     <button class="sort" data-sort="first_name">First Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="last_name">Last Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="major">Major</button>
                  </th>
                  <th colspan="2">
                     <input type="text" class="search" placeholder="Search">
                  </th>
               </tr>
            </thead>
            <tbody class="list">
               <tr>
                  <td class="id"></td>
                  <td class="first_name"></td>
                  <td class="last_name"></td>
                  <td class="major"></td>
                  <td class="edit"><button id="edit_button">Edit</button></td>
                  <td class="remove"><button id="remove_button">Delete</button></td>
               </tr>
            </tbody>
         </table>
      </div>
   </body>
</html>

The new or modified lines are lines 20, 53, 56-66, 69-81, 95-97 and 110-117. Line 20 adds another document level variable used to hold the student’s id when editing or deleting a student. Lines 53 and 58 comment out console.log statements that were used for debugging purposes.

Line 56 saves the student’s id so that when we update the Dexie database we have that id available. Note that the id must be converted to a number to work with the Dexie database. Line 57 uses await to wait for the getting the student from the Dexie database. We need to wait for this so the values are available for use on lines 62-64.

Lines 59-61 get references to the input text boxes inside the edit dialog. Lines 62-64 set the values of those boxes using the student values obtained on line 57. Lines 65 and 66 get a reference to the edit dialog and show it as a modal dialog.

Lines 69-81 define the function handleEdit(). Note that this function is declared async so that we can use await on lines 76-77. Lines 76 and 77 will make the program pause until the student object has been updated in the Dexie database before updateStudentsTable() is called on line 78.

Lines 70-75 just get references to the input text boxes in the edit dialog and get the values from those input boxes.

Lines 79 and 80 just get a reference to the edit dialog and close that dialog box.

Lines 95-97 are an important change to the updateStudentsTable() function. These lines add the event handler to the Edit button in mylist. This has to be done in the updateStudentsTable() function. That will make it so that the event handlers are attached every time mylist is updated. So, lines 110-117 just comment that for loop from the init() function. Otherwise, we would lose the event handlers for the Edit button each time we update the table. Failing to do this would make it so that the Edit buttons would only work until mylist is updated.

Let’s clean up the code by removing all the commented out lines. Here is the code with the comments removed.

DexiePlusList.html
<!DOCTYPE html>
<html>
   <head>
      <style>
         thead button {
            border: none;
            font-weight: bold;
         }
      </style>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script src="//cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let students_data = [];
         let options = {
            valueNames: [ 'id', 'first_name', 'last_name', 'major' ]
         };
         let mylist;
         let saved_student_id;

         function initDatabase() {
            if (db === undefined) {
               db = new Dexie("Test database");
               db.version(1).stores({
                  students: `++id,
                     first_name,
                     last_name,
                     major`
               });
            }
         }

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

         function handleAdd() {
            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();
         }

         async function showEditDialog(event) {
            const row = event.target.parentNode.parentNode;
            const id = row.getElementsByClassName('id')[0].textContent;
            saved_student_id = Number(id);
            const student = await mylist.get('id',id)[0]._values;
            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');
            first_name_edit_box.value = student.first_name;
            last_name_edit_box.value = student.last_name;
            major_edit_box.value = student.major;
            const edit_dialog = document.getElementById('edit_dialog');
            edit_dialog.showModal();
         }

         async function handleEdit() {
            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();
            const edit_dialog = document.getElementById('edit_dialog');
            edit_dialog.close();
         }

         async function updateStudentsTable() {
            students_data = await db.students.toArray();
            mylist.clear();
            for (let student of students_data) {
               let student_value = {
                  id: student.id,
                  first_name: student.first_name,
                  last_name: student.last_name,
                  major: student.major
               };
               mylist.add(student_value);
            }
            for (let i = 0; i < mylist.items.length; i++) {
               mylist.items[i].elm.cells[4].addEventListener('click', showEditDialog);
            }
         }

         async function init() {
            initDatabase();
            mylist = new List('student_list', options);
            await updateStudentsTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            const add_ok = document.getElementById('add_ok');
            add_ok.addEventListener('click', handleAdd);
            const edit_ok = document.getElementById('edit_ok');
            edit_ok.addEventListener('click', handleEdit);
         }

      </script>
   </head>
   <body>
      <button id="add_button">Add student</button><br>
      <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>
      <div id="student_list">
         <table border="1">
            <thead>
               <tr>
                  <th>
                     <button class="sort" data-sort="id">id</button>
                  <th>
                     <button class="sort" data-sort="first_name">First Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="last_name">Last Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="major">Major</button>
                  </th>
                  <th colspan="2">
                     <input type="text" class="search" placeholder="Search">
                  </th>
               </tr>
            </thead>
            <tbody class="list">
               <tr>
                  <td class="id"></td>
                  <td class="first_name"></td>
                  <td class="last_name"></td>
                  <td class="major"></td>
                  <td class="edit"><button id="edit_button">Edit</button></td>
                  <td class="remove"><button id="remove_button">Delete</button></td>
               </tr>
            </tbody>
         </table>
      </div>
   </body>
</html>

Enabling delete

Now that we have figured out how to edit a student, it should be relatively straightforward to delete a student. Here is what we need to do:

  • Add markup for a delete dialog. This dialog will allow the user to confirm or cancel the delete.

  • Create a handler for clicking on the Delete button.

  • Modify the for loop in the updateStudentsTable() function so that it attaches the event handler to the Delete button.

  • Add handlers for clicking on the delete_ok and delete_cancel buttons inside the delete dialog.

Here is the new code:

DexiePlusList.html
<!DOCTYPE html>
<html>
   <head>
      <style>
         thead button {
            border: none;
            font-weight: bold;
         }
      </style>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <script src="//cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let students_data = [];
         let options = {
            valueNames: [ 'id', 'first_name', 'last_name', 'major' ]
         };
         let mylist;
         let saved_student_id;

         function initDatabase() {
            if (db === undefined) {
               db = new Dexie("Test database");
               db.version(1).stores({
                  students: `++id,
                     first_name,
                     last_name,
                     major`
               });
            }
         }

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

         function handleAdd() {
            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();
         }

         async function showEditDialog(event) {
            const row = event.target.parentNode.parentNode;
            const id = row.getElementsByClassName('id')[0].textContent;
            saved_student_id = Number(id);
            const student = await mylist.get('id',id)[0]._values;
            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');
            first_name_edit_box.value = student.first_name;
            last_name_edit_box.value = student.last_name;
            major_edit_box.value = student.major;
            const edit_dialog = document.getElementById('edit_dialog');
            edit_dialog.showModal();
         }

         async function handleEdit() {
            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();
            const edit_dialog = document.getElementById('edit_dialog');
            edit_dialog.close();
         }

         async function showDeleteDialog(event) {
            const row = event.target.parentNode.parentNode;
            const id = row.getElementsByClassName('id')[0].textContent;
            saved_student_id = Number(id);
            const student = await mylist.get('id', id)[0]._values;
            const student_to_delete = document.getElementById('student_to_delete');
            student_to_delete.textContent = student.first_name + " " +
               student.last_name + "?";
            const delete_dialog = document.getElementById('delete_dialog');
            delete_dialog.showModal();
         }

         async function handleDelete() {
            await db.students.delete(saved_student_id);
            updateStudentsTable();
            document.getElementById('delete_dialog').close();
         }

         async function updateStudentsTable() {
            students_data = await db.students.toArray();
            mylist.clear();
            for (let student of students_data) {
               let student_value = {
                  id: student.id,
                  first_name: student.first_name,
                  last_name: student.last_name,
                  major: student.major
               };
               mylist.add(student_value);
            }
            for (let i = 0; i < mylist.items.length; i++) {
               mylist.items[i].elm.cells[4].addEventListener('click', showEditDialog);
               mylist.items[i].elm.cells[5].addEventListener('click', showDeleteDialog);
            }
         }

         async function init() {
            initDatabase();
            mylist = new List('student_list', options);
            await updateStudentsTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            const add_ok = document.getElementById('add_ok');
            add_ok.addEventListener('click', handleAdd);
            const edit_ok = document.getElementById('edit_ok');
            edit_ok.addEventListener('click', handleEdit);
            const delete_ok = document.getElementById('delete_ok');
            delete_ok.addEventListener('click', handleDelete);
            const delete_cancel = document.getElementById('delete_cancel');
            delete_cancel.addEventListener('click', () => {
               document.getElementById('delete_dialog').close();
            });
         }

      </script>
   </head>
   <body>
      <button id="add_button">Add student</button><br>
      <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>
      <dialog id="delete_dialog">
         Delete <label id="student_to_delete"></label>
         <br><br>
         <button id="delete_cancel">Cancel</button>
         <button id="delete_ok">Ok</button>
      </dialog>
      <div id="student_list">
         <table border="1">
            <thead>
               <tr>
                  <th>
                     <button class="sort" data-sort="id">id</button>
                  <th>
                     <button class="sort" data-sort="first_name">First Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="last_name">Last Name</button>
                  </th>
                  <th>
                     <button class="sort" data-sort="major">Major</button>
                  </th>
                  <th colspan="2">
                     <input type="text" class="search" placeholder="Search">
                  </th>
               </tr>
            </thead>
            <tbody class="list">
               <tr>
                  <td class="id"></td>
                  <td class="first_name"></td>
                  <td class="last_name"></td>
                  <td class="major"></td>
                  <td class="edit"><button id="edit_button">Edit</button></td>
                  <td class="remove"><button id="remove_button">Delete</button></td>
               </tr>
            </tbody>
         </table>
      </div>
   </body>
</html>

The new lines are lines 81-91, 93-97, 113, 127-132 and 161-166.

Lines 81-91 define the showDeleteDialog() function. This is the function that will be displayed when the user hits the Delete button in the table. Line 82 gets the table row that contains the Delete button in the table. This is just like line 53 for the showEditDialog() function. Line 83 gets the id for that student. Line 84 converts this id into a number and stores it in saved_student_id. Line 85 uses await to wait for returning the correct student object from mylist. After line 85 completes we get a reference the the <label> element inside the delete dialog. The markup for this <label> element is on line 162. Lines 87-88 fill in that <label> with the student’s first name last name and a "?". Lines 89-90 get a reference to the delete dialog and the show it as a modal dialog.

Lines 93-97 define the handleDelete() function. This function is called when the user hits the Ok button in the delete dialog. Line 94 waits for the delete process in the Dexie database completes. Then line 95 updates the table, updating mylist. Line 96 closes the delete dialog.

Line 113 is the line that attaches the Delete button in the table rows to the showDeleteDialog() function. Note that we tie this action to cells[5], as that is the Delete button. See the following screen shot from earlier that shows this. The "td.remove" is that Delete button.

mylist items 0 elm cells

Lines 127-132 update the init() function. Lines 127 and 128 get a reference to the Ok button in the delete dialog and attach the handleDelete() function to clicking on that button. Line 129 gets a reference to the Cancel button in the delete dialog. Lines 130-132 attach an anonymous function to the event of clicking on that Cancel button. That anonymous function will just close the delete dialog box.

Lines 161-166 provide the markup for the delete dialog box. Line 162 creates a prompt inside that dialog box. The <label> element is filled in when the showDeleteDialog() function runs. Lines 164 and 165 provide the Cancel and Ok buttons for the delete dialog. When the program runs, this is what the delete dialog looks like if the user click on Delete for John Smith:

delete dialog john

A short review

Let’s make a short review of the important processes when writing a program that uses Dexie.js for a persistent database and List.js to provide a view of the contents of that database:

  • A List object called mylist is constructed from the markup that contains the list or table that we want to display. This means that changes to mylist are what changes the table that is displayed.

  • A Dexie database table is created to persistently store the records within the client’s browser. Whenever we want to make changes to the data, we need to first make those changes to the Dexie database. Then, we call updateStudentsTable() to clear mylist and then make the appropriate additions to mylist.

  • We don’t make changes to mylist directly or the Dexie database can’t be maintained. We make the changes in Dexie and then recreate mylist.

  • Since the table that is displayed is directly tied to mylist, we need to attach event handlers for buttons like the Edit and Delete button to the objects in mylist. This needs to be performed every time that mylist is updated.

  • The id for a student obtained through the edit or delete button handling is text. This is fine for getting a student from mylist. But this id must be converted to a number before performing any queries on the Dexie database.

  • When an id is obtained through any of the show dialog functions (showEditDialog() or showDeleteDialog()), you need to save the student’s id as a document level variable. This makes the id available to the functions that need this id to perform an edit (handleEdit()) or delete (handleDelete()).