Apr 3 2011

LiteGrid Roadmap

Category: liteGridMatt @ 06:16

The LiteGrid jQuery Grid plug-in is getting some new code as I drive towards the 1.0 release.  I’ve (finally) started on an MVC library to make working with LiteGrid within MVC applications trivially-simple, I’ve created a sample application to serve as live documentation, and a variety of NuGet packages will be part of the 1.0 release.  If you want to know more, read on.

What is LiteGrid?

As a refresher, LiteGrid aims to be an easy-to-rich grid extend plug-in that plays very nicely with ASP.NET MVC.  The JavaScript core is built using an event-driven, pluggable architecture that makes it easy to add or alter behavior by composing modules together.  Out of the box, it supports:

  • AJAX-based data provider supporting retrieval, updates (both multi-row batch and single-cell modes), updates, and inserts, with full support for retrieving and rebinding database-populated values post insert/update (more on this in a future post)
  • Batch saving module that allows users to persist their changes on-demand
  • BlockUI integration, nicely disabling access to the grid during data operations
  • Cell saving module that automatically saves changes whenever a cell is edited
  • Formatting module that allows you to specify anything from simple to advanced formatting rules
  • Editable via integration with JEditable
  • Layout manager that adds resizable columns, fixed header row, and more
  • Integration with jQuery UI
  • Row addition module that does exactly what it sounds like
  • Row deletion module that adds a delete button to all rows
  • Injection protection through escaping potentially dangerous characters, and unescaping them prior to edit
  • Dynamic row striping that maintains the correct striping even as the grid changes
  • Toolbar that supports custom buttons
  • Tree-grid functionality, allowing rows to be nested under other rows
  • Client-side sorting, even when the tree-grid module is used.

An easy-to-extend JavaScript programming model is only one of the goals I had for LiteGrid though.  The other goal was to make something that played very nicely with ASP.NET MVC.  This is an area where other jQuery grids disappoint.  Unfortunately, I really fell short of my original goal.  Though I highlighted many of the MVC-related classes I wrote, I never actually released any of the helpers I made for working with LiteGrid.  But that’s all going to change.

Towards 1.0

Thanks to the persistent reminders via E-mail, I’ve once again made LiteGrid a priority, and I’ll be working towards a 1.0 release later this month.  The 1.0 release will include a library of MVC classes (HTML helpers, ActionResults, etc) as well as a sample MVC 3 application that shows how to use LiteGrid.  I’ve just committed the first step towards this release to source control, so if you’re curious or can’t wait for the official release, feel free to check out the code.  There’s already a partially working sample application with a basic example of how to move data to/from LiteGrid and an MVC 3 application.  I’ll continue to flesh that sample out as the MVC library grows and things inch towards the 1.0 release.

What else is coming in the 1.0 release?  That’s a very good question.  In addition to MVC-specific helpers, I’m planning on doing a cleanup pass through the LiteGrid JavaScript and reducing the amount of code that’s needed for common scenarios (right now, you have to explicitly initialize each module you want to use, even the really common ones like row stripping).  1.0 will also ship with probably 3 NuGet packages: one for the core JavaScript library, one for the MVC library, and one for the sample application. 

Contributors Wanted

It’s very difficult to stay motivated on your own.  I think there’s huge potential for LiteGrid, especially within the ASP.NET MVC space, and I’d love for others to step in and help it realize that potential.  If you know JavaScript and/or MVC and would like to help out, please contact me. 

If there are other features people are interested in seeing for 1.0, please let me know, and I’ll do my best to include them.  In the meantime, look for future posts to highlight some of the things that are happening to the codebase as it moves towards the 1.0 release. 

(Oh, and I haven’t forgotten about Fluently-XML either.  The final post in that series as well as the source code release are both in the works.)

Tags:

Dec 27 2010

liteGrid Updated

Category: liteGridMatt @ 18:13

Thanks to the efforts of James Kolpack (whose blog has apparently imploded), liteGrid has been updated to the latest versions of jQuery, jQuery UI, and more.  The long-standing bug with the resizable columns not working correctly in Chrome has also been fixed.  You can check out a demo here.  Thanks, James!

