Using Tabulator

Using Tabulator

Using Tabulator for tables

Tabulator is an open source JavaScript library that can display tables like List.js. Here is a link to the Tabulator Documentation.

Tabulator

Tabulator is a much bigger library than List.js and can do more things in displaying the data. So, if all you need for an application to do is display the data in a table, be able to search and sort the table, and provide basic CRUD functionality, List.js may be all that you need. But, if you need something fancier, and or something that can be used as a React component, then Tabulator may be a good choice. Although Tabulator used to have a jQuery dependence, it no longer uses jQuery for the main components. So, if you are trying to avoid relying on jQuery, Tabulator may be a better choice than something like DataTables. Also there are parts of DataTables (the editor) that are not free.

A simple example using Tabulator

Here is a page, "showTabulator.html" that is a simple example of making use of Tabulator. This is taken largely from the documentation for a Quickstart for Tabulator. The changes made are to set up for using a Dexie database as the underlying data storage. But, this example does not use Dexie and just hardcodes the data for simplicity.

showTabulator.html
<!DOCTYPE html>
<html>
   <head>
      <link href="https://unpkg.com/tabulator-tables@6.2.1/dist/css/tabulator.min.css" rel="stylesheet">
      <script src="https://unpkg.com/tabulator-tables@6.2.1/dist/js/tabulator.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/luxon/3.4.4/luxon.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let table;
         let tabledata = {};

         function createTabulator() {
            if (table === undefined) {
               table = new Tabulator("#example-table", {
                  height: 205,
                  data: tabledata,
                  layout: "fitColumns",
                  columns: [
                     {title: "Name", field: "name", width: 150},
                     {title: "Age", field: "age", hozAlign: "left", formatter: "progress"},
                     {title: "Favorite color", field: "col"},
                     {title: "Date of Birth", field: "dob", sorter: "date", hozAlign: "center" },
                  ],
               });
            }
         }

         function updateTable() {
            table.on("rowClick", (event, row) => {
               alert("Row data " + JSON.stringify(row.getData()));
               console.log(event.target);
            });
         }

         function loadData() {
            tabledata = [
            {id:1, name:"Oli Bob", age:"12", col:"red", dob:"08/12/2017"},
            {id:2, name:"Mary May", age:"1", col:"blue", dob:"05/14/1982"},
            {id:3, name:"Christine Lobowski", age:"42", col:"green", dob:"05/22/1982"},
            {id:4, name:"Brendon Philips", age:"125", col:"orange", dob:"08/01/1980"},
            {id:5, name:"Margret Marmajuke", age:"16", col:"yellow", dob:"01/31/1999"},
            ];
         }
         function init() {
            loadData();
            createTabulator();
            updateTable();
         }
      </script>
   </head>
   <body>
      <div id="example-table"></div>
   </body>
</html>

Lines 3 and 4 include the CSS and JavaScript needed to use the Tabulator library. Line 6 includes the JavaScript for using the Luxon DateTime objects. This was something that was not obvious in the Quickstart documentation as it did not show the HTML for their example.

Line 8 makes it so that the init() function is called when the document finishes loading. Lines 9 and 10 define document level variables that will be used in more than one function. The variable table will be the Tabulator object, and tabledata is where the data for the Tabulator will be stored within the program.

Lines 12-26 define the createTabulator() function. This will create the Tabulator object called table. If table is already defined, this function won’t do anything. There are several properties that are used for table. On line 15, the height is specified. This is the height of what will be displayed for the table. If the number of rows would not fit in that height, a scroll bar is used so that you can scroll down in the table. Tabulator only renders what is visible, so if you have a large amount of data, setting a height like this will make the rendering much quicker. Line 16 specifies the data to use for table. Line 17 sets the layout so that the columns resize to fit the width of the data. At the same time, Tabulator will by default make it so that the table fits the width of the browser window. Lines 18-23 specify the columns for the table. Line 19 sets the first column with a header of "Name" and uses the data field name. It also sets the column width for name to be 150 pixels. Line 20 sets the second column with a header of "Age" and uses the data field age. It also sets the text to be left-aligned and formats the data as "progress" (as in progress bar). Line 21 sets the third column to have a header of "Favorite color" and uses the data field, col. Finally, line 22 sets the fourth column header to "Date of Birth" and the data field to be, dob. It sets the sorter property to be "date" so it assumes that Luxon DateTime objects are used for the dates. This line also specifies that the dates will be center-aligned in the table cell.

