When you’re done poking through this, take a peek at how you can double the speed of this script in The Joy of an Optimized, Complete Javascript Table Sort, and then check out the final version of it at The Joy of Cows On Tables

Ah table sorting. There are few problems that have been solved as many times as you have been. Unfortunately, some of the nicest solutions, such as mootable, are also pretty overbearing. Check out this feature list:

  1. Total re-styling of your table.
  2. Editable table cells.
  3. Loading table contents from JSON.
  4. Loading table contents from JSON over XHR.
  5. Server-side sorting using the above.
  6. Client-side sorting.
  7. Re-ordering of columns, column options.
  8. Nice fade effects.
  9. Event hooks.

Whoa, too much. Over at ICA we needed something way more lightweight. I was pretty much looking for this:

  1. Client-side sort of various formats (like mm/dd/yy).
  2. Zebra or striped tables.
  3. Support sorting with hidden rows on the table.
  4. Be fairly fast.
  5. Use mootools (which we already use).
  6. Use a table already on the DOM.

Let’s take a look at how to make a javascript table sort that follows best practices, is relatively minimal, and fast. Nothing here is completely new stuff, but hopefully walking through it will help you write a better table sort next time you need just a table sort, and not all the overhead of a library. I’ll be using mootools sort of aggressively, but the core ideas and practices here are portable to any environment.

Here we go!

HTML Assumptions and Javascript Style

Our code is going to make a few assumptions. Assumptions are always a tradeoff. They can be a detriment if you don’t know what the assumptions are, but are a great enhancement to your consistency and coding speed if you know what they are. We’re going to assume the table we want to sort’s HTML looks something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

<table cellpadding="0" cellspacing="0" id="sort_this">
  <thead>
    <tr>
      <th>a header</th>
      ...
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>a value
      ...
    </tr>
    <tr>
      <td>another value</td>
      ...
    </tr>
  </tbody>
</table>

Take note of a few things:

  1. We gave the table an id, “sort_this”
  2. We used thead and tbody sections
  3. We used “th” tags for the headers

All of that is really just good HTML, the semantic use of th and td, for example, is just good HTML table markup.

Javascript can be kludged onto a page in a thousand different ways, we’re going to stick with three tenets:

  1. Be unobtrusive (keep javascript out of our HTML).
  2. Use objects and instances to keep our code reusable (and able to work with multiple tables).
  3. Use options as a hash for readability.

Let’s look at a basic mootools javascript object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

var SortingTable;
SortingTable = new Class({

  initialize: function( table, options ){
    ...
  },

  sort_by_header: function( text ){
    ...
  }

});

SortingTable.stripe_table = function( tr_elements ){

}

You can see how we added instance and class functions. “initialize” is an instance function run when we instantiate our object, “sort_by_headers” is an instance function we call on an object.

“stripe_table” is added in a different manner, that’s because “stripe_table” is a class function, we want to be able to use it without instantiating “SortingTable” at all. When we use SortingTable, it should look like this:

1
2

new SortingTable( 'sort_this' );

Or maybe if we have options:

1
2

new SortingTable( 'sort_this', { zebra: false } );

We could create an instance and call sort_by_methods:

1
2
3

var sorting_table = new SortingTable( 'sort_this', { zebra: false } );
sorting_table.sort_by_header( 'name' );

Or call a class method without an instance:

1
2

SortingTable.stripe_table( $$( '#sort_this tbody tr' ) );

Alright, we have a basic set of common practices we can base our code off. Let’s dive into a very basic table sort script.

Basic Javascript Table Sorting

Now this is only a starting point, the table sort below is so basic it isn’t very user friendly. It is a good place to start understanding how we sort things in general, and deal with the tables on the DOM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