Tags:

Jan 28 2010

Using liteGrid with ASP.NET MVC, Part 2 – The LiteGridUpdater

Category: liteGridMatt @ 10:54

Part 1 – The LiteGridJsonResult

In the last post, I showed how we use a custom JsonResult to send data to liteGrid.  Displaying data is great and all, but liteGrid supports client-side editing as well.  With the BatchSaveModule, liteGrid will batch up all the client-side changes and send them to the server in one big lump.  We need an easy, reusable way to process and apply those changes server-side.  Thankfully we now have the LiteGridUpdater class:

public abstract class LiteGridUpdater<TModel, TTarget> 
    where TModel : ILiteGridUpdateModel<TTarget> 
    where TTarget : class, new()
{
    /// <summary>
    /// Applies updates from the view models.
    /// </summary>
    /// <param name="models">The models containing changes.</param>
    /// <returns>The view models that were applied to the targets.  IDs for new entities
    /// will be synchronized to the real objects.</returns>
    public TModel[] ApplyUpdates(TModel[] models)
    {
        ...
    }

    /// <summary>
    /// Moves the object to a new parent.
    /// </summary>
    /// <param name="model"></param>
    protected virtual void MoveToNewParent(TModel model)
    {
        throw new NotImplementedException("Moving items to a new parent is not supported.");
    }

    /// <summary>
    /// Derived classes can implement this if they want to support
    /// adding new items from liteGrid.  Implementors must perform
    /// three actions: create a new TTarget; apply the updates
    /// from model to the new TTarget; synchronize the ID of the
    /// new TTarget with the model.
    /// </summary>
    /// <param name="model"></param>
    /// <returns></returns>
    protected virtual TTarget CreateAndUpdate(TModel model)
    {
        throw new NotImplementedException("Adding new items is not supported.");
    }

    /// <summary>
    /// Derived classes can implement this if they want to support deletes.
    /// If implemented, this should delete the specified target.
    /// </summary>
    /// <param name="model">The view model.</param>
    /// <param name="target">The object that should be deleted.</param>
    protected virtual void DoDelete(TModel model, TTarget target)
    {
        throw new NotImplementedException("Deleting items is not supported.");
    }

    /// <summary>
    /// Called before a model is updated.  You can transform the 
    /// model by overriding this method.
    /// </summary>
    /// <param name="model"></param>
    protected virtual void BeforeUpdateModel(TModel model)
    {
    }

    /// <summary>
    /// Implemented by derived classes, used to find the specific object
    /// that should be updated. 
    /// </summary>
    /// <param name="model"></param>
    /// <returns></returns>
    protected abstract TTarget GetTarget(TModel model);
}

This generic abstract class will take the data from liteGrid in the form of view models and apply the changes to your “backend” data types.  There are only two restrictions.  First, the view model must implement the ILiteGridUpdateModel interface (shown below), and the backend type must have a parameterless constructor. 

The LiteGridUpdater is implemented using the Template Method Pattern.  The core logic of applying the updates is implemented in LiteGridUpdater:

public TModel[] ApplyUpdates(TModel[] models)
{
    if (models == null)
    {
        throw new ArgumentNullException("models", "No models were received by the server.  Please try again.");
    }

    //First, apply any updates and moves. Note that because liteGrid sends things in order,
    //we are guaranteed to add a new object before we'll process anything that was added as a child
    //to the new object.
    foreach (var updatedModel in models)
    {
        BeforeUpdateModel(updatedModel);

        //Get the existing object or create a new one.
        TTarget target = GetTarget(updatedModel);

        if (target != null)
        {
            updatedModel.MapTo(target);
        }
        else
        {
            CreateAndUpdate(updatedModel);
        }

        //Apply any moves
        if (updatedModel is ILiteGridMovableModel<TTarget>
            && ((ILiteGridMovableModel<TTarget>)updatedModel).NewParentId.HasValue)
        {
            MoveToNewParent(updatedModel);
        }
    }

    //Now apply any deletes.
    foreach (var updateModel in models)
    {
        if (updateModel.Deleted == true)
        {
            DoDelete(updateModel, GetTarget(updateModel));
        }
    }

    return models;
}

