Aside from a slew of bug fixes and minor changes, the biggest thing to happen to liteGrid this week is the addition of the RowSortModule. This module enables client-side sorting and works on both regular and tree-grid tables. You can check it out on the updated demo. As usual, everything in liteGrid just bolts on, so all you have to do is add RowSortModule to your list of modules, set the “sortable” property to true on the columns you wish to allow sorting by, and you’re all set:
$(function() { $("#demoTable").inrad_liteGrid({ columns: [ {field: "name", editable: true, header: "Name", sortable: true}, {field: "price", editable: true, header: "Unit Price", sortable: true }, {field: "coating", editable: true, header: "Coating", sortable: true }, {field: "description", editable: true, header: "Description", sortable: true } ], dataProvider: new WidgeDataProvider(), modules: [new TreeGridModule(), new StripifyModule(), new JEditableModule(), new ToolbarModule(), new BatchSaveModule(), new FormatModule(), new BlockUIModule(), new RowAdditionModule(), new RowDeleteModule(), new RowSortModule()], rowIdColumn: "id" }); });
Note: I plan on adding an option that will make all columns sortable by default, but for now, sorting is opt-in instead of opt-out.
That’s all there is to it. Let’s look (quickly) at the implementation:
function RowSortModule() { var base = this; var liteGrid; var options; //Hooks in to liteGrid. base.initialize = function(grid, opts) { ... } //Converts the table headers into clickable headers that will sort the grid. base.makeHeadersClickable = function() { ... } //Performs the actual sort. base.columnClicked = function() { ... } //Recursively sorts the rows and append them to the table. base.doSortAndAppend = function(sortItems) { ... } //Compares left to right, returning -1 if left is smaller, 0 if they are equal, //and 1 if left is larger (logic is inverted if sortAsc is false). base.compare = function(left, right) { ... } }
First is the standard initialize method that all modules must implement, which ties in to the liteGrid event system (makeHeadersClickable). When a header is clicked, the rows are sorted (recursively if the tree-grid module is in use) and re-appended to the grid. Let’s dig in to each function one at a time:
base.initialize = function(grid, opts) { //The headers need to be modified after the layout provider has created them. liteGrid = grid; options = opts; liteGrid.$el.bind("headerRendered", base.makeHeadersClickable); //An arrow that is used to indicate sort direction. base.sortArrow = $("<div class='sort-arrow ui-icon'/>"); //Will be used to store the column that was last sorted on. base.sortColumn = null; //True if the last sort was ascending, false otherwise. base.sortAsc = false; }
References to the options and grid are stored for use later, and the “headerRendered” event is bound so that the header can be modified. A piece of DOM is created that will visually indicate the sort direction using jQuery UI. Variables are also configured that will be used to indicate the sort direction and which column was sorted.
The next function wires up the sorting logic to the headers:
base.makeHeadersClickable = function() { //Loop through the headers and mark those that are sortable as sortable. $(options.columns).each(function(i) { if (this.sortable === true) { $("th:eq(" + i + ")", liteGrid.headerTable).addClass("sortable"); } }); //Bind a click handler to the sortable ones. $("th.sortable", liteGrid.headerTable).click(base.columnClicked); }
The corresponding <th> element for each sortable column is augmented with a “sortable” class. An event handler is then bound to these columns that will handle the actual sorting:
base.columnClicked = function() { var header = $(this); //Get the zero-based index and column that was clicked. var index = header.parent().children().index(header); var column = options.columns[index]; //If this is the same column we already sorted, toggle the direction if (base.sortColumn == column) { base.sortAsc = !base.sortAsc; } //Otherwise, reset it to sort ascending else { base.sortColumn = column; base.sortAsc = true; } if (base.sortAsc) { base.sortArrow.removeClass("ui-icon-carat-1-s"); base.sortArrow.addClass("ui-icon-carat-1-n"); } else { base.sortArrow.removeClass("ui-icon-carat-1-n"); base.sortArrow.addClass("ui-icon-carat-1-s"); } //Extract key/list pairs where the key is the value for the row being sorted, and the list is //the row and its children. var sortItems = []; //Used for quickly finding a row by its ID. var lookup = {}; //Each row is added either to the root sortItems array or to the children array of its parent. $("tr", liteGrid.$el).each(function() { var row = $(this); var dataItem = row.data("dataItem"); //This represents the row that will be sorted. var sortItem = { key: dataItem[base.sortColumn.field], row: row, children: [] }; lookup[dataItem[options.rowIdColumn]] = sortItem; //If the row doesn't have a parent, it goes in the root list, otherwise it gets added to //its parent sortItem if (row.attr("parentId")) { lookup[row.attr("parentId")].children.push(sortItem); } else { sortItems.push(sortItem); } }); //Recursively sort the rows in memory and re-append them to the table. base.doSortAndAppend(sortItems); //Add the sort indicator to the header. base.sortArrow.appendTo($("> div", header)); //The row order has changed liteGrid.$el.trigger("tableUpdated", liteGrid); }
This function looks complex, but it actually isn’t all that bad. First is some logic to handle the sort direction. The first time a column is clicked, sorting is done in ascending order. If the user clicks the same column again before clicking another column, the sort direction is reversed. Next, an array of root-level rows is built with the row’s value for the sortable field serving as the sort key. A lookup object is used so that child rows can be associated to the correct parent and sorted recursively. The actual work of sorting and arranging things in the grid is performed by doSortAndAppend:
base.doSortAndAppend = function(sortItems) { sortItems.sort(base.compare); $(sortItems).each(function() { var sortItem = this; //Add the row to the grid. sortItem.row.appendTo($("tbody", liteGrid.$el)); if (sortItem.children.length > 0) { base.doSortAndAppend(sortItem.children); } }); }
The built-in sort function does most of the work using a custom compare function. Each sorted item is added back to the grid, and if it contains children, is processed recursively. Items are compared using a simple function that returns –1 for less than, 0 for equal, and 1 for greater than. These values are very important, apparently. I ran into (surprise!) inconsistent sort behavior with IE8 when I used something different.
So, that’s it for RowSortModule, which is available right now on SVN. Go check it out!