var SortingTable;
SortingTable = new Class({

  initialize: function( table, options ) {
    this.table = $(table);
    this.tbody = $(this.table.getElementsByTagName('tbody')[0]);
    
    this.headers = new Hash;
    var thead = $(this.table.getElementsByTagName('thead')[0]);
    $each(thead.getElementsByTagName('tr')[0].getElementsByTagName('th'), function( header, index ) {
      var header = $(header);
      this.headers.set( header.getText(), { column: index } );
      header.addEvent( 'mousedown', function(evt){
        var evt = new Event(evt);
        this.sort_by_header( evt.target.getText() );
      }.bind( this ));
    }.bind( this ) );
  },

  sort_by_header: function( header_text ){
    this.rows = new Array;
    var trs = this.tbody.getElements( 'tr' );
    while ( trs.length > 0 ) {
      var row = { row: trs.shift().remove() };
      this.rows.unshift( row );
    }

    var header = this.headers.get( header_text );
    if ( this.sort_column >= 0 && this.sort_column == header.column ) {
      // They were pulled off in reverse
    } else {
      this.sort_column = header.column;
      this.rows.sort( this.compare_rows.bind( this ) );
    }

    while (this.rows.length > 0) {
      var row = this.rows.shift();
      row.row.injectInside( this.tbody );
    }
    this.rows = false;
  },

  compare_rows: function( r1, r2 ) {
    r1.compare_value = $(r1.row.getElementsByTagName('td')[this.sort_column]).getText();
    r2.compare_value = $(r2.row.getElementsByTagName('td')[this.sort_column]).getText();
    if ( r1.compare_value > r2.compare_value ) { return  1 }
    if ( r1.compare_value < r2.compare_value ) { return -1 }
    return 0;
  }

});

Oh the bitter pill of javascript. Let’s boil it down, there really isn’t too much going on here. In the initialize (which remember, is run as soon as we instantiate):

  1. We find some nodes on the DOM so we don’t need to find them again later: this.table, this.tbody and this.thead.
  2. In $each, we walk all of the “th” tags in thead and do two things:
    1. add the innerText as a key in this.headers, with a value that includes the index, or which column we’re dealing with. This is a map for later.
    2. add a “mousedown” event handler to the “th” tag, firing sort_by_header with the th’s innerText

Stay with me. Look at the initialize again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

  initialize: function( table, options ) {
    this.table = $(table);
    this.tbody = $(this.table.getElementsByTagName('tbody')[0]);
    
    this.headers = new Hash;
    var thead = $(this.table.getElementsByTagName('thead')[0]);
    $each(thead.getElementsByTagName('tr')[0].getElementsByTagName('th'), function( header, index ) {
      var header = $(header);
      this.headers.set( header.getText(), { column: index } );
      header.addEvent( 'mousedown', function(evt){
        var evt = new Event( evt );
        this.sort_by_header( evt.target.getText() );
      }.bind( this ));
    }.bind( this ) );
  },

See the lines that close out the each and addEvent functions? They’re using .bind( this ) to attach the internal “this” of the function back to our instantiated object. It’s a nice trick that let’s us have a “mousedown” event that is attached to our object.

Deep breath here, let’s step through the sort_by_header section.

1
2
3
4
5
6
7
8

  sort_by_header: function( header_text ){
    this.rows = new Array;
    var trs = this.tbody.getElements( 'tr' );
    while ( trs.length > 0 ) {
      var row = { row: trs.shift().remove() };
      this.rows.unshift( row );
    }

So this creates an internal array of rows, then proceeds to walk all the tr’s on tbody. For each one, it is doing two things: Shifting one tr off the array of trs, and using .remove() to drop it off the DOM. Each row is stuffed in an object and added to the top of the this.rows array.

It’s important that it’s added to the beginning of the rows array, because that means if all we needed to do was reverse the rows, we can just replay this array one by one and attach it’s rows to the DOM again. Reverse without calling .reverse(), nice.

1
2
3
4
5
6
7
8

    var header = this.headers.get( header_text );
    if ( this.sort_column >= 0 && this.sort_column == header.column ) {
      // They were pulled off in reverse
    } else {
      this.sort_column = header.column;
      this.rows.sort( this.compare_rows.bind( this ) );
    }

Here we pull the header object out of our headers hash using the passed text (which is the innerText from the attached event in initialize), and then compare it to our last sort_column. It they are the same, we can move on and just re-insert the rows. If they’re different, we need to call this.rows.sort().

.sort() is a javascript method for sorting an array, it’s native to the language. By default, .sort() will sort rows in alphabetical order. To sort in any other way, you can pass it a function, and it’ll use answers of 1, -1 and 0 to figure out the row order. In this example, we’re telling sort to use this.compare_rows, and also reminding it that compare_rows should be run on our current object.

Learn more about sort at w3schools.

1
2
3
4
5
6
7

    while (this.rows.length > 0) {
      var row = this.rows.shift();
      row.row.injectInside( this.tbody );
    }
    this.rows = false;
  },