Derived classes are responsible for implementing the details for how to perform the various operations, which include create a new entity, deleting an existing entity, and moving an entity (in the case of hierarchical entities).  Derived classes don’t necessarily have to implement all the operations.  Obviously you don’t need to worry about MoveToNewParent if your entities aren’t hierarchical.  If you aren’t allowing the users to add new rows, then you don’t need to worry about implementing the CreateAndUpdate method, either.  The same goes for DoDelete.  The BeforeUpdateModel method is optional.  You can use it to hook in and perform custom actions prior to applying updates (we use it to hook in validation).  In fact, the only operation you have to implement in a derived class is GetTarget.  This operation receives a view model and is responsible for locating the corresponding backend entity and returning it.  In our case, this involves looking the entity up using Castle ActiveRecord, but LiteGridUpdater is not coupled to any data access strategy.

Updates to existing entities are handled by the view model.  The ILiteGridUpdateModel defines a single method for performing the updates on an entity:

public interface ILiteGridUpdateModel<TTarget>
{
    /// <summary>
    /// True if the model was deleted. 
    /// </summary>
    bool? Deleted { get; }

    /// <summary>
    /// Maps the view model to the specified target.
    /// </summary>
    /// <param name="target"></param>
    void MapTo(TTarget target);
}

We use AutoMapper in our concrete liteGrid view models, but you are free to implement the update logic as you see fit.  Here’s a made up example for a Widget:

public class WidgetViewModel : ILiteGridUpdateModel<Widget>
{
    public int WidgetId { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    public bool? Deleted { get; set; }

    /// <summary>
    /// Configures AutoMapper mappings.
    /// </summary>
    static WidgetViewModel()
    {
        Mapper.CreateMap<WidgetViewModel, Widget>();
    }

    /// <summary>
    /// Creates an uninitialized view model
    /// </summary>
    public WidgetViewModel()
    {
        
    }

    /// <summary>
    /// Maps the view model to the specified target.
    /// </summary>
    /// <param name="target"></param>
    public void MapTo(Widget target)
    {
        Mapper.Map(this, target);
    }
}

After LiteGridUpdater applies the updates, it returns the same view models that it received.  These should be pumped back to liteGrid so that it knows what changed and can update its state accordingly.

Prior to creating LiteGridUpdater, we had quite a bit of repeated, complex logic in our system for dealing with liteGrid updates.  Now that we have LiteGridUpdater, we have a cleaner, more robust solution, and a lot less duplicate logic. 

Again, I really need to find the time to move LiteGridUpdater as well as some of our other liteGrid infrastructure into the liteGrid project as I originally intended.  Maybe someday soon I’ll find that mythical block of “free time” that everyone else seems to be enjoying.

Tags:

Jan 21 2010

Using liteGrid with ASP.NET, Part 1 &ndash; The LiteGridJsonResult

Category: liteGridMatt @ 11:25

When I first started building liteGrid, one of my design goals was to build a grid library that worked easily with ASP.NET MVC.   I’m not sure I’ve delivered on that goal, at least not externally.  Internally, we’ve built quite a lot of MVC plumbing that makes working with liteGrid easier.  We have custom JSON result types, view models, helpers, etc.  Sadly time has not yet permitted me to migrate most of these improvements back into the (empty) Visual Studio .NET project that’s in liteGrid, but I plan to start doing that as soon as I have time to do some cleanup and refactoring.  In the meantime, I thought I would start showing off some of the plumbing that we’ve built, starting with the LiteGridJsonResult.

The LiteGridJsonResult is used to send data to an instance of liteGrid.  The intended use is something like this:

//Get your objects from wherever
var domainObjects = GetDomainObjects();

return new LiteGridJsonResult(
    domainObjects.Select(d => new MyCustomViewModel(d)).ToArray()
);

Note that we’re converting our domain objects to view models, then sending the view models to liteGrid through the result.  Client-side, this means that liteGrid is going to receive data that looks exactly like the view models we’re working with server-side.  This allows us to build our column definitions, validation rules, etc. from our view models in a strongly-typed way. 

The actual implementation looks like this:

/// <summary>
/// A custom JsonResult that returns data in a format that 
/// liteGrid expects.
/// </summary>
public class LiteGridJsonResult : StandardJsonResult
{
    /// <summary>
    /// The data items being sent to the grid.
    /// </summary>
    public Array DataItems { get; private set; }