Lines 28-33 define the updateTable() function. This should be called everytime the data is changed. Lines 29-32 attach an anonymous event handler to the rowClick event. Line 30 displays an alert using the data in that row. So, row.getData() gets a JavaScript object of the data stored in that row. Line 31 displays event.target. When you run the program, you can see how using row.getData() is needed to get the actual data, as it is difficult at best to get that information from the event object. So, having something like row.getData() makes things much easier. This is a nice feature of Tabulator.

Lines 35-43 define the loadData() function. This is meant to be run once inside of init() to set the values for the data table. In a more complex program, getting the actual data from a source like a Dexie database should be performed in the updateTable() function. If you look at the example in the Tabulator documentation, I have changed the format of the dates to MM/DD/YYYY. The example in the Tabulator documentation uses DD/MM/YYYY, which is what much of the world outside the US uses.

Lines 44-48 define the init() function. This loads the data into the tabledata variable, creates the Tabulator object (table) and then calls updateTable() to attach the event handlers to the displayed table.

Line 52 is the one line of markup needed to make a place for the Tabulator object to be displayed. Here is a screenshot of the table after clicking on the Age column to sort by age:

simple sort by age

Note how the age is displayed as a progress bar. Next we can look at a screenshot that shows the alert shown when we click on row with Margret Marmajuke in it:

simple alert margret

After dismissing the alert box, this is what the console shows for event.target:

simple event target

This shows that the column clicked was the Age column as it shows the green color for that cell. If we had clicked in another column, the data would be different. If we had chosen to display event.currentTarget that would be a complex element to pick the data from. So, row.getData() is a very useful function.

A more complex example

Now that we have had a look at a simple example of using Tabulator, let’s try to use it to create a simple CRUD application. Just as we did with List.js let’s use a Dexie database to create a persistent form of data storage. Here is the starting code for TranslatorPlusDexie.html:

TranslatorPlusDexie.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <link href="https://unpkg.com/tabulator-tables@6.2.1/dist/css/tabulator.min.css" rel="stylesheet">
      <script src="https://unpkg.com/tabulator-tables@6.2.1/dist/js/tabulator.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let table;
         let students_data = [];

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

         function createTabulator() {
            if (table === undefined) {
               table = new Tabulator("#students_table", {
                  height: 205,
                  data: students_data,
                  layout: "fitColumns",
                  responsiveLayout: "hide",
                  columns: [
                     {title: "id", field: "id"},
                     {title: "First Name", field: "first_name"},
                     {title: "Last Name", field: "last_name"},
                     {title: "Major", field: "major"},
                  ],
               });
            }
         }

         async function updateTable() {
            students_data = await db.students.toArray();
            table.addData(students_data);
            table.on("rowClick", (event, row) => {
               console.log(JSON.stringify(row.getData()));
            });
         }

         function init() {
            initDatabase();
            createTabulator();
            updateTable();
         }
      </script>
   </head>
   <body>
      <div id="students_table"></div>
   </body>
</html>

Line 4 allows us to use Dexie.js. Lines 5 and 6 give us access to use Tabulator styles and the basic Tabulator capabilities. As usual, line 8 makes it so that the init() function is called when the DOM finished loading. Line 9 defines a variable called db that will be a reference to the Dexie database. Line 10 defines a variable called table that will be a reference to the Tabulator object. Line 11 defines students_data as an array that will be used to hold the data that will be rendered by Tabulator.

Lines 13-23 define the initDatabase() function. This uses a Dexie database already stored on my client. That will give us some rows of data for the Tabulator table.

Lines 25-40 define the createTabulator() function. This will construct table if it is not already constructed. On line 27, we set Tabulator to use the <div> element with and id="students_table for displaying the table. Lines 28-37 define additional properties for displaying the table. Line 28 sets a height of 205 pixels. As mentioned earlier, this speeds up the rendering of the table in case you have a lot of data. Line 29 sets the data source for the table. Lines 30 and 31 together make for a table that changes column widths nicely if the size of the window is changed. This could be important if your application runs on the desktop and also on a mobile phone. Lines 32-37 define the columns. At this point, we defined the columns as simply as possible.

Lines 42-49 define the updateTable() function. This should be called anytime there are changes made to the data. Line 43 gets the data from the source, the Dexie database. We use await to make sure the request completes before the next line of instruction. Line 44 uses the addData() function to add the data to the table. Lines 45-47 add handlers that respond to clicking on a row. Line 46 use the getData() function to get an object that holds all the data for the row that has been clicked on.

Lines 50-54 define the init() function. This just calls initDatabase(), then createTabulator() and finally updateTable().

Line 58 creates a <div> with an id="students_table to provide a place to draw the table.

The following screen shot shows the table and the Console after clicking on the first row:

tabulator dexie table1

Putting in the capability to add a student

Now that we can display data from a Dexie database, let’s put in the capability to add a student. This involves several steps:

  • Create a <dialog> element that contains input boxes for the first_name, last_name and major fields.

  • Add a button that will be clicked on to show the dialog for adding a student. Attach an event handler called showAddDialog() that will be called when clicking on the add button.

  • Create the showAddDialog() function that will display the add dialog.

  • Attach event handlers for the Cancel and Ok buttons inside the add dialog.

  • Create an event handler called handleAdd() that will be called when the Ok button in the add dialog is clicked. This function will update the Dexie database.

  • Modify the updateTable() function so that it clears the table (table.setData([])). This is done right after the existing line to await geting the data from the Dexie database. Then, the data can be added back in using the existing call to table.addData().

Here is the new code:

TranslatorPlusDexie.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <link href="https://unpkg.com/tabulator-tables@6.2.1/dist/css/tabulator.min.css" rel="stylesheet">
      <script src="https://unpkg.com/tabulator-tables@6.2.1/dist/js/tabulator.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let table;
         let students_data = [];

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

         function createTabulator() {
            if (table === undefined) {
               table = new Tabulator("#students_table", {
                  height: 205,
                  data: students_data,
                  layout: "fitColumns",
                  responsiveLayout: "hide",
                  columns: [
                     {title: "id", field: "id"},
                     {title: "First Name", field: "first_name"},
                     {title: "Last Name", field: "last_name"},
                     {title: "Major", field: "major"},
                  ],
               });
            }
         }

         function showAddDialog() {
            document.getElementById('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});
            updateTable();
            document.getElementById('add_dialog').close();
         }

         async function updateTable() {
            students_data = await db.students.toArray();
            table.setData([]);
            table.addData(students_data);
            table.on("rowClick", (event, row) => {
               console.log(JSON.stringify(row.getData()));
            });
         }

         function init() {
            initDatabase();
            createTabulator();
            updateTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            document.getElementById('add_ok').addEventListener('click', handleAdd);
            document.getElementById('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>
      <div id="students_table"></div>
   </body>
</html>

The new lines are lines 42-44, 46-56, 60, 71-76 and 81-92. Lines 42-44 define the showAddDialog() function. This is called when the user clicks on the Add button and is used to display the add dialog.

Lines 46-56 define the handleAdd() function. Lines 47-49 get references to the input text boxes for the first_name, last_name and major fields. Lines 50-52 store the input values for use in the call to db.students.add() on line 53. Line 54 then makes a call to the updateTable() function. Line 55 closes the add dialog box.

Line 60 clears the table for the data is added in on line 61. If you fail to clear the table before the data is added to the table, you will see all the old data twice.

Lines 71 and 72 get a reference to the add button and attach the showAddDialog() function as the handler when the add button is clicked. Line 72 makes handleAdd() the function that is called when the Ok button in the add dialog is clicked. Lines 74-75 make it so that when the user clicks on the Cancel button in the add dialog, the dialog box just closes.

Line 81 puts the add button in above the <dialog> element. Lines 82-92 define the <dialog> element used for the add dialog. Lines 83-88 add prompts and input text boxes for getting the first_name, last_name and major, respectively. Line 90 adds the Cancel button for the add dialog and line 91 adds the Ok button for the add dialog.

Editing a student

We could perform the editing in a manner similar to the way that we did it for the DexiePlusList project. But, Tabulator allows for inline editing with a small amount of setup code. So, that is how we will do editing. You could also add a button in each row that will be for editing any editable fields in that row. But, we will start with just the inline editing to show how this might be done.

Inline editing

To perform inline editing we need to make a change to the column definitions for the table. Then, we need to add a "rowClick" handler for the table rows. This handler will have to update the Dexie database, and then call the updateTable() function. Here is the modified code:

TranslatorPlusDexie.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <link href="https://unpkg.com/tabulator-tables@6.2.1/dist/css/tabulator.min.css" rel="stylesheet">
      <script src="https://unpkg.com/tabulator-tables@6.2.1/dist/js/tabulator.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let table;
         let students_data = [];

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

         function createTabulator() {
            if (table === undefined) {
               table = new Tabulator("#students_table", {
                  height: 205,
                  data: students_data,
                  layout: "fitColumns",
                  responsiveLayout: "hide",
                  columns: [
                     {title: "id", field: "id"},
                     {title: "First Name", field: "first_name", editor: "input"},
                     {title: "Last Name", field: "last_name", editor: "input"},
                     {title: "Major", field: "major", editor: "input"},
                  ],
               });
            }
         }

         function showAddDialog() {
            document.getElementById('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});
            updateTable();
            document.getElementById('add_dialog').close();
         }

         async function handleCell(cell) {
            const data = cell.getData();
            const id = Number(data.id);
            const fn = data.first_name;
            const ln = data.last_name;
            const maj = data.major;
            await db.students.update(id, {
               first_name: fn, last_name: ln, major: maj
            });
         }

         async function updateTable() {
            students_data = await db.students.toArray();
            table.setData([]);
            table.addData(students_data);
            table.on("rowClick", (event, row) => {
               console.log(JSON.stringify(row.getData()));
            });
            table.on("cellEdited", (cell) => {
               handleCell(cell);
            });
         }

         function init() {
            initDatabase();
            createTabulator();
            updateTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            document.getElementById('add_ok').addEventListener('click', handleAdd);
            document.getElementById('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>
      <div id="students_table"></div>
   </body>
</html>
The new or modified lines are lines 34-36, 58-67 and 76-78. Lines 34-36 modify the column definitions by adding the *editor* property with a value of *"input"*. There are other values that can be used for the *editor* property such as "list" when you want to use a drop-down list for choices the value can take on. You can look at the documentation for examples of the built-in editors that can be used: https://tabulator.info/docs/6.2/edit[Tabulator User Editing Data]. Just adding this property makes it so that you can edit the value in that column in place by clicking on it. But, to save the changes, more most be done.

To save the edit changes, lines 58-67 define the handleCell() function. This function is called when the table "cellEdited" event occurs. This event handler is attached on lines 76-78 inside the updateTable() function. Line 59 gets the data stored in the cell that was changed. Line 60 gets the id to be used to update the Dexie database. Lines 61-63 obtain the values that could have been changed. Lines 64-66 wait for the Dexie database to be updated with those values. This will save the changes to the Dexie database, so that those changes will persist.

Deleting a student

We will delete a student in a manner similar to what was done in the DexiePlusList project. That is we will create a dialog box that gives the users a chance to change their mind about deleting the student. But, we will select the row for deleting in a different manner. Here is the new code:

TranslatorPlusDexie.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <link href="https://unpkg.com/tabulator-tables@6.2.1/dist/css/tabulator.min.css" rel="stylesheet">
      <script src="https://unpkg.com/tabulator-tables@6.2.1/dist/js/tabulator.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let table;
         let students_data = [];
         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 createTabulator() {
            if (table === undefined) {
               table = new Tabulator("#students_table", {
                  height: 205,
                  data: students_data,
                  layout: "fitColumns",
                  responsiveLayout: "hide",
                  columns: [
                     {title: "id", field: "id"},
                     {title: "First Name", field: "first_name", editor: "input"},
                     {title: "Last Name", field: "last_name", editor: "input"},
                     {title: "Major", field: "major", editor: "input"},
                  ],
               });
            }
         }

         function showAddDialog() {
            document.getElementById('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});
            updateTable();
            document.getElementById('add_dialog').close();
         }

         async function handleCell(cell) {
            const data = cell.getData();
            const id = Number(data.id);
            const fn = data.first_name;
            const ln = data.last_name;
            const maj = data.major;
            await db.students.update(id, {
               first_name: fn, last_name: ln, major: maj
            });
         }

         function showDeleteDialog(row) {
            const data = row.getData();
            const delete_name = document.getElementById('delete_name');
            saved_student_id = Number(data.id);
            delete_name.textContent = data.first_name + " " + data.last_name;
            document.getElementById('delete_dialog').showModal();
         }

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

         async function updateTable() {
            students_data = await db.students.toArray();
            table.setData([]);
            table.addData(students_data);
            table.on("rowClick", (event, row) => {
               console.log(JSON.stringify(row.getData()));
            });
            table.on("cellEdited", (cell) => {
               handleCell(cell);
            });
            table.on("rowContext", (event, row) => {
               event.preventDefault(); //stop browser's context menu from showing
               showDeleteDialog(row);
            });
         }

         function init() {
            initDatabase();
            createTabulator();
            updateTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            document.getElementById('add_ok').addEventListener('click', handleAdd);
            document.getElementById('add_cancel').addEventListener('click', () =>{
               document.getElementById('add_dialog').close();
            });
            document.getElementById('delete_ok').addEventListener('click', handleDelete);
            document.getElementById('delete_cancel').addEventListener('click',
               () => { document.getElementById('delete_dialog').close(); });
         }
      </script>
   </head>
   <body>
      <button id="add_button">Add</button> &nbsp;&nbsp; Right-click row to delete
      <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="delete_dialog">
         Delete <label id="delete_name"></label>?
         <br><br>
         <button id="delete_cancel">Cancel</button>
         <button id="delete_ok">Ok</button>
      </dialog>
      <div id="students_table"></div>
   </body>
</html>

The new lines are 12, 70-76, 78-82, 94-97, 110-112, 117 and 129-134. Line 12 defines a document level variable called saved_student_id to hold the id of the student to be deleted. Lines 70-76 define the showDeleteDialog() function. Line 71 gets the data from the row that has been right-clicked on. Line 72 gets a reference to the label in the delete dialog. Line 73 saves the student’s id. Line 74 sets the <label> contents to the first and last name of the student. Finally, line 75 shows the delete dialog as a modal dialog.

Lines 78-82 define the handleDelete() function. This is the function called when the user click on the Ok button in the delete dialog. Line 79 waits for the student to be deleted from the Dexie database. Line 80 calls updateTable() to update the table. Line 81 just closes the delete dialog box.

Lines 94-97 attaches event handlers to the event of right-clicking on a row. Line 95 prevents the context-sensitive menu of the browser from popping up. Line 96 calls the showDeleteDialog() function.

Line 110 just makes it so that clicking on the Ok button in the delete dialog will call the function handleDelete(). Lines 111-112 will make it so that clicking on the Cancel button in the delete dialog, will just close the dialog without making any changes.

Line 117 was modified with the text: "&nbsp;&nbsp; Right-click row to delete". This will let the user know that right-clicking a row will delete that row. Lines 129-134 add the markup for the delete dialog. Line 130 will provide a prompt for the user asking if they want to Delete <student’s name>. Lines 132 and 133 add in the Cancel and Ok buttons respectively.

The following screen shot shows what the delete dialog looks like if the first row was right-clicked:

right click

The users can click Cancel if they change their mind, or click Ok to delete that user.

Using an Edit button

We can add an Edit button that uses a dialog box for editing. This would allow changing more than one of the fields at a time. Here is the new code that makes use of an edit button.

TranslatorPlusDexie.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <link href="https://unpkg.com/tabulator-tables@6.2.1/dist/css/tabulator.min.css" rel="stylesheet">
      <script src="https://unpkg.com/tabulator-tables@6.2.1/dist/js/tabulator.min.js"></script>
      <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet">
      <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/js/all.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let table;
         let students_data = [];
         let saved_student_id;

         let editIcon = function(cell, formatterParams) {
            return "<i class='fa-solid fa-pen-to-square'></i>";
         }

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

         function createTabulator() {
            if (table === undefined) {
               table = new Tabulator("#students_table", {
                  height: 205,
                  data: students_data,
                  layout: "fitColumns",
                  responsiveLayout: "hide",
                  columns: [
                     {title: "id", field: "id"},
                     {title: "First Name", field: "first_name", editor: "input"},
                     {title: "Last Name", field: "last_name", editor: "input"},
                     {title: "Major", field: "major", editor: "input"},
                     {title: "", headerSort: false, formatter: editIcon,
                        width: 40, hozAlign: "center",
                        cellClick: (e, cell) => { showEditDialog(cell);},
                     },
                  ],
               });
            }
         }

         function showAddDialog() {
            document.getElementById('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});
            updateTable();
            document.getElementById('add_dialog').close();
         }

         async function handleCell(cell) {
            const data = cell.getData();
            const id = Number(data.id);
            const fn = data.first_name;
            const ln = data.last_name;
            const maj = data.major;
            await db.students.update(id, {
               first_name: fn, last_name: ln, major: maj
            });
         }

         function showEditDialog(cell) {
            const data = cell.getRow().getData();
            saved_student_id = Number(data.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');
            first_name_edit_box.value = data.first_name;
            last_name_edit_box.value = data.last_name;
            major_edit_box.value = data.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();
            db.students.update( saved_student_id,
               { first_name: fn, last_name: ln, major: maj});
            updateTable();
            document.getElementById('edit_dialog').close();
         }

         function showDeleteDialog(row) {
            const data = row.getData();
            const delete_name = document.getElementById('delete_name');
            saved_student_id = Number(data.id);
            delete_name.textContent = data.first_name + " " + data.last_name;
            document.getElementById('delete_dialog').showModal();
         }

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

         async function updateTable() {
            students_data = await db.students.toArray();
            table.setData([]);
            table.addData(students_data);
            table.on("rowClick", (event, row) => {
               console.log(JSON.stringify(row.getData()));
            });
            table.on("cellEdited", (cell) => {
               handleCell(cell);
            });
            table.on("rowContext", (event, row) => {
               event.preventDefault(); //stop browser's context menu from showing
               showDeleteDialog(row);
            });
         }

         function init() {
            initDatabase();
            createTabulator();
            updateTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            document.getElementById('add_ok').addEventListener('click', handleAdd);
            document.getElementById('add_cancel').addEventListener('click', () =>{
               document.getElementById('add_dialog').close();
            });
            document.getElementById('edit_ok').addEventListener('click', handleEdit);
            document.getElementById('edit_cancel').addEventListener('click',
               () => { document.getElementById('edit_dialog').close(); });
            document.getElementById('delete_ok').addEventListener('click', handleDelete);
            document.getElementById('delete_cancel').addEventListener('click',
               () => { document.getElementById('delete_dialog').close(); });
         }
      </script>
   </head>
   <body>
      <button id="add_button">Add</button>    Right-click row to delete
      <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="delete_name"></label>?
         <br><br>
         <button id="delete_cancel">Cancel</button>
         <button id="delete_ok">Ok</button>
      </dialog>
      <div id="students_table"></div>
   </body>
</html>

The new lines are lines 7-8, 16-18, 44-47, 80-91, 93-104, 146-148 and 168-178. Lines 7 and 8 enable the use of FontAwesome icons. This is a relatively simple way of including icons for buttons. There are other ways of including icons that use SVG images. Using SVG images removes dependencies on libraries, but they can make the code look messier. But, that is something that you may find it worth investigating.

Lines 16-18 define a function called editIcon that will be used as a formatter for the edit button. Note on line 17, that a FontAwesome icon is used.

Lines 44-47 define a column that will hold the edit button for that row. Setting headerSort to false will make it so that sorting by that column is not allowed. Note that the formatter has been set to the editIcon function defined on lines 16-18. Setting the width to 40 will keep the column just wide enough to hold the edit button. The cellClick property is set to an anonymous function that just calls the showEditDialog() with the cell information as an argument. The cell information contains a row and you can call row.getData() to get the data in the row for editing.

Lines 80-91 define the showEditDialog() function that is called when the edit button in the table is clicked. Line 81 gets the data from the row that contains the cell. Line 82 saves the id in the saved_student_id variable as that will be needed when we update the Dexie database. Lines 83-85 just get references to the input text boxes so that lines 86-88 can put in the current values for those fields. Lines 89-90 get a reference to the edit dialog box and show that as a modal dialog box.

Lines 93-104 define the handleEdit() function that is called when the user hits Ok in the edit dialog. Lines 94-96 get references to the input text boxes and lines 97-99 save the values the user has input. Lines 100-101 update the Dexie database. Line 102 calls the updateTable() function to update the displayed table. Then, line 103 closes the edit dialog box.

Line 146 makes it so that the handleEdit() function is called when the user hits the Ok button in the edit dialog. Lines 147-148 make it so that if the user hits the Cancel button in the edit dialog, the edit dialog will close without making any changes.

Lines 168-178 add the markup for the edit dialog box. Lines 169-174 add the prompts and input text boxes for all of the fields. Line 176 puts in the Cancel button for the edit dialog, and line 177 puts in the Ok button for the edit dialog.

Here is a screen shot showing the edit dialog box when the user hits the edit button for the row containing John Smith:

edit john smith

To save any changes, the user would click Ok. Otherwise, hitting Cancel will close the edit dialog box without saving any changes.

Putting in a delete button

We can add a delete button alongside the edit button. Here is the new code that does that.

TranslatorPlusDexie.html
<!DOCTYPE html>
<html>
   <head>
      <script src="https://unpkg.com/dexie/dist/dexie.js"></script>
      <link href="https://unpkg.com/tabulator-tables@6.2.1/dist/css/tabulator.min.css" rel="stylesheet">
      <script src="https://unpkg.com/tabulator-tables@6.2.1/dist/js/tabulator.min.js"></script>
      <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet">
      <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/js/all.min.js"></script>
      <script>
         document.addEventListener('DOMContentLoaded', init);
         let db;
         let table;
         let students_data = [];
         let saved_student_id;

         let editIcon = function(cell, formatterParams) {
            return "<i class='fa-solid fa-pen-to-square'></i>";
         }

         let trashIcon = function(cell, formatterParams) {
            return "<i class='fa-regular fa-trash-can'></i>";
         }

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

         function createTabulator() {
            if (table === undefined) {
               table = new Tabulator("#students_table", {
                  height: 205,
                  data: students_data,
                  layout: "fitColumns",
                  responsiveLayout: "hide",
                  columns: [
                     {title: "id", field: "id"},
                     {title: "First Name", field: "first_name", editor: "input"},
                     {title: "Last Name", field: "last_name", editor: "input"},
                     {title: "Major", field: "major", editor: "input"},
                     {title: "", headerSort: false, formatter: editIcon,
                        width: 40, hozAlign: "center",
                        cellClick: (e, cell) => { showEditDialog(cell);},
                     },
                     {title: "", headerSort: false, formatter: trashIcon,
                        width: 40, hozAlign: "center",
                        cellClick: (e, cell) => {
                           showDeleteDialog(cell.getRow());
                        },
                     },

                  ],
               });
            }
         }

         function showAddDialog() {
            document.getElementById('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});
            updateTable();
            document.getElementById('add_dialog').close();
         }

         async function handleCell(cell) {
            const data = cell.getData();
            const id = Number(data.id);
            const fn = data.first_name;
            const ln = data.last_name;
            const maj = data.major;
            await db.students.update(id, {
               first_name: fn, last_name: ln, major: maj
            });
         }

         function showEditDialog(cell) {
            const data = cell.getRow().getData();
            saved_student_id = Number(data.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');
            first_name_edit_box.value = data.first_name;
            last_name_edit_box.value = data.last_name;
            major_edit_box.value = data.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();
            db.students.update( saved_student_id,
               { first_name: fn, last_name: ln, major: maj});
            updateTable();
            document.getElementById('edit_dialog').close();
         }

         function showDeleteDialog(row) {
            const data = row.getData();
            const delete_name = document.getElementById('delete_name');
            saved_student_id = Number(data.id);
            delete_name.textContent = data.first_name + " " + data.last_name;
            document.getElementById('delete_dialog').showModal();
         }

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

         async function updateTable() {
            students_data = await db.students.toArray();
            table.setData([]);
            table.addData(students_data);
            table.on("rowClick", (event, row) => {
               console.log(JSON.stringify(row.getData()));
            });
            table.on("cellEdited", (cell) => {
               handleCell(cell);
            });
            table.on("rowContext", (event, row) => {
               event.preventDefault(); //stop browser's context menu from showing
               showDeleteDialog(row);
            });
         }

         function init() {
            initDatabase();
            createTabulator();
            updateTable();
            const add_button = document.getElementById('add_button');
            add_button.addEventListener('click', showAddDialog);
            document.getElementById('add_ok').addEventListener('click', handleAdd);
            document.getElementById('add_cancel').addEventListener('click', () =>{
               document.getElementById('add_dialog').close();
            });
            document.getElementById('edit_ok').addEventListener('click', handleEdit);
            document.getElementById('edit_cancel').addEventListener('click',
               () => { document.getElementById('edit_dialog').close(); });
            document.getElementById('delete_ok').addEventListener('click', handleDelete);
            document.getElementById('delete_cancel').addEventListener('click',
               () => { document.getElementById('delete_dialog').close(); });
         }
      </script>
   </head>
   <body>
      <button id="add_button">Add</button>    Right-click row to delete
      <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="delete_name"></label>?
         <br><br>
         <button id="delete_cancel">Cancel</button>
         <button id="delete_ok">Ok</button>
      </dialog>
      <div id="students_table"></div>
   </body>
</html>

The new lines of code are lines 20-22 and lines 52-57. Lines 20-22 define a function called trashIcon. This function will be used as the formatter property when putting in the last column. Lines 52-57 add in that last column to the columns for the Tabulator table. Line 52 makes it so that column is not sortable and uses the formatter function called trashIcon. Line 54-56 define the anonymous function that is called when the user clicks on the delete button for a given row. On line 55, we pass cell.getRow() as the argument to the showDeleteDialog() function as that function expects a table row to be passed.

All the rest of the functionality to delete a student already existed in the code for deleting the student that was right-clicked on. Here is a screen shot showing the delete dialog being displayed when the user clicked on the delete button for Bob Data:

delete bob data

If the user clicks Ok, Bob Data will be deleted. Otherwise, if the user hits cancel the dialog box will close and nothing else happens.