This section is the meat- we take out sorted or reversed array, shift it’s contents off the top one by one, and add then to this.tbody. Shifting them off gets them out of memory as we move along so we don’t leave arrays sitting in RAM.

That’s the brunt of table sorting in Javascript right there. sort_by_header ripped rows off the table, reversed or sorted them, and then reinserted them onto the DOM.

Our actual table sort logic is the following:

1
2
3
4
5
6
7
8
9
10

  compare_rows: function( r1, r2 ) {
    r1.compare_value = $(r1.row.getElementsByTagName('td')[this.sort_column]).getText();
    r2.compare_value = $(r2.row.getElementsByTagName('td')[this.sort_column]).getText();
    if ( r1.compare_value > r2.compare_value ) { return  1 }
    if ( r1.compare_value < r2.compare_value ) { return -1 }
    return 0;
  }

});

compare_rows get’s two arguments from sort, two rows objects to compare. The text of the td cells is fetched and compared. Pretty straight ahead here, the only trickery is finding out what column to use by reaching into this.sort_column. I like having the whole row in there to compare, it opens to door to having secondary sorting by another column (like iTunes’ “Album By Artist” sorting).

Huzzah! we can nearly rejoice. I’d go back and look over the code we just walked through, if you can understand what’s going on up there, this next block of hackery should make perfect sense.

Why Our Simple Sort Sucks

There are some problems with the simple script above:

  1. It pulls the a given td cell off the DOM and does conversion on it multiple times, that makes it slow.
  2. It isn’t flexible enough to sort mm/dd/yy.

Those are two pretty damning faults, so let’s clean them up before adding any new features.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

var SortingTable;
SortingTable = new Class({

  initialize: function( table, options ) {
    this.table = $(table);    
    this.tbody = $(this.table.getElementsByTagName('tbody')[0]);

    this.headers = new Hash;
    var thead = $(this.table.getElementsByTagName('thead')[0]);
    $each(thead.getElementsByTagName('tr')[0].getElementsByTagName('th'), function( header, index ) {
      var header = $(header);
      this.headers.set( header.getText(), { column: index } );
      $(header).addEvent( 'mousedown', function(evt){
        var evt = new Event(evt);
        this.sort_by_header( new evt.target.getText() );
      }.bind( this ));
    }.bind( this ) );

    this.load_conversions();
  },

  sort_by_header: function( header_text ){
    this.rows = new Array;
    var trs = this.tbody.getElements( 'tr' );
    while ( trs.length > 0 ) {
      var row = { row: trs.shift().remove() };
      this.rows.unshift( row );
    }

    var header = this.headers.get( header_text );
    if ( this.sort_column >= 0 && this.sort_column == header.column ) {
      // They were pulled off in reverse
    } else {
      this.sort_column = header.column;
      if (header.conversion_function) {
        this.conversion_function = header.conversion_function;
      } else {
        this.conversion_function = false;
        this.rows.some(function(row){
          var to_match = $(row.row.getElementsByTagName('td')[this.sort_column]).getText();
          if (to_match == ''){ return false }
          this.conversions.some(function(conversion){
            if (conversion.matcher.test( to_match )){
              this.conversion_function = conversion.conversion_function;
              return true;
            }
            return false;
          }.bind( this ));
          if (this.conversion_function){ return true; }
          return false;
        }.bind( this ));
        header.conversion_function = this.conversion_function.bind( this );
        this.headers.set( header_text, header );
      }
      this.rows.each(function(row){
        row.compare_value = this.conversion_function( row );
      }.bind( this ));
      this.rows.sort( this.compare_rows.bind( this ) );
    }

    while (this.rows.length > 0) {
      var row = this.rows.shift();
      row.row.injectInside( this.tbody );
    }
    this.rows = false;
  },

  compare_rows: function( r1, r2 ) {
    if ( r1.compare_value > r2.compare_value ) { return  1 }
    if ( r1.compare_value < r2.compare_value ) { return -1 }
    return 0;
  },
  
  load_conversions: function() {
    this.conversions = $A([
      // YYYY-MM-DD, YYYY-m-d
      { matcher: /\d{4}-\d{1,2}-\d{1,2}/,
        conversion_function: function( row ) {
          var cell = $(row.row.getElementsByTagName('td')[this.sort_column]).getText();
          var re = /(\d{4})-(\d{1,2})-(\d{1,2})/;
          cell = re.exec( cell );
          return new Date(parseInt(cell[1]), parseInt(cell[2], 10) - 1, parseInt(cell[3], 10));
        }
      },
      // Fallback 
      { matcher: /.*/,
        conversion_function: function( row ) {
          return $(row.row.getElementsByTagName('td')[this.sort_column]).getText();
        }
      }
    ]);
  }

});

this.load_conversions(); is the big new thing in initialize. It’s that function at the end of the class that has and array of hashes each with a “matcher” and “conversion_function”.

Really, the main difference is in the sorting section of sort_by_header, in the meat and bones:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

      this.sort_column = header.column;
      if (header.conversion_function) {
        this.conversion_function = header.conversion_function;
      } else {
        this.conversion_function = false;
        this.rows.some(function(row){
          var to_match = $(row.row.getElementsByTagName('td')[this.sort_column]).getText();
          if (to_match == ''){ return false }
          this.conversions.some(function(conversion){
            if (conversion.matcher.test( to_match )){
              this.conversion_function = conversion.conversion_function;
              return true;
            }
            return false;
          }.bind( this ));
          if (this.conversion_function){ return true; }
          return false;
        }.bind( this ));
        header.conversion_function = this.conversion_function.bind( this );
        this.headers.set( header_text, header );
      }
      this.rows.each(function(row){
        row.compare_value = this.conversion_function( row );
      }.bind( this ));
      this.rows.sort( this.compare_rows.bind( this ) );

Ok, don’t get thrown. Javascript’s weird features mean loops are pretty messy. Notice the first time the rows are walked we use “.some(“. Some is a mootools array function that acts like each until the function returns true, then it breaks the loop. This is what’s going on here:

  1. See if we have a conversion_function. If we don’t…
    1. Walk through td’s in the column.
    2. If it’s innerText is blank, go to the next element.
    3. Walk through the available conversions.
    4. If a conversion matches the matcher, assign it to this.conversion_function and break the loop
    5. Save the conversion_function on the header object so we don’t need to find it later.
  2. Walk all our row objects and run the conversion_function on each, save it onto the row.
  3. Run sort with compare_rows.

compare_rows, you can see, now expects to sort with the compare_value:

1
2
3
4
5
6

  compare_rows: function( r1, r2 ) {
    if ( r1.compare_value > r2.compare_value ) { return  1 }
    if ( r1.compare_value < r2.compare_value ) { return -1 }
    return 0;
  },

By adding new conversions to load_conversions, you can support sorting of all kinds of different formats and sub-columns. And you’ll only be running that conversion once on a cell (until you sort another column and come back, this doesn’t do aggressive caching of the whole table in memory).

The Sweet Smell Of Success

All that’s needed now is a sprinkling of zebra or striped tables, and stuffing some extra baggage onto the row objects, and we’ll support those last two features:

  1. Zebra or striped tables.
  2. Hidden/expandable rows.

It looks something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145

//
// new Star.Table( 'my_table', {
//   zebra: true,     // Stripe the table, also on initialize
//   details: false,  // Has details every other row
// });
//
// The above were the defaults.  The regexes in load_conversions test a cell
// begin sorted for a match, then use that conversion for all elements on that
// column.
//
// Requires mootools Class, Array, Function, Element, Element.Selectors,
// Element.Event, and you should probably get Window.DomReady if you're smart.
//

var SortingTable;
SortingTable = new Class({

  initialize: function( table, options ) {
    this.options = $merge({
      zebra: true,
      details: false
    }, options);
    
    this.table = $(table);
    
    this.tbody = $(this.table.getElementsByTagName('tbody')[0]);
    if (this.options.zebra) {
      SortingTable.stripe_table( this.tbody.getElements( 'tr' ) );
    }

    this.headers = new Hash;
    var thead = $(this.table.getElementsByTagName('thead')[0]);
    $each(thead.getElementsByTagName('tr')[0].getElementsByTagName('th'), function( header, index ) {
      var header = $(header);
      this.headers.set( header.getText(), { column: index } );
      header.addEvent( 'mousedown', function(evt){
        var evt = new Event(evt);
        this.sort_by_header( evt.target.getText() );
      }.bind( this ));
    }.bind( this ) );

    this.load_conversions();
  },

  sort_by_header: function( header_text ){
    this.rows = new Array;
    var trs = this.tbody.getElements( 'tr' );
    while ( trs.length > 0 ) {
      var row = { row: trs.shift().remove() };
      if ( this.options.details ) {
        row.detail = trs.shift().remove();
      }
      this.rows.unshift( row );
    }

    var header = this.headers.get( header_text );
    if ( this.sort_column >= 0 && this.sort_column == header.column ) {
      // They were pulled off in reverse
    } else {
      this.sort_column = header.column;
      if (header.conversion_function) {
        this.conversion_function = header.conversion_function;
      } else {
        this.conversion_function = false;
        this.rows.some(function(row){
          var to_match = $(row.row.getElementsByTagName('td')[this.sort_column]).getText();
          if (to_match == ''){ return false }
          this.conversions.some(function(conversion){
            if (conversion.matcher.test( to_match )){
              this.conversion_function = conversion.conversion_function;
              return true;
            }
            return false;
          }.bind( this ));
          if (this.conversion_function){ return true; }
          return false;
        }.bind( this ));
        header.conversion_function = this.conversion_function.bind( this );
        this.headers.set( header_text, header );
      }
      this.rows.each(function(row){
        row.compare_value = this.conversion_function( row );
      }.bind( this ));
      this.rows.sort( this.compare_rows.bind( this ) );
    }

    var index = 0;
    while (this.rows.length > 0) {
      var row = this.rows.shift();
      row.row.injectInside( this.tbody );
      if (row.detail){ row.detail.injectInside( this.tbody ) };
      if ( this.options.zebra ) {
        row.row.removeClass( 'alt' );
        if (row.detail){ row.detail.removeClass( 'alt' ); }
        if ( ( index % 2 ) == 0 ) {
          row.row.addClass( 'alt' );
          if (row.detail){ row.detail.addClass( 'alt' ); }
        }
      }
      index++;
    }
    this.rows = false;
  },

  compare_rows: function( r1, r2 ) {
    if ( r1.compare_value > r2.compare_value ) { return  1 }
    if ( r1.compare_value < r2.compare_value ) { return -1 }
    return 0;
  },
  
  load_conversions: function() {
    this.conversions = $A([
      // YYYY-MM-DD, YYYY-m-d
      { matcher: /\d{4}-\d{1,2}-\d{1,2}/,
        conversion_function: function( row ) {
          var cell = $(row.row.getElementsByTagName('td')[this.sort_column]).getText();
          var re = /(\d{4})-(\d{1,2})-(\d{1,2})/;
          cell = re.exec( cell );
          return new Date(parseInt(cell[1]), parseInt(cell[2], 10) - 1, parseInt(cell[3], 10));
        }
      },
      // Fallback 
      { matcher: /.*/,
        conversion_function: function( row ) {
          return $(row.row.getElementsByTagName('td')[this.sort_column]).getText();
        }
      }
    ]);
  }

});

SortingTable.stripe_table = function ( tr_elements  ) {
  var counter = 0;
  $$( tr_elements ).each( function( tr ) {
    if ( tr.style.display != 'none' && !tr.hasClass('collapsed') ) {
      counter++;
    }
    tr.removeClass( 'alt' );   
    if ( !(( counter % 2 ) == 0) ) {
      tr.addClass( 'alt' );   
    }
  }.bind( this ));
}

Nice.

That was a lot of ground to cover, as in, way more than I had any intention of covering :-). I’ve made a lot of assumptions (remember those?) about your javascript fu in this post, but if you have any questions just ask!

You can pull this code down as javascript or take a look at some running examples, including how to use hidden rows.

Also look at optimization steps and an updated script at The Joy of an Optimized, Complete Javascript Table Sort, and then check out the final script in The Joy of Cows On Tables

Thanks for reading and come again, -Matt

22 Responses

  1. mv64 at January 11th, 2008 at 03:44 PM

    very nice, but it’s not alphabetical, I think it should be.

  2. chinahorse at January 11th, 2008 at 03:59 PM

    Yes, I agree. Is it sorting every column? It appears to only be sorting the date column, no matter which <th> you click. Is it easy to make it sort the cols alphabetically?

  3. Matthew Beale at January 11th, 2008 at 04:16 PM

    Wow- some folks on the mootools forums pointed out an IE bug, and in fixing that I also tried using innerText, which was a cluster fsck. Using getText() instead seems to be consistent enough.

    Geez, thanks for your patience, give it another shot.

    -Matt

  4. mv64 at January 11th, 2008 at 08:13 PM

    Thanks for fix…...it’s very nice addition to Mootools framework…...Thanks a lot. Also I tested on Latest IE7, FF, Safari, NNv9…. Thanks!

  5. starlight at January 12th, 2008 at 08:30 PM

    Hey all, I seem to have this working but it’s not sorting it properly. (It all appears to work and only sorts on the last column it seems) Has the bug been fixed? Great job on this so far!

  6. starlight at January 13th, 2008 at 12:02 AM

    got it working, cheers.

  7. hello there at January 14th, 2008 at 08:00 PM

    would be nice to see the example with several hundred rows – performance in sorting is a huge issue, and looking at the zillions of libraries out there… many of them konk out completely, taking 5 seconds to sort a table with 1,000 rows.

  8. minroi_aoi at January 16th, 2008 at 05:23 AM

    Umm… (i think) i found a bug. If you have numbers 1 – 10 then after you sort it, it will be…

    1 10 2 3 4 5 6 7 8 9

    can you fix this ???? maybe convert the cell content to integer first if the content is numeric, rather than parse it as a string.

  9. Matthew Beale at January 16th, 2008 at 08:52 AM
    @minroi_aoi – Right. That’s because our fallback conversion function returns a string, so those numbers are compared as strings. If you want to compare them as numbers you can add a conversion function:
          // Numbers
          { matcher: /^\d+$/,
            conversion_function: function( row ) {
              var cell = $(row.row.getElementsByTagName('td')[this.sort_column]).getText();
              return parseInt(cell);
            }
          },
    That’s why having conversions is so nice, you can sort in a variety of formats.

    @hello there – I’m working out a blog post to go over some optimizations right now. Using firebug’s profiler on a thousand rows I see about 2.5 seconds. IE’s sort is probably a bit slower. Using the profiler it’s pretty easy to see where a difference can be made, I’ll post the tweaks sometime today, probably.

  10. goulian at January 16th, 2008 at 05:20 PM

    Hi, very nice script, tested with ie6 works fine ONLY if zebra option is true.

    Thanks for posting it…

    All the best John…

  11. Jason Boxman at January 19th, 2008 at 07:48 PM

    That’s amazing! I’ll be looking into integrating this into a current internal application I am developing. It’s just what I was looking for, as I’d rather leave the sorting and such client side for now.

    Thanks!

  12. Steve at January 21st, 2008 at 07:18 PM

    It’s awesome! Nice scripts!

  13. minori_aoi at February 6th, 2008 at 03:04 AM

    wow thanks, very nice and clean solution. btw you should clean those spam _.

  14. Harmen at February 6th, 2008 at 06:05 AM

    nice script!

    but how to set a column to be sorted on init?

  15. Mike at February 16th, 2008 at 10:18 PM

    Thanks, I’ve been looking for something like this for a while! One question: how would you add up/down arrows to the column headings to show the current sort direction?

    Thanks again!

  16. Mike at February 18th, 2008 at 01:42 AM

    Sorry, one more question: I’m nesting collapsible rows like so, using a nested table: - row 1 - row 1.a (nested table) —row 1.a.1 —row 1.a.2

    It’s working except that row 1 expands only enough to accommodate the content for row 1.a; when that’s expanded the content for 1.a.1 and 1.a.2 extends past the viewable space. Do you know if there’s a way to tweak this so row 1.a expands as needed to fit content in it’s nested rows? Thanks again and sorry for the spam.

  17. Tom Greever at February 19th, 2008 at 11:35 PM

    Hey, just wanted to let you know I used your script on my site. http://www.communitychristian.org/aboutus/staff

    Thanks!!

  18. bawasa at February 21st, 2008 at 09:26 AM

    In div.collapsed, I need to place another table say 4×6 with an id. When I tried it’s not sorting properly. It also sorts tds from inside table. I even tried by giving adding both the table names in css. But its not working.

    I don’t know much about javascript.

    Can any help me out.

    And one more thing date sorting is not working as per month. It sorts simply by first alphabet not by month. eg. April – August – January – March

    Please help! Thanks

  19. avdistribution at March 10th, 2008 at 12:11 AM

    I love the script but am having the same problem mentioned above-numbers sorting by the first digit-so in ascending order a price of 299.99 appears before 39.99. I added the numbers conversion function as suggested in the above post, but am not sure if I put it in the right place, or if it should replace the “fallback” function.

  20. Web Design Glasgow at March 14th, 2008 at 05:59 AM

    Hey, thanks very much for writing this up and sharing – it’s a very well executed class for sorting tables, and I’ve looked at a few!

    Mike, to add arrows that reflect which column is sorted on which direction, you might do the following:

    In the initialize function, change this.headers.set( header.getText(), { column: index } ); to this.headers.set($(header).getText(), {cell:header, column:index});

    In the first if statement of the sort_by_header function, add header.cell.setStyle(‘background’, ‘url(images/’ + (String(header.cell.getStyle(‘background’)).contains(‘down’) ? ‘up’ : ‘down’) + ’.gif) no-repeat top right’);

    to the true execution, and header.cell.getSiblings().each(function(th) {th.setStyle(‘background’, ’’)}); header.cell.setStyle(‘background’, ‘url(images/down.gif) no-repeat top right’);

    to the else execution. Be sure to put up.gif and down.gif in the appropriate location.

    Best wishes

  21. RS Justin at March 24th, 2008 at 03:46 PM

    Hey there…great script! I’ve been using a prototype-driven script for a while that works great, but it sort of thwarted my desire to be all mootools all the time…this give me exactly what I was looking for.

    One question, however: the script I was using before had a neat little feature where, by using a class “nosort” you could deactivate a column header and make the table NOT sortable by that col. This was especially good for columns that had checkboxes or other form-y elements…which I see currently throw your script for a loop.

    Which is totally fine…I wonder, though, how hard it would be to get you (or someone else here who knows what they’re doing) to implement that NOSORT functionality?

    Thanks…and again, great work!!

  22. Mike at April 2nd, 2008 at 12:14 PM

    Thanks for this awesome contribution. Very fast, doesn’t require some quirky DOM rewrite, and as easy to setup as can be imagined.

    One thing … unless I’m missing something, I’ve noticed that colspans break the script. Although, supporting cell spans may add an additional level of complexity to this already highly optimized piece of code. Never mind, then!

    Cheers!

Sorry, comments are closed for this article.