    /// <summary>
    /// Creates a result with the specified data items. 
    /// </summary>
    /// <param name="dataItems"></param>
    public LiteGridJsonResult(Array dataItems)
    {
        DataItems = dataItems;
    }

    /// <summary>
    /// Copies the values into the Data anonymous object. 
    /// </summary>
    /// <param name="context"></param>
    public override void ExecuteResult(ControllerContext context)
    {
        //The base class serializes the DataItems to JSON
        //and assigns it to a 'dataItems' property in the result.
        Properties.Add("dataItems", DataItems);

        base.ExecuteResult(context);
    }
}

The base class, StandardJsonResult, is a specialization of MVC’s JsonResult that we’ve created.  It exposes a Boolean Status property that indicates success/failure of a JSON operation in a consistent way (poor choice of name in hindsight, but it serves the intended purpose).  It was originally created when we were using anonymous types instead of view models, and was intended to make it easier to test that objects were being projected correctly (yeah, I know, bad idea).  It’s on my TODO list to rework it.  For now, suffice to say that it takes care of projecting our view models into JSON that looks like this (the format that liteGrid expects):

{
 status: true,
 dataItems: [{ID: 1, Name: "Name", Title: "Title"}, {ID: 2, Name: "Name", Title: "Title"}]
}

LiteGridJsonResult is still a little rough around the edges (this blog post actually reminded me of how much work we still need to do), but it works.  In the next post, I’ll show you how we handle synchronizing changes from liteGrid back to the server-side object model.

Tags:

Jan 15 2010

Work-around for jQuery UI Draggable refresh limitation

Category: liteGridMatt @ 10:01

The jQuery UI draggable plug-in is used within liteGrid by the DraggableRowsModule.  It enables users to rearrange the order of rows as well as to place rows into a hierarchy (if the TreeGridModule is installed). 

Recently a request came through to make liteGrid scroll when you are dragging rows.  This becomes very important in large grids when you want to make a newly added row a child of the first row in the grid.  Without scrolling, you must resort to repeatedly drag-and-dropping your way up the list.  This feature sounded easy enough to implement, but like most things dealing with tables, HTML, and CSS, it wasn’t.

First there was the draggable’s ‘scroll’ option.  In the demo, using this option achieves exactly the result I was after, but it didn’t work for liteGrid.  As usual, I believe this to be a limitation of using the plug-in with a table element instead of with a standard block-level element.  So, until I find time to rewrite the liteGrid rendering to use div’s instead of a table, that solution was out of the question.

Adding the logic necessary to make liteGrid scroll was actually quite easy to do.  In the DraggableRowsModule, I added the following to the drag event handler:

//If we're dragging outside of the grid, scroll.  We can't
//use the built-in scroll capabilities of draggable because 
//its not the table that we need to scroll, but rather the parent. 
var body = base.liteGrid.bodyDiv;
var bodyOffset = body.offset();
var bodyHeight = body.height();
var currentScroll = body.attr("scrollTop");

var rowOffset = ui.helper.offset()

//If we're above the body div, scroll up.
if (rowOffset.top < bodyOffset.top) {
    body.attr("scrollTop", currentScroll - base.options.dragIncrement);
}
else if (rowOffset.top > (bodyOffset.top + bodyHeight)) {
    body.attr("scrollTop", currentScroll + base.options.dragIncrement);
}

This worked, except when the drag handle caused the window to resize.  When that happened, suddenly the drop targets no longer lined up with the correct elements.  You can see what I mean here:

I'm pretty sure that's not where my mouse is...

After some digging, I found the ‘refreshPositions’ option in the draggable plug-in, but the documentation states it can have major performance impacts.  Well, the documentation is right.  For a grid with a lot of rows, the performance was not acceptable.  What I really wanted was a way to tell draggable to update the positions on demand as opposed to every time; after all, unless the grid scrolls, there’s no need to refresh the positions.  There is a work item queued up to add this functionality to jQuery UI, but no activity has been performed on it to date. [sad faec]

I tried a few different means of hacking it but ran into failures every time before I finally settled on this solution:

//This is wired in by the module's init function
base.dragGhost = function(event, ui) {

    var dragHandle = $(event.target);

    //If dragging caused a scroll, the positions were refreshed already,
    //so we can turn thi sback off..
    if (dragHandle.draggable('option', 'refreshPositions')) {
        dragHandle.draggable('option', 'refreshPositions', false)
    }

    //--SNIP: Boring code to handle adding children to a row--

    //--SNIP: More boring code to grab offsets, see snippet above

    if (rowOffset.top < bodyOffset.top) {
        //Here's the interesting part
        dragHandle.draggable('option', 'refreshPositions', true);
        body.attr("scrollTop", currentScroll - base.options.dragIncrement);
    }
    else if (rowOffset.top > (bodyOffset.top + bodyHeight)) {
        //And again
        dragHandle.draggable('option', 'refreshPositions', true);
        body.attr("scrollTop", currentScroll + base.options.dragIncrement);
    }
}

When the grid scrolls, I temporarily enable the refreshPositions option.  The next time the drag handle moves, jQuery UI will refresh the drop target positions as desired, and my event handler disables the option again.  This achieves the desired effect: when dragging-and-dropping, the droppable positions are only re-calculated when the grid is scrolling, otherwise they are left alone.  This can still cause a slow down during grid scrolling, but it’s the only solution I’ve found so far.

This feature is available now on the liteGrid trunk.

Tags:

Jan 15 2010

Recent liteGrid changes

Category: liteGridMatt @ 04:09

Despite the lack of noise on the topic, liteGrid has received a host of updates recently thanks almost entirely to the efforts of James "poprhythm" Kolpack.  I’ve updated the online demo of liteGrid so you can play with some of the recent changes, which include validation support, checkbox columns, and a slew of other minor improvements.

Tags:

Oct 19 2009

The long road to fixing delete behavior in liteGrid

Category: liteGridMatt @ 03:46

A lot has been going on with liteGrid recently.  The application at my day job that necessitated its creation is nearing production, and numerous additional features are being implemented in support of finishing that app.  One of the big things that recently came up was making sure delete operations worked when the grid was in “tree grid” mode (meaning the TreeGridModule was loaded).  While liteGrid has supported deletes for some time, it didn’t work correctly with tree grids.  When I was tasked with fixing this deficiency, I initially thought “this will be easy, I just need to remove the children when the parent is deleted!”  As is often the case in software, this was a gross oversimplification of the issue.

Why so complex?

If the grid was static, yeah, it would be a  simple matter of deleting children once a parent has been deleted.  Unfortunately, the grid is no longer static.  Users can add new rows (via RowAdditionModule) and now they can re-arrange rows and change the parent/child relationships (via DraggableRowsModule).  Deletes in liteGrid have always been a synchronous operation, while everything else is asynchronous.  Why is this bad?  Well, think about it for a second.  Let’s assume I have the following structure initially:

flat 

This is the structure the server knows about: I have 4 widgets that are not related.  Now let’s say that I re-arrange things like so:

nested

Let’s assume I haven’t saved yet.  At this point, the client has a very different view of the relationships than the server does.  The client knows that everything is now nested under Test 1 while the server still thinks everything is a singleton leaf.  Under the old model, what happens when I click the delete button for Test 1?  The server will remove Test 1, and that’s it. As far as it knows, Test 1 had no children.  But the client knows that Test 1 had children and will assume that they’ve been deleted, too.  See the problem?

Let’s consider another case.  Say we start out with the parts nested as in the second figure, but flatten them out so that they are all singleton leaves as in the first diagram.  Again, until we save, the server and client have very different views.  If we delete Test 1 on the client, the server will also delete Test 2, 3, and 4, since it thinks those items are nested under Test 1. 

The root of the problem is that deletes are synchronous while all other operations are batched. 

The solution

Once the root problem is well-understood, fixing it was seemingly straight-forward: make deletes a batch operation just like adds, moves, and edits.  Batching everything allows the server to update its state so that its view of the world matches the client before it starts nuking things.  While this sounds easy, it was, in fact, not. 

First, there were the expected problems with Internet Explorer.  I won’t dive into too many details, suffice to say that Internet Explorer 8’s table rendering is absolute junk.  I don’t know how, but they actually made it worse than it was in IE 7. 

Ignoring the IE8 problems, I still had to do a fair amount of work to get the user experience that I wanted.  Since deletes would be batched until the user clicked the Save button, I wanted deleted rows to be clearly visible, effectively disabled (meaning they couldn’t be edited or moved), and I wanted the delete operation to be undoable.  This would have been fairly simple if not for the tree grid module, which makes everything more complex.  HTML tables really aren’t well-suited for rendering nested sets like this. 

You have no idea how hard this was...

Disabling the rows was a problem.  I didn’t want the delete module to have to know about all the current and future modules that might have added behavior to the grid, so removing the behavior manually was not an option.  Instead, I used jQuery’s clone method to create a copy of the deleted row and its children.  The clones maintained the classes and data of the originals but none of the event handlers, effectively disabling them.  I replaced the original rows with the clones and provided a button to undo the delete operation.  When clicked, the rows are re-bound to their data items, which (thankfully) restores all the functionality added by other liteGrid modules.  At the end of the day, I had a nice batch solution that was not much more complex than my original solution for handling deletes. 

You can check out the latest demo online.  Code as always is available on the SVN repository.

Tags:

Oct 9 2009

liteGrid &ndash; now with draggable rows!

Category: liteGridMatt @ 05:14

I’ve just finished up the draggable rows module for liteGrid.  You can check out the demo page here.  The module supports the tree-grid module (but does not require it).  It’s built using jQuery UI’s draggable and droppable behaviors.  I’ve tested it on IE 8, Firefox 3.5, and Chrome, and it seems to work fine everywhere.  If you find a bug, please report it.

As part of this change, I’ve altered the behavior of the tree-grid module.  Prior to this release, it added it’s own column for the tree expander.  As of this release, it (and the draggable rows module) requires that you define a “utils” column.  Both the drag handle and the row expander will be added to this column.  If the column isn’t defined, an error will be logged to the console, and both modules will abort initialization. 

In other liteGrid news, I noticed that the resizable columns are no longer working Chrome.  I will try to fix this ASAP.  If anyone would like to tackle that bug, please feel free.

Tags:

Sep 28 2009

(My) Javascript Stupidity

Category: liteGridMatt @ 06:48

I just fixed a stupidly obvious bug in liteGrid that shows up when you have multiple grids on a page.  Look at the default options for liteGrid:

$.inrad.liteGrid.defaultOptions = {
    columns: [],
    dataProvider: new NullDataProvider(),
    modules: [],
    missingValue: "",
    rowIdColumn: "ID",
    layoutProvider: new BasicLayoutProvider()
}

The bug is right there.  Keep looking.  Give up?  Wait, you already see the bug?  Crap.  Yeah, well, I don’t consider myself a JavaScript expert, and that’s a good example of why.

For those that are like me and didn’t see it, the problem is the use of “new” in the options.  What I meant for this code to say was “every liteGrid by default gets a new NullDataProvider and a new BasicLayoutProvider”, but that’s not what that code says.  What it actually says is “every liteGrid by default gets a this NullDataProvider and a this BasicLayoutProvider.” That’s right, if you have multiple liteGrids that use the defaults, they will share the layout and data provider.  That’s not a problem for NullDataProvider since it doesn’t do anything, but that’s a big problem for BasicLayoutProvider, which handles the rendering and UI for the grid. 

I’m not sure what the best option is for handling this.  For now, I’ve had the option default to null, and a new BasicLayoutProvider is substituted during initialization, but I don’t like that.  I thought about changing defaultOptions to be a method that returned a new object containing the defaults, but then users can’t change the defaults without replacing the entire method.  Any suggestions?

Tags:

Aug 28 2009

liteGrid&rsquo;s RowSortModule

Category: liteGrid | jQueryMatt @ 06:31

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!

Tags: