-->
Bookmark and Share

Table Sort

See the demo page for the finished version of the code.

Getting Values

First, in order to sort the table, we need a way to get the data within it. We know that we can use the rows and cells collections to access each TD or TH cell in the table, but we need a way to extract the actual text contained within those elements.

In the previous samples, we simply took the nodeValue of the cell's firstChild property. This works because each TD or TH tag contained plain text. So in the DOM, each cell had a single child node which happened to be a text node.

If we had HTML like this:

  <td>sample <b>text</b></td>
  <td><a href="page.html">sample text</a></td>

that code would either have failed to get all the text or even, in the case of the second cell above, caused a scripting error.

While the planets table also uses plain text in each cell, it would be nice to have a generalized function for extracting all the text from any given cell, no matter how its contents are arranged. The following function does just that.

function getTextValue(el) {

  var i;
  var s;

  // Find and concatenate the values of all text nodes contained
  // within the element.
  s = "";
  for (i = 0; i < el.childNodes.length; i++)
    if (el.childNodes[i].nodeType == document.TEXT_NODE)
      s += el.childNodes[i].nodeValue;
    else if (el.childNodes[i].nodeType == document.ELEMENT_NODE &&
             el.childNodes[i].tagName == "BR")
      s += " ";
    else
      // Use recursion to get text within sub-elements.
      s += getTextValue(el.childNodes[i]);

  return normalizeString(s);
}

This function looks at each child node of the cell in order, building a text string as it goes along. When a child is a text node, it appends that text to the string. When the child is some other type of node, it first calls itself recursively to extract any text from that child and then appends that text to the string.

One exception is for BR tags. For these it appends a single space to the string to represent the line break implied by that tag.

Browser Compatibility

Note the use of document.TEXT_NODE and document.ELEMENT_NODE in code above. These are constants defined in the DOM standard, along with others, which correlate to values the DOM uses for the nodeType property.

Some browsers (notably IE) do not define these properties. You'll need to define them yourself in order for the script to work on these browsers. The code below does just that.
// This code is necessary for browsers that don't reflect the DOM
// constants (like IE).
if (document.ELEMENT_NODE == null) {
  document.ELEMENT_NODE = 1;
  document.TEXT_NODE = 3;
}
So if document.ELEMENT_NODE is not defined by a given browser, the script defines both it and document.TEXT_NODE.

The call to normalizeString() at the end of the function cleans up any white space (spaces, tabs, newlines, etc.) in the string. That function simply replaces multiple white space characters with a single space and removes any leading or trailing white space.

// Regular expressions for normalizing white space.
var whtSpEnds = new RegExp("^\\s*|\\s*$", "g");
var whtSpMult = new RegExp("\\s\\s+", "g");

function normalizeString(s) {

  s = s.replace(whtSpMult, " ");  // Collapse any multiple whites space.
  s = s.replace(whtSpEnds, "");   // Remove leading or trailing white
                                  // space.
  return s;
}

The end result is similar to what you would get by stripping out any HTML tags between the <TD> and </TD> (or the <TH> and </TH>) tag pair for a cell.

Comparing Values

Once we have the values, we need to be able to compare them in order to do any sorting. For the "Name" column in the planets table, we can just compare the two strings. But for the other columns, the values are numeric. The problem is that the values returned by getTextValue() are strings and would normally be compared lexicographically in JavaScript, not numerically. The code below illustrates the problem.

alert(1000 > 3);      // <== displays 'true.'
alert("1000" > "3");  // <== displays 'false.'

Show (1000 > 3) | Show ("1000" > "3")

We want to be able to compare values either as strings or, when appropriate, as numbers. So we define a function to compare two string values based on whether or not they can be interpreted as numeric values.

function compareValues(v1, v2) {

  var f1, f2;

  // If the values are numeric, convert them to floats.

  f1 = parseFloat(v1);
  f2 = parseFloat(v2);
  if (!isNaN(f1) && !isNaN(f2)) {
    v1 = f1;
    v2 = f2;
  }

  // Compare the two values.
  if (v1 == v2)
    return 0;
  if (v1 > v2)
    return 1
  return -1;
}

The compareValues() function first checks to see if the two string values passed to it can be converted to numbers using the parseFloat() function. If either call to parseFloat() returns the value NaN (a JavaScript constant meaning "not a number") it leaves them unchanged. But if both can be converted to numbers, it converts them.

You could take this function further, say by using regular expressions to recognize different data formats. For example, you could test if a string is in a currency format like "$1234.56" which could be converted to a numeric value. Or you could check for a date format like "07/04/1776" and create Date objects for comparison. It all depends on what kind of data you have in your table.

The function compares the two values and returns a value using the same convention defined for comparison functions used with the Array object's sort() method. In other words, it returns a negative value if v1 is less than v2, a positive value if v1 is greater than v2 or zero if v1 is equal to v2.

Arrays vs. Collections

Incidentally, you may be wondering why we don't just use the Array object's sort() method on the rows property of the table element. The problem is that the rows property is not a JavaScript array, it is an HTMLCollection object.

Even though you access the individual rows in this collection using array notation (i.e., using square brackets with a numeric index) and it has a length property (as arrays do) it is not an Array object. HTML collections do not have a sort() method. We'll just have to provide that functionality ourselves.