Course: JavaScript

Progress (0%)

Exercise: Sortable Tables

Exercise 55 Hard

Prerequisites for the exercise

  1. JavaScript Events — Event Objects
  2. JavaScript Events — Basics
  3. All previous chapters

Objective

Construct a SortableTable class that creates an interactive table whereby the items could be sorted based on a particular field.

Description

In HTML DOM — Building Tables Exercise, you created an HTML table based on an array of items, each of which represented a row of the table, while the headings were obtained via the properties of these items.

Now, you have to take that idea a step further.

In this exercise, you have to create a SortableTable class that creates a sortable table based on a given array of items.

Here's what we mean by a 'sortable table':

Initially the table should be the same as it would be if it was implemented as a normal table (just as we did in HTML DOM — Building Tables Exercise). The main difference is that in a sortable table, all the headings (i.e. <th> elements) of the table are clickable.

When a given heading is clicked, the rows of the table are either sorted or returned to their original ordering based on the point at which the click is made and also depending on which column is clicked.

For instance, given the sortable table below,

NamePrice ($)
Orange juice2.30
Chocolate cookie1.00
Lemon tart1.50

suppose we click on the 'Price ($)' heading. The data would then get reorganized according to the price of each item, in ascending order, as follows:

NamePrice ($) ▴
Chocolate cookie1.00
Lemon tart1.50
Orange juice2.30

Such kinds of interactive tables are very common in apps that deal with tabular data. Sorting the data based on given fields is a really handy way of being able to view the data in a particular order and make more intuition out of it.

Following are a couple of things to abide by in your sortable table implementation:

  • On the very first click on a heading, the table should be reordered in ascending values of the corresponding field.
  • On the next click on the same heading, the table should be reordered in descending values of the corresponding field.
  • On the third click on the same heading, the table should return to its original order.
  • Clicking on any other heading apart from the one which was clicked previously (if any) should get the table to be reordered in ascending values of the corresponding field (of that recently-clicked heading). The previous order must be overridden.

In order to make it clear as to which heading is the table sorted by and in which order, also implement the following points:

  • Add the HTML entity symbol (you can copy this as normal text) next to the text of a heading if the table is sorted in ascending values of its corresponding field.
  • Add the HTML entity symbol next to the text of a heading if the table is sorted in descending values of its corresponding field.
  • If the table is in its original order, no such symbols should be visible.

Apart from this, the constructor of the SortableTable class must follow the signature shown below:

new SortableTable(data[, element])

data is the array of items to display in the form of an HTML table, while element is the element node inside which the table should be placed. As you can see, it must be optional, and default to the <body> element.

To get an even better idea of what you ought to create, take a look at the example below:

Live Example

View Solution

New file

Inside the directory you created for this course on JavaScript, create a new folder called Exercise-55-Sortable-Tables and put the .html solution files for this exercise within it.

Solution

Let's start by thinking about the state that we need to maintain for a sortable table.

Firstly, we obviously need to store the array passed in as the first argument to the constructor. Secondly, we need to store the name of the property, i.e. the key, that is used to sort the data. Besides this, we need to store the order of the sorting, if there is any sorting at all.

We also need to store the <table> element of the sortable table since we'll need it every now and then when we want to reorder the table and change the content of its headings.

There's one thing more which might not be very obvious at this stage. As stated in the description above, when we click on a heading, the sorting of the table based on the previously-clicked heading must be removed and the text of that (previously-clicked) heading reset to its original value.

This means that we also need to store a reference to the previous heading element so that we could easily change its text.

And so this gives us a total of five pieces of state data.

Let's now intuitively name and structure them:

  • data will refer to the list passed into the constructor as the first argument, which is the data of the table.
  • element will refer to the underlying <table> element.
  • sorting will be an object describing the table's underlying sorting. The order property of this object will specify the sorting order while the key property will specify the key used in the sorting.
  • previousHeadingElement will refer to the previously-clicked heading (<th>) element node.

So far, everything looks great.

Let's move on...

The next thing to think about is that when we call SortableTable() with given values, what to do at that point? Should the table-creation logic be placed inside the constructor or should it be inside a separate method?

Well, we'll take a pretty intuitive approach.

Since the table's data won't itself ever change — it'll just merely get reordered — we could go with three different SortableTable methods to handle the table-creation and table-ordering logic:

  • initTableMarkup() will, as the name suggests, initialize the markup structure of the table. For example, if the given list has 10 items, each with 6 properties, then initTableMarkup() will create a <table> element with 11 <tr>s, where the first <tr> consists of 6 <th>s while the remaining 10 <tr>s consist of 6 <td>s.
  • reorderData() will handle all the logic of sorting the data of the table, if there is a need to sort it (which can be done by inspecting the sorting property).
  • populateData(data) will serve to add concrete data, provided as an argument, into these respective elements.

Inside the constructor, we'll first call initTableMarkup() and then populateData() with the data list passed into the constructor.

Simple.

The benefit of this approach is that we just have to create the entire <table> once, reusing its existing structure. We don't have to constantly tear down the previous table's nodes and then create new ones upon each ordering change; only the data therein changes. That's it.

Last but not the least, to make the table heading click-interactive, we have to assign click listeners to each one. And for this, we also need to make sure that when a heading is clicked, there's some way for the handler to figure out which key the heading corresponds to.

There are essentially two ways to achieve this.

  1. Create a different handler for each heading that remembers its corresponding key.
  2. Store the key on each heading (<th>) element node and likewise, use the exact same handler for all headings.

We'll go with the former approach.

Alright, now that we have a rock-solid plan in hand, we can finally move to the most exciting and awaited part of the exercise — the coding.

Here's the minimal code of the class:

class SortableTable {
   constructor(data, parentElement = document.body) {
      this.data = data;
      this.element = null;
      this.sorting = {
         order: null,
         key: null
      }
      this.previousHeadingElement = null;
   
      this.initTableMarkup();
      this.populateData(data);
   }

   initTableMarkup(parentElement) {
      /* To be defined */
   }

   populateData(data) {
      /* To be defined */
   }
}

Now, let's define the initTableMarkup() method.

We'll take a straightforward approach: create all the element nodes individually via DOM methods.

Here's the method's definition:

class SortableTable {
   /* ... */

   initTableMarkup(parentElement) {
      var tableElement = document.createElement('table');

      // Create table header.
      var tableRowElement = document.createElement('tr');
      var headings = Object.keys(this.data[0]);

      for (var heading of headings) {
         var tableHeadingElement = document.createElement('th');
         tableHeadingElement.onclick = this.headingClickHandler.bind(this, heading);
         tableHeadingElement.textContent = heading;
         tableRowElement.appendChild(tableHeadingElement);
      }
      tableElement.appendChild(tableRowElement);

      // Create as many body rows as there are entries in data
      for (var entry of this.data) {
         var tableRowElement = document.createElement('tr');
         for (var prop in this.data[0]) {
            var tableDataElement = document.createElement('td');
            tableRowElement.appendChild(tableDataElement);
         }
         tableElement.appendChild(tableRowElement);
      }

      this.element = tableElement;
      parentElement.appendChild(tableElement);
   }
}

Perhaps, the most important step happens in line 13 where we set up a click handler on each <th> element. The click handler is obtained by calling bind() on the headingClickHandler() method.

This is done to create a separate handler for each heading that remembers the value of the corresponding key (headings[i] in the code above).

The definition of headingClickHandler() is also pretty important — it has to determine which key and order to use for the table's sorting, if any at all.

Following we define headingClickHander():

class SortableTable {
   /* ... */

   headingClickHander(key, e) {
      var currentKey = this.sorting.key;
      var currentOrder = this.sorting.order;
      var newKey = currentKey;
      var newOrder = currentOrder;

      if (currentKey === key) {
         if (currentOrder === 'asc') {
            newOrder = 'desc';
         }
         else {
            this.prevTableHeadingElement = null;
            newKey = null;
            newOrder = null;
         }
      }

      // Resetting the previous <th> only makes sense if the previous sorting key
      // and the new one are different. And in this we need to make sure that
      // the previous sorting key wasn't an empty string ('').
      else {
         if (this.prevTableHeadingElement && currentKey !== '') {
            this.prevTableHeadingElement.textContent = currentKey;
         }
         newKey = key;
         newOrder = 'asc';
      }


      if (newOrder === 'asc') {
         e.currentTarget.textContent = key + ' ' + '▴';
      }
      else if (newOrder === 'desc') {
         e.currentTarget.textContent = key + ' ' + '▾';
      }
      else {
         e.currentTarget.textContent = key;
      }

      // Update to new values.
      this.sorting.key = newKey;
      this.sorting.order = newOrder;
      this.reorderData();

      this.prevTableHeadingElement = e.currentTarget;
   }
}

When a heading is clicked, this method takes over and, in the end, calls reorderData().

The purpose of reorderData() is simply to reorder the table's data.

It particular, reorderData() first makes a copy of the instance's data list so that the original ordering always remain intact, and then, if there is a non-null   sorting.order and sorting.key, calls the sort() method on this copied list. Otherwise, it just proceeds without any processing of the list.

Following is the definition of reorderData():

class SortableTable {
   /* ... */

   reorderData() {
      var sortingKey = this.sorting.key;
      var sortingOrder = this.sorting.order;
      var clonedData = this.data.slice();

      var sortingFunction;
      if (sortingOrder === 'asc') {
         sortingFunction = function(a, b) {
            if (a[sortingKey] < b[sortingKey]) return -1;
            if (a[sortingKey] > b[sortingKey]) return 1;
            return 0;
         }
      }
      else if (sortingOrder === 'desc') {
         sortingFunction = function(a, b) {
            if (a[sortingKey] < b[sortingKey]) return 1;
            if (a[sortingKey] > b[sortingKey]) return -1;
            return 0;
         }
      }

      clonedData.sort(sortingFunction);
      this.populateData(clonedData);
   }
}

Notice how reorderData() calls the same populateData() method called inside the constructor. This is an example of code reuse.

The last thing left to see is the definition of populateData() which does a very basic job — take the list passed in and fill the table with data in that very order. That's it.

Here's how populateData() looks:

class SortableTable {
   /* ... */

   populateData(data) {
      var tableElement = this.element;

      // Start with the second row.
      var tableRowElement = this.element.childNodes[1];

      for (var entry of data) {
         var tableDataElement = tableRowElement.firstChild;
         for (var key in entry) {
            tableDataElement.textContent = entry[key];
            tableDataElement = tableDataElement.nextSibling;
         }
         tableRowElement = tableRowElement.nextSibling;
      }
   }
}

With this, we complete this exercise.

Let's give the final result a try.

Live Example

It works absolutely flawlessly!

"I created Codeguage to save you from falling into the same learning conundrums that I fell into."

— Bilal Adnan, Founder of Codeguage